diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e89a19..71a4dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +Cross-platform path fix (FN-029), caught by the Windows CI matrix that Linux-only local runs had been missing. + +- `--files` glob expansion wrote OS-native separators into `docs/TASKS.md` — backslashes on Windows. Because the committed scope block is matched against `git` output (always forward-slash) by the drive-check scope gate, Windows-declared scopes silently failed to match their own changed files, and the committed artifact was not portable across machines. +- The pattern is now normalized to forward-slash BEFORE globbing (so a Windows-style `src\dir\*.ts` still expands) and every stored path uses `Path.as_posix()`. The done-report backlinks written into `TASKS.md`/`PROGRESS.md` are normalized the same way. +- The FN-021 regression test now feeds a backslash-style pattern and asserts the committed Allowed-scope block contains no backslash, so the contract is enforced even on Linux-only runs (76 total). + ## v0.8.4 Close-before-report ordering fix: closes the fourth round of field findings (FN-027/FN-028) and corrects the root-cause analysis of FN-023. The theme: the closeout ledger runs from a snapshot taken before anything mutates, and every verdict printed matches what actually happened on disk. diff --git a/scripts/coderail.py b/scripts/coderail.py index 95338f5..a5cf598 100644 --- a/scripts/coderail.py +++ b/scripts/coderail.py @@ -345,7 +345,7 @@ def write_done_report(root: Path, shown: str, title: str, lines += [f"- {w}" for w in tdd_warnings] + [""] lines += ["## Full gate output", "", "```", gate_output.rstrip(), "```", ""] path.write_text("\n".join(lines), encoding="utf-8") - return str(path.relative_to(root)) + return path.relative_to(root).as_posix() # FN-029: portable path in TASKS/PROGRESS # ------------------------------------------------- blueprint coverage @@ -722,9 +722,14 @@ def cmd_start(args) -> int: raw_files += [f.strip() for f in chunk.split(",") if f.strip()] files: list[str] = [] for pat in raw_files: + # FN-029: TASKS.md is a committed, cross-platform artifact whose paths + # are matched against git output (always forward-slash). Normalize the + # pattern to forward-slash BEFORE globbing (so a Windows-style + # "src\x\*.ts" still expands), and store results forward-slash too. + pat = pat.replace("\\", "/") if any(ch in pat for ch in "*?["): matches = sorted( - str(p.relative_to(root)) for p in root.glob(pat) if p.is_file() + p.relative_to(root).as_posix() for p in root.glob(pat) if p.is_file() ) files += matches or [pat] else: @@ -1268,7 +1273,7 @@ def cmd_progress(args) -> int: else: checked = "retroactive entry - no verify commands were registered" if matching: - checked += f"; surviving report: {matching[-1].relative_to(root)}" + checked += f"; surviving report: {matching[-1].relative_to(root).as_posix()}" next_hint = snap.get("next_hint") or "decide with the user" accepted = list(zip(snap.get("accept_items", []), diff --git a/tests/test_structure.py b/tests/test_structure.py index ee8c020..fb26c95 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -1224,7 +1224,10 @@ def test_files_globs_expand_and_accumulate(): for n in ['director_core.ts', 'director_utils.ts']: (root/'src/director'/n).write_text('export {}\n', encoding='utf-8') r = cr('start', 'Glob scope task', - '--files', 'src/director/director*.ts', + # FN-029: a backslash-style pattern must still expand and, more + # importantly, must be stored forward-slash - TASKS.md is a + # committed artifact matched against git output on every OS. + '--files', r'src\director\director*.ts', '--files', 'docs/NOTES.md,README.md', '--verify', 'true') check(r.returncode == 0, r.stdout) @@ -1232,6 +1235,11 @@ def test_files_globs_expand_and_accumulate(): for expect in ['src/director/director_core.ts', 'src/director/director_utils.ts', 'docs/NOTES.md', 'README.md']: check(f'- {expect}' in tasks, f'missing expanded file {expect}: {tasks[-800:]}') + # Narrow to THIS task's Allowed scope block (the template's example + # task legitimately contains an escaped "\##" heading elsewhere). + allowed = tasks[tasks.index('Allowed:'):tasks.index('Forbidden:')] + check('\\' not in allowed, + f'FN-029: backslash leaked into committed TASKS scope: {allowed!r}') def test_shim_probes_candidate_homes():