88import subprocess
99from pathlib import Path
1010
11- from cecli .spec .steering import workspace_lib_missing
1211from cecli .spec .todos import ChecklistItem
1312
1413_PATH_IN_CHECKLIST = re .compile (
15- r"(?:`((?:lib|test)/[\w./-]+)`|((?:lib|test)/[\w./-]+))" ,
16- re .IGNORECASE ,
14+ r"`([^`\n]+)`|(?<![`\w])((?:[\w.-]+/)+[\w./-]+)" ,
1715)
1816
19- _SNAPSHOT_DIRS = ("lib" , "test" )
20- _MAX_LIST_FILES = 24
21-
22-
23- def list_workspace_test_files (workspace : str | Path , * , limit : int = _MAX_LIST_FILES ) -> list [str ]:
24- return _list_tree_files (Path (workspace ).resolve (), "test" , limit = limit )
17+ _MAX_TOP_LEVEL = 20
2518
19+ _SNAPSHOT_ORIENTATION = (
20+ "- **Note:** Top-level listing is orientation only — do **not** ContextManager-add "
21+ "these entries unless the checklist or implementation tasks name them."
22+ )
2623
27- def _list_tree_files (root : Path , subdir : str , * , limit : int = _MAX_LIST_FILES ) -> list [str ]:
28- base = root / subdir
29- if not base .is_dir ():
30- return []
31- out : list [str ] = []
32- for path in sorted (base .rglob ("*" )):
33- if not path .is_file ():
34- continue
35- if path .name .startswith ("." ):
36- continue
37- rel = path .relative_to (root ).as_posix ()
38- out .append (rel )
39- if len (out ) >= limit :
40- break
41- return out
24+ _NO_PATH_NEXT_ACTION = (
25+ "This checklist item names **no file paths**. Use **## Implementation tasks** "
26+ "(injected above) for deliverable paths for this step — pick **one missing file**, "
27+ "then **ContextManager create** → **ReadRange** → **EditText**. "
28+ "**Do not** skip to later numbered tasks."
29+ )
4230
4331
4432def paths_from_checklist_text (text : str ) -> list [str ]:
@@ -48,6 +36,8 @@ def paths_from_checklist_text(text: str) -> list[str]:
4836 raw = (match .group (1 ) or match .group (2 ) or "" ).strip ().rstrip ("/" )
4937 if not raw or raw in seen :
5038 continue
39+ if "/" not in raw and not re .search (r"\.[a-z]{2,5}$" , raw , re .IGNORECASE ):
40+ continue
5141 seen .add (raw )
5242 found .append (raw )
5343 return found
@@ -211,28 +201,19 @@ def resolve_implement_focus(
211201
212202
213203def dart_test_paths_for_focus (workspace : str | Path , focus : ChecklistItem ) -> list [str ]:
214- """Best-effort test file paths for a checklist item mentioning tests."""
215- paths = paths_from_checklist_text (focus .text )
216- test_paths = [p for p in paths if p .startswith ("test/" ) and p .endswith (".dart" )]
217- if test_paths :
218- return test_paths [:4 ]
219- all_tests = list_workspace_test_files (workspace )
220- lower = focus .text .lower ()
221- tokens = [
222- t
223- for t in re .split (r"[\W_]+" , lower )
224- if len (t ) > 3 and t not in {"write" , "unit" , "tests" , "test" , "for" }
225- ]
226- scored : list [tuple [int , str ]] = []
227- for path in all_tests :
228- path_lower = path .lower ()
229- score = sum (1 for tok in tokens if tok in path_lower )
230- if score :
231- scored .append ((score , path ))
232- if scored :
233- scored .sort (key = lambda x : (- x [0 ], x [1 ]))
234- return [p for _ , p in scored [:4 ]]
235- return [f for f in all_tests if f .endswith ("_test.dart" )][:4 ]
204+ """Dart test files explicitly named in the checklist and present on disk."""
205+ root = Path (workspace ).resolve ()
206+ if not (root / "pubspec.yaml" ).is_file ():
207+ return []
208+ out : list [str ] = []
209+ for rel in paths_from_checklist_text (focus .text ):
210+ if not rel .endswith (".dart" ):
211+ continue
212+ if deliverable_paths_exist (root , [rel ]):
213+ out .append (rel )
214+ if len (out ) >= 4 :
215+ break
216+ return out
236217
237218
238219def resolve_flutter_executable () -> str | None :
@@ -263,24 +244,29 @@ def build_workspace_snapshot_lines(workspace: str | Path) -> list[str]:
263244 lines = ["## Workspace snapshot (verified on disk — do **not** ls to rediscover)" ]
264245 from cecli .spec .pubspec_repair import pubspec_repair_snapshot_lines
265246
266- pubspec = root / "pubspec.yaml"
267- if pubspec .is_file ():
268- lines .append ("- `pubspec.yaml` — present" )
269- else :
270- lines .append ("- `pubspec.yaml` — **missing**" )
247+ try :
248+ entries = sorted (root .iterdir (), key = lambda p : (not p .is_dir (), p .name .lower ()))
249+ except OSError :
250+ entries = []
251+ top_names = [
252+ e .name + ("/" if e .is_dir () else "" )
253+ for e in entries [:_MAX_TOP_LEVEL ]
254+ if not e .name .startswith ("." )
255+ ]
256+ if top_names :
257+ preview = ", " .join (f"`{ name } `" for name in top_names )
258+ extra = f" (+{ len (entries ) - _MAX_TOP_LEVEL } more)" if len (entries ) > _MAX_TOP_LEVEL else ""
259+ lines .append (f"- **Top level:** { preview } { extra } " )
271260
272- for sub in _SNAPSHOT_DIRS :
273- files = _list_tree_files (root , sub )
274- if not files :
275- lines .append (f"- `{ sub } /` — **empty or missing**" )
276- continue
277- preview = ", " .join (f"`{ f } `" for f in files [:8 ])
278- extra = f" (+{ len (files ) - 8 } more)" if len (files ) > 8 else ""
279- lines .append (f"- `{ sub } /` — { len (files )} file(s): { preview } { extra } " )
261+ lines .append (_SNAPSHOT_ORIENTATION )
280262 lines .extend (pubspec_repair_snapshot_lines (root ))
281263 return lines
282264
283265
266+ def _flutter_project (workspace : str | Path ) -> bool :
267+ return (Path (workspace ).resolve () / "pubspec.yaml" ).is_file ()
268+
269+
284270def build_implement_next_action_lines (
285271 workspace : str | Path ,
286272 checklist : list [ChecklistItem ],
@@ -314,51 +300,57 @@ def build_implement_next_action_lines(
314300 )
315301 paths = paths_from_checklist_text (focus .text )
316302 on_disk = deliverable_paths_exist (workspace , paths ) if paths else False
317- test_files = _list_tree_files (Path (workspace ).resolve (), "test" )
303+ named_dart_tests = [
304+ p for p in paths if p .endswith (".dart" ) and deliverable_paths_exist (workspace , [p ])
305+ ]
318306
319- if is_test_related_checklist_text (focus .text ) and test_files :
320- target = next (( f for f in test_files if "test" in f ), test_files [0 ])
321- lines .append (f"Focus checklist: **{ focus .text .strip ()} ** — test file(s) already on disk." )
307+ if is_test_related_checklist_text (focus .text ) and named_dart_tests :
308+ target = named_dart_tests [0 ]
309+ lines .append (f"Focus checklist: **{ focus .text .strip ()} ** — ` { target } ` is on disk." )
322310 lines .append (
323311 f"1. **ReadRange** `{ target } ` with `@000` / `000@` once\n "
324- f"2. **EditText** only if tests need fixes\n "
325- f"3. BrightVision runs **`flutter test`** at end of this turn — **do not** run flutter via Command\n "
326- f"4. Mark this checklist item done **only after** BrightVision reports tests passed\n "
327- f"**Do not** call ls, Grep, GitStatus, or repeat ReadRange on the same file."
312+ f"2. **EditText** only if tests need fixes"
328313 )
329- elif on_disk and is_test_related_checklist_text (focus .text ):
330- test_files = _list_tree_files (Path (workspace ).resolve (), "test" )
331- target = next ((f for f in test_files if "test" in f ), test_files [0 ] if test_files else None )
332- if target :
314+ if _flutter_project (workspace ):
333315 lines .append (
334- f"Focus checklist: **{ focus .text .strip ()} ** — deliverable files already exist."
335- )
336- lines .append (
337- f"1. **ReadRange** `{ target } ` with `@000` / `000@` once\n "
338- f"2. **EditText** only if tests need fixes\n "
339- f"3. BrightVision runs **`flutter test`** at end of this turn — **do not** run flutter via Command\n "
340- f"4. Mark this checklist item done **only after** BrightVision reports tests passed\n "
341- f"**Do not** call ls, Grep, GitStatus, or repeat ReadRange on the same file."
316+ "3. BrightVision runs **`flutter test`** at end of this turn — **do not** run flutter via Command\n "
317+ "4. Mark this checklist item done **only after** BrightVision reports tests passed"
342318 )
343319 else :
344- lines .append (
345- f"Focus: **{ focus .text .strip ()} ** — create tests with **ContextManager** + "
346- "**ReadRange** + **EditText** (one file). **No ls.**"
347- )
320+ lines .append ("3. Mark this checklist item done **only after** edits succeed" )
321+ lines .append ("**Do not** call ls, Grep, GitStatus, or repeat ReadRange on the same file." )
322+ elif is_test_related_checklist_text (focus .text ) and paths and not on_disk :
323+ lines .append (f"Focus checklist: **{ focus .text .strip ()} **" )
324+ lines .append (
325+ "Create the test file(s) **named in this item** with **ContextManager create**, "
326+ "then **ReadRange** + **EditText**. **No ls.**"
327+ )
328+ elif is_test_related_checklist_text (focus .text ):
329+ lines .append (f"Focus checklist: **{ focus .text .strip ()} **" )
330+ lines .append (
331+ "Name target file path(s) in the checklist, then **ContextManager** / **ReadRange** / "
332+ "**EditText** on **one** file. **No ls.**"
333+ )
348334 elif on_disk :
349- target = paths [0 ] if paths else "lib/"
335+ target = paths [0 ]
350336 lines .append (
351337 f"Focus checklist: **{ focus .text .strip ()} ** — paths exist on disk (`{ target } `)."
352338 )
353339 lines .append (
354340 "**ReadRange** the target source file, then **EditText** to finish. **No ls.**"
355341 )
356- elif workspace_lib_missing ( workspace ) :
342+ elif paths and not on_disk :
357343 lines .append (f"Focus checklist: **{ focus .text .strip ()} **" )
358344 lines .append (
359- "**ContextManager ** to scaffold `lib/` (and `test/` if needed), then **ReadRange** + "
360- "**EditText ** on one file. **Do not ls** empty directories ."
345+ "Paths **named in this item ** are not on disk yet — use **ContextManager create** "
346+ "(not `add` on missing files), then **ReadRange ** + **EditText** on **one** target file ."
361347 )
348+ lines .append (
349+ "**Do not** ls, Grep, or ReadRange paths **not named** in this checklist item."
350+ )
351+ elif not paths :
352+ lines .append (f"Focus checklist: **{ focus .text .strip ()} **" )
353+ lines .append (_NO_PATH_NEXT_ACTION )
362354 elif resume :
363355 lines .append (f"Focus checklist: **{ focus .text .strip ()} **" )
364356 lines .append (
0 commit comments