Repository navigation
Portability: 74 path, encoding and git-output defects fixed across the guards and gates; portability suite as gate 11 - #173
Conversation
tests/portability/ holds cases that differ on other machines; _winsim.py loads a real module with ntpath swapped in, so Windows path behaviour is testable on Linux CI. The suite is registered in backtest.sh; count claims updated; ledger lists it as not yet qualified until its cases are shown to fail on the pre-fix code. Refs #171.
… rubric file, and checkouts stay LF check-repo-hygiene.py reads `git ls-files -z` decoded as UTF-8, so an accented name is no longer a phantom `"scripts/` directory and an accented draft name is reported under its own name; it joins git paths with posixpath, flags tracked paths that collide after NFC + casefold, and matches an archive README case-blind. check-ledger-coverage.py compares relpaths with '/' (PR #152's two hunks) and reads `ls-files -z` as UTF-8. The pre-commit reads its staged lists NUL-delimited, scores renamed and type-changed files (ACMRT), and fails loudly on a staged file missing from the working tree. quality_score.py dispatches on the suffix case-blind, exits 1 on a file it cannot score, and passes the path to R as an argument instead of splicing it into R source. The ledger gate and the scorer print UTF-8, so an em dash on a cp932 pipe no longer decides their exit code. .gitattributes forces LF in every checkout (`* text=auto eol=lf`, plus PR #152's shell rules); the index is already all LF, so nothing is renormalised. Findings fixed: ledger-coverage-relpath-backslash, ledger-coverage-relpath, ledger-coverage-ls-files-no-z, hygiene-ls-files-no-z, hygiene-lsfiles-quoted-nonascii, ls-files-locale-decode, hygiene-numbered-duplicate-ospath-join, hygiene-misses-case-colliding-paths, hygiene-archive-readme-case-divergence, precommit-rename-skips-quality-gate, precommit-quoted-names-skip-quality-and-battery, precommit-quality-gate-skips-non-ascii-names, precommit-quoted-staged-names, quality-score-case-sensitive-suffix, quality-score-rscript-backslash-path, no-gitattributes-crlf-shell, crlf-shell-scripts-break-non-msys-bash, gitattributes-partial-autocrlf, and gate-stdout-emdash-codepage for the two gates in this group (check-spec-conformance.py, file-issue.py, validate-findings.py and the battery's helpers still print em dashes to the console encoding). tests/portability/test_gitout.py: 29 cases. Against 08a7641, 26 fail and the 3 controls pass. All 29 pass now. Refs #171 Refs #151 Co-authored-by: JonOM123 <54511210+JonOM123@users.noreply.github.com>
… spellings of a gate path as it denies the literal one Every spelling below named .claude/hooks, .claude/settings*.json or .githooks to the shell that runs it, and the guard ALLOWED it silently while the literal twin denied. Now: - a backslash is also a separator: the token is tested a second time with `\` read as `/` (a second form, so no standing deny is lost); the `_REDIR` backstop unquotes its target first, as bash does - Git Bash drive paths (/c/..., /cygdrive/c/...) are read as the drive they name, in the scope half, the -C composition, the event cwd and CLAUDE_PROJECT_DIR - -C values compose with "/" (PR #152's join), so `git -C .claude -C hooks rm ...` is .claude/hooks on Windows too - program names go through one `_prog()`: basename, casefold, `.exe` dropped (rm.exe, GIT.EXE, bash.exe -c, and the r19 NICE/BASH residual) - a CR is deleted before anything reads the command (MSYS bash does) - the event is read as bytes and decoded UTF-8 with "replace" (utf-8-sig), not with the ANSI code page - names compare with casefold(), not lower() (APFS folds U+017F to s) - in_project compares NFC-casefolded forms, accepts the path as named or as resolved (a symlinked .claude/.claude/hooks/.githooks carrying the protected name is in scope), treats a root that ends in a separator as its own prefix (project at /, D:\, a share root), and falls back to file identity per ancestor (firmlinks, bind mounts, an aliased CLAUDE_PROJECT_DIR) The docstring's residual list is updated: the case-varied wrapper residual is closed; the PowerShell tool, drive-relative C:.claude, Win32 name aliasing (8.3 names, trailing dots) and NTFS's upcase table are disclosed as unprobed. Pinned by tests/portability/test_rot.py: 79 cases (native subprocess, ntpath simulation, and disk-independent scope units). On 08a7641 62 fail and the 17 that pass are all controls; all 79 pass now. A mutation run reverting each repair alone fails at least one case per repair. hook-battery 312/312. Fixed: rot-dash-c-ospath-join, rot-backslash-separators, rot-msys-drive-paths-out-of-scope, guard-stdin-codepage-fail-open, guard-stdin-mojibake-scope-bypass, exe-suffix-bypasses-guards, carriage-return-in-word-bypasses-guards, msys-path-spellings-bypass-root-of-trust, hook-stdin-ansi-codepage-fail-open, rot-unicode-casefold, rot-scope-string-compare, rot-symlinked-claude-dir, rot-windows-backslash-and-msys-paths, rot-project-at-filesystem-root, rotg-backslash-token-bypass, rotg-gitbash-drive-path-bypass, rotg-scope-case-sensitive-apfs, rotg-a54-compose-dash-C (root-of-trust-guard.py parts only; the git-guardrails / issue-guard / settings.json parts of the shared findings belong to other groups) Refs #171 Refs #151 Co-authored-by: JonOM123 <54511210+JonOM123@users.noreply.github.com>
… paths and code pages The guards now name the program by its basename on either separator with any `.exe` dropped, so `git.exe reset --hard`, a quoted 'C:\Program Files\Git\cmd\git.exe' and `gh.exe issue create` are denied like their bare twins; a carriage return inside a word is dropped the way Git Bash drops it; issue-guard is registered a second time for `Bash(gh.exe *)`, keeping the `Bash(gh *)` filter. Every python hook reads stdin as bytes decoded as UTF-8 and does its file and subprocess I/O in UTF-8, so a Windows code page no longer turns an accented path into mojibake or a CJK event into unparseable JSON. claim-reconcile watches backslash paths, compares project-relative paths as POSIX (PR #152's as_posix, applied to the current file) and case- and NFC-insensitively. The hardcoded-path check catches C:\Users\... with either slash in any case and .rmd / .DO extensions, without a global (?i). log-reminder logs readable names; statusline, notify and the -C Git Bash drive spelling work on Windows. Findings fixed: - guards-miss-dot-exe-program-names (git-guardrails, issue-guard, settings.json; the root-of-trust-guard sites are outside this change) - claim-reconcile-watch-regex-slash-only - claim-reconcile-watch-backslash (git-output and pr152-audit reports) - claim-reconcile-relpath-backslash - claim-reconcile-case-variant-paths - git-guardrails-msys-dash-c-false-deny - hardcoded-path-misses-single-backslash - hardcoded-path-check-misses-windows-and-case - log-reminder-quoted-and-cp1252 - nonguard-hooks-stdin-silent - compaction-plan-read-locale - statusline-stdin-locale - statusline-ctx-hash-mismatch-cp1252 - statusline-plan-badge-from-subdir - notify-sh-no-notification-on-windows Pinned by tests/portability/test_hooks.py: 43 cases, 36 fail on 08a7641 and all 43 pass here (the other 7 are controls). Refs #171 Co-authored-by: JonOM123 <54511210+JonOM123@users.noreply.github.com>
…hold on Windows paths and code pages check-links and check-skill-integrity resolve a target against the tracked files (git ls-files -z) in exact case, decode %20, and read <a b.md> and titled destinations; only tracked markdown is scanned. check-staleness matches its skip list against '/' paths, and it and stamp-render.sh hash content with CRLF folded to LF, identically on both sides. A BOM before frontmatter no longer hides it. validate-findings, file-issue and slide-qa read, write and decode UTF-8 explicitly, and none of the touched gates can be crashed by a cp932 pipe. validate-setup asks its own repository and compares the folder core.hooksPath names; it checks +x only on the .sh hook settings.json runs directly. sync_to_docs quotes the lecture name. The hook battery cds to its repository and drops an inherited CLAUDE_PROJECT_DIR, rewrites event paths to the native spelling under Git Bash with the sed pattern and replacement escaped (PR #152's rewrite), sets nativestrict on the two ln -s lines only, and reports c57-c64 UNREACHABLE on Windows while Linux and macOS still run them (312 cases either way). tests/portability/test_gates.py: 34 cases; 30 fail on 08a7641, all pass now (the 4 others are controls). Fixed: staleness-skip-regex-slash-only, staleness-skip-regex-vs-relpath, staleness-skip-backslash, staleness-crlf-hash, staleness-false-fail-on-autocrlf, links-percent-encoding-and-angle-brackets, gates-read-disk-not-index (check-links and skill-integrity; the hygiene half is another file's), links-gate-case-insensitive-exists, bom-frontmatter-misreport, finding-id-separator-sensitive, validate-findings-locale-read, file-issue-gh-decode, slide-qa-arrow-print, validate-setup-hookspath-string-equality, validate-setup-reads-cwd-repo, validate-setup-false-exec-bit-warning, sync-to-docs-unquoted-arg, battery-depends-on-invoker-cwd, battery-symlink-cases-unreachable-on-windows, battery-ev-sed-metachar; and the parts of gate-stdout-emdash-codepage in these files (spec-conformance, the battery's ctx/h5 helpers). Refs #171 Co-authored-by: JonOM123 <54511210+JonOM123@users.noreply.github.com>
… no rubric, and a dangling symlink is named as one Fix-up of 82a4590 after independent verification: - tests/portability/test_gitout.py: the two cp1252-locale cases patch locale.getencoding, which exists only from Python 3.11. On 3.9/3.10 (3.9.6 is macOS's /usr/bin/python3; README says 3.9+) both ERRORed, the portability gate went red and the installed pre-commit blocked every commit. They now skip below 3.11, where subprocess does not decode through that function and the case would pin nothing; CI on 3.12 keeps the pin. - scripts/quality_score.py: a file type with no rubric (.md, .py, ...) is skipped again, not exit 1. The case-blind suffix dispatch stays. The PR checklist's `quality_score.py <changed-files>` exited 1 whenever a non-rubric file changed; the pre-commit never passes such a file, so it gained nothing from the exit 1. - .githooks/pre-commit: a staged symlink named *.R/.qmd/.tex whose target is missing is reported as a symlink, not as "missing from the working tree". It still blocks, as before this fix-up. Tests: test_gitout.py now 30 cases (one replaced, one added). Against 82a4590's quality_score.py and pre-commit, exactly the two new cases fail; all 30 pass now. The suite under /usr/bin/python3 3.9.6: 82a4590 gave "2 of 31 cases FAILED"; now "ALL PASS (32 cases)", 2 skipped. Not changed here: no qualification-ledger row yet for the new checker behaviour (LEDGER.md is outside this group's files), and the staleness gate's STALE-RENDER on the guide, which dates from 08a7641. Refs #171
…t is one, and catches the ::$DATA and /proc/cygdrive spellings there Fix-up of 4f05d34 after an independent verification of it. Now: - the backslash form in matches_root_of_trust is added only where `\` really is a separator: native Windows Python (os.sep) or the Cygwin/MSYS runtime's own Python. Unconditional, it denied the ordinary escaped in-place script on macOS and Linux, which the in-place branch tests as an operand. Measured natively, each of sed -i '' 's/\.githooks/githooks/' docs/x.md sed -i '' 's/scripts\/hooks/.claude\/hooks/g' README.md sed -i 's/\.claude\/hooks\/old\.py/.claude\/hooks\/new.py/g' docs/guide.md perl -pi -e 's/old\/dir/.claude\/settings.json/g' README.md sed -i 's/foo/.claude\/hooks/' notes.txt was ALLOW at 08a7641, DENY at 4f05d34, and is ALLOW again. The docstring that called the POSIX cost "a quoted name that really contains a backslash" is corrected. - on those platforms the Windows form also drops an NTFS default-stream suffix from the last segment (`::$DATA`, `::$INDEX_ALLOCATION`, `:$I30:$INDEX_ALLOCATION`), so `echo x > '.claude\settings.json::$DATA'` and `rm -rf '.claude\hooks:$I30:$INDEX_ALLOCATION'` deny under the ntpath simulation instead of allowing silently. Not run on real Windows; a stream suffix mid-path joins the unprobed residual list. - a Git Bash drive path spelled through `/proc/cygdrive/c/...` is read as the drive it names, like `/c/...` and `/cygdrive/c/...`. - the PowerShell residual bullet no longer claims the permission layer covers that tool: settings.json has no rule for it and an empty deny list, so nothing in the repository guards it, and bypass and auto mode do not show the prompt that default mode would. tests/portability/test_rot.py: 100 cases (was 79). The native bs_* deny cases moved to WindowsTest (win_bs_mixed, win_bs_git_dash_c, win_bs_githooks_mv); natively they now pin that a backslash is part of a name, with the five sed/perl forms above and two controls. On 4f05d34 16 fail, every one a case added here; on 08a7641 69 fail; all 100 pass now. Reverting each repair alone fails at least one case (unconditional backslash 10, MSYS/Cygwin Python 1, stream suffix 7, /proc/cygdrive 2). hook-battery 312/312, portability 102/102; backtest fails only on the CHANGELOG "10 gates" inventory count. Refs #171
…names the gh spellings its filter never reaches Fix-up of 9d90fc3 after an independent verification of it. Now: - the hardcoded-path check's drive branch refuses a drive letter glued to a letter or digit. At 9d90fc3 a scheme's last letter read as a drive: `gs://users/x.csv`, `https://users/x`, `hdfs://users/x` and `{"key:/users/me": 1}` in a .py file warned, and denied under CLAUDE_STRICT_PATHS=1; 08a7641 never matched them. Every intended spelling still matches (a quote, space, `(`, `=`, `\\?\` or `file:///` before the drive). - issue-guard's docstring lists `GH`, `GH.EXE` and a quoted 'C:\Program Files\GitHub CLI\gh.exe' beside `/path/to/gh` as residuals. Claude Code 2.1.283 matches `Bash(gh *)` as a case-sensitive /^gh( .*)?$/ on the joined argv, so the hook is never spawned for them, though each runs gh on Windows (and `GH` on a case-insensitive macOS disk). The hook-level denies for them stay, marked as reachable only where the filter is ignored (Claude Code < 2.1.85). - two incident notes say what was measured: at 08a7641 `C:/Users/me` matched through the `/Users/` branch and `c:/users/me` did not; an undecodable `git status` never refused a clean tree (it prints nothing) but denied a dirty one for the wrong reason and denied an allowed `--autostash` over tracked-only dirt. - the test classes cite finding ids, not a gitignored plan's item numbers. Also fixed by 9d90fc3 but missing from its list: the git-guardrails and issue-guard halves of carriage-return-in-word-bypasses-guards, guard-stdin-codepage-fail-open, guard-stdin-mojibake-scope-bypass and hook-stdin-ansi-codepage-fail-open. Those findings were filed against root-of-trust-guard.py, whose own halves are handled separately. Not fixed here: it predates 9d90fc3 and is not a portability defect. A `)` or backtick glued to a command's last word hides a destructive git command on every platform. `(git reset --hard)`, `(cd sub && git reset --hard)`, `(git push origin main --force)`, `$(git reset --hard)` and echo `git reset --hard` in backticks are silent on a dirty tree at 08a7641, 9d90fc3 and here. It needs its own issue and battery cases. tests/portability/test_hooks.py: 46 cases (was 43). On 9d90fc3 three fail (test_filter_misses_are_disclosed, test_controls_stay_allowed, and test_every_class_cites_a_finding_id against the old docstrings); on 08a7641 39 fail; all 46 pass here. test_autostash_over_tracked_dirt_is_allowed reproduces the --autostash false deny with an ASCII locale (fails on 08a7641, passes since 9d90fc3). hook-battery 312/312, portability 48/48; backtest fails only on the CHANGELOG "10 gates" inventory count. Refs #171
…ong-case target fails; check-quotes reads the path the id names A UTF-8 BOM in front of a file's first '# Heading' no longer hides that heading's anchor: check-links (anchors_for and the scan read) and check-skill-integrity (collect_anchors and the check-3 read) read with utf-8-sig, as parse_frontmatter already stripped it. The link gates keep judging by the index, and now give the reason. A target that exists on disk but is untracked reads "not tracked by git (git add it; a clone and GitHub do not have it)". One that differs only in letter case from a tracked path names that path. check-links also names the untracked, unignored markdown it left out of the scan (git ls-files --others --exclude-standard, so a nested worktree counts as its own repository's). The verdict is unchanged. validate-findings --check-quotes reads a finding's file (and the other paths its evidence names) through the same canon_path the id uses. Slides\deck.tex and Slides/sub/../deck.tex are checked on macOS and Linux instead of "file not found". The battery's (e) comment cites the total as TOTAL=$((PASS + FAIL + UNREACH)), and says that c57-c64 on Windows are counted through UNREACH. tests/portability/test_gates.py: 38 cases, 4 new and 4 adjusted to the new reasons. 8 fail on the port/gates scripts (fcae2cc), and all 38 pass now. Not changed here: the STALE-RENDER on guide/ and docs/ was already present at 08a7641 (the guide qmd changed without a re-render) and needs a render and stamp outside this group. The cost of the two battery runs is unchanged: measured on the same machine, one combined run took 34.4 s and the two parallel runs 32.9 s. check-derived-counts.py still says PASS + FAIL, and the overlap with PR #152's hook-battery.sh hunk is left for the owner. Refs #171
… gates; Claude Code worktrees are gitignored TROUBLESHOOTING: the guards are wired to the Bash tool; on Windows the PowerShell tool is on by default (and the only shell tool without Git Bash), so commands run through it pass no guard — how to keep shell work on the guarded route (owner: document, do not deny). Refs #171.
…he portability work is recorded and qualified - git-guardrails: `(`, `)` and backticks separate segments, so `(git reset --hard)`, `$(git reset --hard)`, backticks and a substitution-carried merge are denied (Refs #172). Battery c69–c75; the pre-fix hook fails exactly c69–c73. Guide and docstring updated: the substitution residual is closed. - Cross-group leftovers from the #171 fix run: install-hooks.sh acts on its own repo, not the caller's cwd; /deploy quotes its argument; chmod advice covers only the shell hooks; finding-schema says ids read backslashes as '/'; a count comment cites PASS+FAIL+UNREACH; the guide names the Bash(gh.exe *) registration; repo-hygiene.md lists case/Unicode collisions. - LEDGER: qualification rows for the four fix groups, #172 and the portability runner (moved out of "not yet qualified"); battery count 312 -> 319 everywhere. - CHANGELOG v2.6.0: portability fixes and the eleventh gate (owner: still v2.6). - Guide re-rendered and re-stamped (the stamp was stale since 08a7641). Backtest exits 0: 11 gates, 319 battery cases, 216 portability cases. Refs #171, #151.
…e, and the records say 74 of 78 are fixed here Review of the portability PR (Refs #171): - .gitattributes: data/** -text, so a CRLF CSV under data/ is stored as deposited. - TROUBLESHOOTING: one-time renormalise for a clone checked out with autocrlf=true. - CHANGELOG: 74 of the 78 fixed here, 3 need the Windows launcher, PowerShell documented; timing stated per path (pre-commit ~20 s; direct backtest and CI ~1 min). - README, CLAUDE.md, guide: the portability suite named as gate 11 everywhere; its cases are pins beside controls, not all pins.
… colliding paths, validate-setup reads its own identity G3: check-links, check-staleness, check-spec-conformance and check-derived-counts spliced the clone's own path into their glob patterns unescaped, so a folder named "Paper [2026]" read as a character class. glob.escape(ROOT) at all four sites (not root_dir=, which is Python 3.10+). check-links and check-staleness now exit 2 when the scan finds nothing, so an empty scan can no longer read as a pass. G5: .githooks/pre-commit skips the stash --keep-index / pop cycle, with a warning, when tracked paths differ only by case or Unicode form (NFC + casefold over each path prefix, NUL-delimited: the hygiene gate's rule). On a macOS or Windows disk that tree is dirty for good, the pop always failed, and every commit left a "pre-commit-gate" stash holding the user's unstaged work. G8: validate-setup.sh asks git -C "$(dirname "$0")/.." for user.name and user.email, as its hooksPath check already did. Pinned in the new tests/portability/test_scripts.py (7 cases, each pin fails on 6cbebe1): the four gates run from a working-tree clone under "Paper [2026]", an empty scan exits 2, a gated commit on a case pair and on an NFC/NFD pair leaves no stash, and the identity is read from outside the clone and from another repository (plus an exported-copy control). Refs #171
…and skips by name what a host cannot run - G1, F1-portability-gate-git-index-leak: scripts/portability-tests.py drops the GIT_* variables it inherits (GIT_EXEC_PATH kept) before discovery. Under `commit -a`, `commit <paths>` and in a linked worktree the pre-commit hook exports an absolute GIT_INDEX_FILE (and GIT_DIR), and a gate run in-process through _winsim listed the user's index instead of its fixture's. Pinned by test_harness GateRunner.test_cases_do_not_read_the_callers_index (fails on the 6cbebe1 runner). In a throwaway repository committed through a copy of .githooks: 6cbebe1 `commit -a` aborted with "1 of 216 cases FAILED"; now `commit -a`, `commit -o <path>` and a linked-worktree commit all pass. - G6: the verdict line counts skipped cases, "ALL PASS (N cases, K skipped)", with one "skipped K: <reason>" line per reason, and an unexpected success fails the gate. Pinned by two GateRunner cases, plus a control that a clean run prints the line it always did. - F3-tests-vacuous-on-py39: the six test_hooks cases and the slide-qa case that rely on -X warn_default_encoding skip below Python 3.10; the file-issue case that patches locale.getencoding skips below 3.11 (test_gitout now shares the one definition in _winsim). FindingsEncoding still runs there: its cp1252 pipe pins the stdout half. Against the 08a7641 code under /usr/bin/python3 3.9.6, 7 of these 8 cases passed; now all 8 skip with a reason, and under 3.12 they still fail on that code. HostMechanisms checks that wherever these skips let a case run, the mechanism bites. - F4-gate11-unpassable-without-symlinks: _winsim probes os.symlink once. test_rot builds its link fixtures only where it can and skips the 7 cases that use them; the notify.sh fixture puts cat, tr and jq in as sh shims; the dangling-symlink pre-commit case skips on the probe. With os.symlink refused (WinError 1314): 6cbebe1 "6 of 116 cases FAILED" (setUpModule errored all of test_rot); now "ALL PASS (223 cases, 8 skipped)". Pinned by HostMechanisms.test_the_suite_runs_where_os_symlink_is_refused. The 8 NATIVE cases asserting POSIX backslash and stream rules also skip where the guard reads `\` as a separator (all 8 turn DENY under ntpath rules). - F7: the runner's docstring says each case is a pin, a control or a harness check; test_gates and test_gitout no longer claim every case fails before. Refs #171
…ty, and pops only a stash it made
G5 follow-up. The collision pre-check covered one cause of a tree that
`stash push --keep-index` cannot clean. Another is run.sh committed with CRLF
before `*.sh text eol=lf` (the template's own rule), and it happens on every
disk. The EXIT-trap pop failed, the commit went through anyway, and every commit
moved the user's unstaged work into another "pre-commit-gate" stash. The advised
`git stash pop` then refused as well.
Right after the push, the hook now checks `git diff`. If the tree is still
dirty it restores every path whose working copy differs from the stash's
(`git diff --name-only -z stash@{0}`), with
`git --literal-pathspecs restore --source=stash@{0} --worktree`, drops the
stash, and validates the working tree. It does not restore only the paths that
`git diff` no longer lists, as the verifier suggested: a still-dirty path the
user had also edited would lose that edit when the stash is dropped. The edited
subcase shows this on a copy of the hook built that way.
Also: `stash push` exits 0 without making a stash when nothing is staged
(`commit --amend`) and the only unstaged change is a submodule's dirty
content. The trap then popped the user's own newest stash into the tree. The
hook now counts a stash as its own only if refs/stash moved. The restore-and-drop
above needs that guarantee too.
Pinned in tests/portability/test_scripts.py by PreCommitDirtyForGood (plain
and edited-dirty-path subcases) and PreCommitMadeNoStash. Both fail on the
6cbebe1 and 679d5b2 hooks and pass here. HOOK now names the hook the cases copy.
Refs #171
…so they fail on the pre-fix gates in UTF-8 mode too - F3 fix-up (verifier): under PYTHONUTF8=1, -X utf8 or Python 3.15's default (PEP 686), subprocess decodes text as UTF-8 without asking locale.getencoding. The two test_gitout cases that patched it in the test process (HygieneLocaleDecode, LedgerGitOutput ..._under_cp1252) then passed on the 08a7641 gates. The HostMechanisms probe held its child at PYTHONUTF8=0, a mode those cases never ran in, so it still passed. _winsim.in_code_page now runs the patch in a child held in locale mode. Both cases and the probe go through it, and needs_getencoding stays a 3.11 check, so the pins still run on 3.15. Pinned by HostMechanisms.test_the_code_page_cases_bite_in_utf8_mode. It runs the two cases under -X utf8 against a stand-in with the 08a7641 reader, and it fails on the fe746d2 test_gitout (both cases 'ok'). Against the 08a7641 gates, both cases now fail under 3.12 and 3.14, in locale mode, PYTHONUTF8=1 and -X utf8, and under a PEP 686 stand-in interpreter (children in UTF-8 mode unless PYTHONUTF8=0). Audit of the subprocess cases that drop PYTHONUTF8 without setting it to 0: the fe746d2 suite was run per case against 08a7641, each first-cut commit (4f05d34, 9d90fc3, 82a4590, fcae2cc) and 6cbebe1, in locale mode, -X utf8, the 3.15 stand-in and 3.12. Only these two cases changed outcome. The new suite on 08a7641 gives identical per-case outcomes in all four modes. - F7 (the part in this group's files): the runner's docstring no longer says the qualification ledger gives the before/after counts, which it does not for test_harness. Refs #171
…iding paths, validate-setup identity (Refs #171)
…es, Python 3.9, no-symlink hosts; skips reported (Refs #171)
…losed residual The flat-separator fix for #172 (6cbebe1) was reviewed and backed out: it cut comments short, so git pull # (daily sync) was refused on a clean tree, and split a command's own arguments at a substitution, a new miss. A nested-substitution rework was tried and its verifier found new bypasses, so it is not shipped. The docstring and guide now list the subshell/substitution form as an open residual (Refs #172); battery cases c69–c75 removed. LEDGER: #172 row removed, battery 312, hooks row 38/46 with 5 controls, grading cell 312/312. Backtest exits 0: 11 gates, 312 battery cases, 233 portability cases.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7884b8f046
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return None | ||
| if r.returncode != 0: | ||
| return None | ||
| return {NFC(f) for f in r.stdout.decode("utf-8", "surrogateescape").split("\0") if f} or None |
There was a problem hiding this comment.
Preserve Unicode form when comparing tracked paths
When the index contains an NFD filename (possible on Linux or on macOS with core.precomposeunicode=false) and Markdown uses the visually equivalent NFC spelling, normalizing every ls-files entry collapses two byte-distinct paths. Consequently, an anchorless link such as [x](caf%C3%A9.md) passes resolves() when only cafe\u0301.md is tracked, even though GitHub and Linux treat the target as missing. Keep raw index spellings for resolution; normalization is suitable only for collision diagnostics.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Moderate issues remain in pre-commit path handling and Quarto output decoding, with related documentation and test-harness cleanup needed.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
This PR hardens repository hooks, gates, scripts, and documentation for Windows paths, Unicode, Git output, and line endings, adding portability testing as gate 11.
Changes:
- Adds Windows-simulation portability tests and expands hook coverage.
- Improves path, encoding, Git-output, and CRLF handling.
- Updates documentation, configuration, changelog, and generated guides.
| File | Summary |
|---|---|
TROUBLESHOOTING.md |
Documents PowerShell coverage and CRLF recovery. |
tests/portability/test_scripts.py |
Adds portability regression tests. |
tests/portability/test_harness.py |
Validates portability harness behavior. |
tests/portability/_winsim.py |
Simulates Windows path behavior; contains a duplicate load implementation. |
scripts/validate-setup.sh |
Resolves repository-local configuration. |
scripts/validate-findings.py |
Canonicalizes paths and UTF-8 I/O. |
scripts/sync_to_docs.sh |
Safely handles lecture-name matching. |
scripts/stamp-render.sh |
Normalizes line endings for fingerprints. |
scripts/slide-qa.py |
Improves portable reporting and encoding. |
scripts/quality_score.py |
Improves extension and path handling; Quarto output still lacks explicit UTF-8 decoding. |
scripts/portability-tests.py |
Adds the portability gate runner. |
scripts/install-hooks.sh |
Installs hooks from the script repository. |
scripts/hook-battery.sh |
Improves cross-platform fixture execution. |
scripts/file-issue.py |
Decodes GitHub CLI output explicitly. |
scripts/check-staleness.py |
Fixes path, EOL, and output handling. |
scripts/check-spec-conformance.py |
Handles escaped roots, BOMs, and encoding. |
scripts/check-skill-integrity.py |
Validates tracked links and encoded paths. |
scripts/check-repo-hygiene.py |
Adds collision detection and NUL-safe Git reads. |
scripts/check-links.py |
Resolves tracked and encoded paths safely. |
scripts/check-ledger-coverage.py |
Improves Windows path and Git decoding. |
scripts/check-derived-counts.py |
Makes gate counts path-safe. |
scripts/backtest.sh |
Registers portability as gate 11. |
README.md |
Documents the eleven-gate workflow. |
guide/workflow-guide.qmd |
Updates gate and hook documentation. |
guide/workflow-guide.html |
Regenerated guide output. |
docs/workflow-guide.html |
Regenerated published guide output. |
docs/index.html |
Updates the gate inventory. |
CLAUDE.md |
Updates backtest documentation. |
CHANGELOG.md |
Records portability changes; its #172 status needs correction. |
.render-stamp |
Updates rendered-artifact fingerprints. |
.gitignore |
Ignores Claude worktrees. |
.github/CONTRIBUTING.md |
Updates gate-count guidance. |
.githooks/pre-commit |
Improves staged-file and stash handling; path comparisons are not NUL-safe. |
.gitattributes |
Enforces LF text checkout policy. |
.claude/skills/deploy/SKILL.md |
Quotes deployment arguments. |
.claude/skills/commit/SKILL.md |
Documents portability gate execution. |
.claude/settings.json |
Registers the Windows gh.exe matcher. |
.claude/scripts/statusline.sh |
Improves UTF-8 parsing and root lookup. |
.claude/rules/repo-hygiene.md |
Documents case and Unicode collisions. |
.claude/references/finding-schema.json |
Documents canonical finding paths. |
.claude/hooks/session-handoff.py |
Uses explicit UTF-8 state I/O. |
.claude/hooks/pre-compact.py |
Uses explicit UTF-8 plan and state I/O. |
.claude/hooks/post-compact-restore.py |
Uses explicit UTF-8 restoration I/O. |
.claude/hooks/open-issues.py |
Improves UTF-8 event and CLI handling. |
.claude/hooks/notify.sh |
Adds Windows notification fallback. |
.claude/hooks/log-reminder.py |
Parses portable Git status output. |
.claude/hooks/issue-guard.py |
Handles Windows executables and UTF-8 events. |
.claude/hooks/git-guardrails.py |
Hardens Windows paths, executables, and encodings. |
.claude/hooks/context-monitor.py |
Uses explicit UTF-8 cache I/O. |
.claude/hooks/claim-reconcile.py |
Normalizes Windows and case-varied paths. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| _staged="$(git diff --cached --name-only -z 2>/dev/null | tr '\0' '\n' | sort)" | ||
| _unstaged="$(git diff --name-only -z 2>/dev/null | tr '\0' '\n' | sort)" | ||
| _overlap="$(comm -12 <(printf '%s\n' "$_staged") <(printf '%s\n' "$_unstaged") | grep -c . || true)" |
| def load(path, name=None, argv=None, as_main=False): | ||
| """Import the real module at `path` with os -> fake (ntpath).""" | ||
| path = _os.path.abspath(path) | ||
| name = name or ("__main__" if as_main else "mod_" + _os.path.basename(path).replace("-", "_").replace(".", "_")) | ||
| spec = importlib.util.spec_from_file_location(name, path) |
…safe pre-commit arrays, dedupe _winsim.load


Summary
.claudeor a project at/(reproduced on macOS). Accented file names confused the gates, and the quality gate skipped renamed and.rfiles silently. On Windows, the guards could fail open on code-page stdin, backslash and/c/spellings,.exenames and CRs.scripts/portability-tests.py,tests/portability/). Windows is simulated by loading the real modules withntpathswapped in, so a Windows defect fails on Linux CI.-Ccomposition, ledger relpaths, claim-reconcile path handling, battery event escaping). Everything is kept as v2.6 (owner).Changed defaults
.gitattributes:* text=auto eol=lf, withdata/** -textso raw data keeps its bytes. An existing clone checked out withcore.autocrlf=trueneeds a one-time renormalise (TROUBLESHOOTING).issue-guardis also registered forBash(gh.exe *).Not in this PR
( … )/$( … )/ backticks, pre-existing): a flat-separator fix and a nested-substitution rework were both tried and backed out after review. The first cut comments short and split arguments; the second opened new bypasses. It stays open and is disclosed in the docstring and guide.Type
fix/...)feat/...)docs/...)chore/...)Test plan
./scripts/backtest.shexits 0: 11 gates, hook battery 312 cases, portability suite 233 casesPYTHONIOENCODING=cp932 ./scripts/backtest.sh: no crashesquality_reports/qualification/LEDGER.md); independent verifiers per group; a five-lens review of the whole diff (23 confirmed findings, all fixed or backed out)quality_score.py100/100./scripts/install-hooks.shnot installed on this machine; the backtest was run by handNotes for reviewer
.claude/hooks/root-of-trust-guard.py(scope check by identity and case folding) and the newtests/portability/.