From 1231d5dcd3e66dfad30321d12010c45937c35b3c Mon Sep 17 00:00:00 2001 From: v0 Date: Tue, 14 Jul 2026 02:07:22 +0000 Subject: [PATCH] fix: normalize --files paths to forward-slash across platforms (FN-029) The Windows CI matrix caught a bug invisible to Linux-only local runs: --files glob expansion used str(Path.relative_to), which yields backslash separators on Windows and wrote them into the committed docs/TASKS.md scope block. The drive-check scope gate matches that block against git output (always forward-slash), so Windows-declared scopes silently failed to match their own changed files, and TASKS.md was not portable across machines. Patterns are now normalized to forward-slash BEFORE globbing (so a Windows 'src\\dir\\*.ts' still expands) and all stored paths use Path.as_posix(), including the done-report backlinks in TASKS.md/PROGRESS.md. The FN-021 regression test feeds a backslash pattern and asserts the committed Allowed scope has no backslash, enforcing the contract even on Linux (76 total). Co-authored-by: v0 --- CHANGELOG.md | 8 ++++++++ scripts/coderail.py | 11 ++++++++--- tests/test_structure.py | 10 +++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) 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():