Repository navigation
tests/test_path_guard_hygiene.py: each stat helper is loaded by its AST and asked with its stat refused, the scan follows every binding #1111 left out, and parity takes the correct spellings it refused (#1134) - #1149
Conversation
…ST and asked, the scanner follows every binding #1111 listed as missed, and the parity rules take the correct spellings they refused (#1134) - NIT 1: a behaviour test. Each `_is_a`/`is_a` found (the twelve and any added later) is loaded by its AST, with only the statements it reads run (`_slice`, `loaded_helper`): no shipped tool is imported, since each one's top level puts its folder on `sys.path` and imports `repo_shape` and the like. With `os.stat` and `os.lstat` refused for one name, each absent errno and Windows error must answer False, and EACCES, EPERM and EIO must come back raised, the same exception; a file, a folder and nothing answer as the disk does. `ROUTES` proves it names each route r1111b listed. - NIT 2: the scanner follows `o = os`, `from os.path import *`, `from os import *`, tuple unpacking, a walrus, a `for` or `with` target, a default parameter, a class or instance attribute, `importlib`, `__import__` and `sys.modules` by a constant name, and the later calls of a guard bound to a name. `PLANTED` holds one self-test each. The `_os_path_names` docstring now names the three `isjunction` lookups. On today's 25 tools it finds the same 190 sites as before. - NIT 3: parity takes `errno not in`, `raise exc`, a `frozenset({...})`, `import errno as E`, `except (FileNotFoundError, NotADirectoryError)` and an unrelated handler in a `try` that calls nothing; each beside a wrong twin that is still refused. - NIT 4: self-tests for r1111b's R2, R3, R7 and R13. - NIT 5: `FOLLOW_UP` must be `#<n>` and match the module docstring; `entry_problems` is proved on planted wrong entries. - NIT 6: the `HELPER` comment names its real shape. - r1111c's NIT 1: a `MODEL` tag for `_CloneGit.is_dir`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs a Windows error, a branch only a flag takes and the disk's answers, and the scan's a guard bound to an attribute (#1134) - `ROUTES` gains `winerror-dropped` (1921 answered as a refusal) and `flag-branch` (EACCES absent only with `link=True`), so dropping the Windows cases or the flag variants from `behaviour_problems` fails. - `test_the_behaviour_test_asks_the_disk_too`: a helper whose answer is the wrong way round is named by the three disk controls. - `PLANTED` gains `guard-as-attribute` (`class C: ex = os.path.exists`, then `self.ex(x)`), the one `Reach.attrs[GUARD]` case. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntry_problems`, so a typo in it and a value that is no issue are proved named on planted input (#1134) The two `FOLLOW_UP` asserts (`#\d+`, and named in the module docstring as "the follow-up") passed on the real value whether they ran or not: the mutant that deletes them (W33) survived. They are now part of `entry_problems`, whose self-test plants `#1101` (r1111b's Z11) and `1110`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThis test-only change substantially hardens path-guard hygiene by combining broader AST reachability and parity analysis with AST-isolated runtime checks for every stat helper, while adding self-tests for missed bindings, correct and incorrect spellings, allowlist integrity, and non-filesystem model methods. File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-22T08:35:25Z) r1149, lane 1's independent review of #1149 at
The full reviewr1149: review of PR #1149 (
|
| Claim | Result |
|---|---|
Only tests/test_path_guard_hygiene.py changes; no --help |
true |
None of the 12 modules imports without side effects (sys.path.insert, sibling imports; the doctor's sys.exit) |
true, all 12 by AST; doctor :466-487 |
_slice per file: 5 (adopt, family), 7 (update-shape), 5 (doctor), 4 (the other eight) |
true. logs/slices.log: no call runs at load in any slice; only import errno (plus import os for update-shape), two tuples, and the defs |
os.stat/os.lstat are patched; all 12 helpers stat through path.stat() (bootstrap's link=True through path.lstat(), which on 3.12 is os.stat(follow_symlinks=False)) |
true |
| ROUTES 12, PLANTED 18 | true (counted) |
| N1-N10 survive base and are killed on head | true: 10 of 10 each way |
r1111b's ten parity-passing wrong helpers are named; os_stat_follow is named for NameError |
true (logs/parity_behaviour.log) |
| Same 190 sites in 18 of 25 files, site for site | true: identical lists, order included |
alias_probe: base misses 14, head 0; 4 over-flags unchanged |
true |
Docstring names the 3 isjunction lookups, and they are the only ones |
true (grep: doctor :10305, siblings :822, :1393) |
| Base has 6 false parity refusals, head 0; each twin refused | true for the writer's twins (see NIT 2) |
| NIT 4: R2a, R3a, R7, R13 killed by the named tests | true |
NIT 5 and Z11 via entry_problems |
true (Z11, W21, W33a, W33b killed) |
MODEL: TAGS text and both reasons |
accurate. _CloneGit.is_dir is a set lookup (adopt-project.py:10195); BENIGN 175 (179); OWN 55 (56), MODEL 2 (2), DELIBERATE 63 (65), ASKED 25 (25), ROOT 20 (21), DEST 9 (9), PINNED 1 (1); KNOWN_OPEN 11 |
| 74 of 74 killed on head; 0 of 20 on base; sha256 restored every time | true (re-run with the writer's runner on my clones) |
| First run 72 of 73, W33 survivor | matches the writer's log; that log does not record the commit |
| Hygiene file 91 passed | true on head |
| The 14 helper files: 562 passed | true, on ccbd0eb0 itself |
| Doctor: 445 passed, 3 skipped | true: 111+1s, 112, 111+1s, 111+1s (the Windows PowerShell row) |
| Collisions 104 | true |
| Doc trio 170 | true |
| Timing 1.21x base | true in substance: I measured base 9.21/9.65/9.32 s (median 9.32) and head 11.32/10.90/10.31 s (median 10.90), so 1.17x. Load average was 4-6 from other lanes; the writer's exact seconds were not reproduced |
Exactly one Closes #1134, no other closing keyword |
true: closingIssuesReferences = [1134]; none in the title or the 3 commits |
Known limit: except (OSError, PermissionError) refused statically, passed by the behaviour test |
true (measured) |
| Known limit: the Z11 check compares spellings only | true (:1043) |
| Known limit: a helper with an unpatched stat is "named, not passed" | true for the primary stat (G6, G7). A fallback through lstat passes (SHOULD 2) |
My mutants: 11 killed of 25
Mutants of the new test code: 11 of 22 killed.
- Killed (11): V1, V2, V3, V4, V5, V6, V8, V9, V11, V17, V21.
- Survived (11):
- V7:
_readsignoresglobal; - V10:
_paireddoes not recurse into nested tuples; - V12: comprehension targets not followed;
- V13: keyword-only defaults not followed;
- V14: an annotated class attribute not followed;
- V15:
_membersunwraps any one-argument call; - V16: the
nothingdisk control dropped; - V18: the behaviour test asks only
HELPERS; - V19:
loaded_helperdoes not checkimport x; - V20: bare
posixpath/ntpath/genericpathnot modules; - V22:
_reraisestakesraise <any name>.
- V7:
Wrong-helper tool mutants in adopt-project.py: 0 of 3 killed. U1 (winerror 5), U2 (kind branch) and U3 (os.lstat fallback) all survive; these are SHOULD 1-2.
Every file was restored, and sha256 matched the HEAD blob for all 25.
Merge results
- With main: fetched as instructed;
origin/mainis still038dafba, the PR's base (ls-remoteagrees), so the merge is head itself and the runs above stand. - With shape-doctor.py and shape_advisory.py: a
.gitor an agent file linked into a folder this user cannot search is said once inwhat is hereoragent files,<name>: cannot be read (<reason>), never absent; a.gitoffers no way in (#1110) #1144's head89486f69:git merge-tree --write-tree ccbd0eb0 89486f69is clean, giving tree9cf9432a. I built it in a scratch clone with no commit;write-treegives9cf9432aback.- On it, the hygiene file plus shape-doctor.py and shape_advisory.py: a
.gitor an agent file linked into a folder this user cannot search is said once inwhat is hereoragent files,<name>: cannot be read (<reason>), never absent; a.gitoffers no way in (#1110) #1144's new file give 116 passed (91 + 25). - Base's and head's scanners agree site for site on shape-doctor.py and shape_advisory.py: a
.gitor an agent file linked into a folder this user cannot search is said once inwhat is hereoragent files,<name>: cannot be read (<reason>), never absent; a.gitoffers no way in (#1110) #1144's tools: 182 sites in 17 files, none unlisted, with KNOWN_OPEN down to 3. - shape-doctor.py and shape_advisory.py: a
.gitor an agent file linked into a folder this user cannot search is said once inwhat is hereoragent files,<name>: cannot be read (<reason>), never absent; a.gitoffers no way in (#1110) #1144 closes no issue, soFOLLOW_UP = "#1110"and the docstring still agree after it lands.
Hygiene
- Nothing was written to GitHub;
ghwas used read-only from a plain shell. - No commit, push,
rm,git configor worktree prune. - Every probe and test ran in the probe environment.
- Concurrency stayed at 4 or fewer pytest calls, each under 540 s.
df -iwas 64% before and after.- My worktree and all clones are clean.
mergedholds the predicted tree staged, as built.
Files are in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1149:
- Mutant runners:
probes/w_mutants.py(the writer's, repointed) andprobes/r_mutants.py(mine). - Probes:
probes/routes_probe.py,probes/routes_real.py,probes/twins_probe.py,probes/twins_base.py,probes/fix_proto.py,probes/compare_scan.py,probes/slices.py. - Logs:
logs/wmut_head.log,logs/wmut_base.log,logs/rmut_head.log,logs/gate_*.log,logs/timing.log,logs/merged_hyg.log. - Clones:
headc,headm,basec,basep,merged.
Lane: openreposhape-1 (openRepoShape-1)
…, a refused Windows error, both kinds and a link whose `stat` alone is refused, and each survivor of r1149's mutants has a self-test (#1134) r1149 found #1149 READY on condition: no MUST, two SHOULDs, four NITs. SHOULD 1: the behaviour test asked EACCES, EPERM and EIO and no refused Windows error, so `_ABSENT_WINERRORS += (5,)` or `_ABSENT_ERRNOS += (errno.ESTALE,)` passed both tests. It now asks every errno outside the four absent ones and EACCES with winerror 5 and 32, and expects the very exception back; a top-level augmented assignment to an `*ABSENT_*` constant is refused by the constants test, which now has a function and a self-test of its own. SHOULD 2: each refused case is asked with S_ISREG and S_ISDIR, and again of a real link to nothing whose `stat` alone is refused (its `lstat` answers a link; skipped where no link can be made), so a refusal answered from `lstat`, or read as absent for one kind only, is named. NIT 1: the stale docstring of the errno-set test. NIT 3: a self-test row for each survivor (`global` in `_reads`, nested tuples, comprehension targets, keyword-only defaults, an annotated class attribute, `_members`' unwrap, the `nothing` disk control, a helper added later, `import x` of a non-stdlib module, bare `posixpath`/`ntpath`/`genericpath`, `raise <another name>`). A failing run prints the first 20 problems and counts the rest. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-22T09:36:10Z) r1149b, lane 1's independent re-check of the fix round at
The full re-checkVerdict: READY. There is no MUST and no SHOULD. S1, S2 and all eleven NIT 3 survivors are closed at Scratch: Checks1. Diff.
2. S1.
3. S2.
4. NIT 3. On head, each mutant is killed by the row the report names. My runner printed "expected row among them: True" for each:
V1 to V22 are 22 of 22 killed once V1 to V3 were adapted to the moved code. 5. Over-reach.
6. Runs on head.
7. Body.
8. CI, as
FindingsMUST: none. SHOULD: none. NIT (optional, none blocks):
Lane: openreposhape-1 (openRepoShape-1) |
…iece of the helper by `ast.unparse`, not `ast.dump`, so a row passes on every Python CI's first run of the file on cd93516, on CPython 3.14.8 (run 39753985022), had one failure in 19769 tests: test_the_parity_test_refuses_another_set[change11-...`Name(id='EACCES', ctx=Load())` is neither `errno.<NAME>` nor a number] The row hard-coded what `ast.dump` prints up to 3.12. From 3.13 `ast.dump` leaves out `ctx=Load()` and the other default fields, so 3.14 built `Name(id='EACCES')`. Base 038dafb builds its messages with `ast.dump` too, but no row there asserts the dumped text, so main was green. The three sites that put a node in a message now use `ast.unparse`, which prints the source form of these expressions the same on 3.12, 3.13 and 3.14: `_members` (an element it cannot read, and a set that is no literal) and `_caught` (a handler that is no dotted name). The EACCES row says `EACCES` is neither `errno.<NAME>` nor a number. Each site is held by an exact text now, where only the EACCES row was: - a row for `sorted(_ABSENT_ERRNOS)` (`_members`, no literal set) and one for `except errors():` (`_caught`, which no row asked before); - the `frozenset({...}) | {errno.EACCES}` row and the `E.ENOENT` row of the twin table name the element they refuse, not just the tail of the message; - the `sorted` and `tuple` cases of the `_members` test compare the whole message. Checked the rest of the file for text that depends on the Python version: no other row asserts an AST dump or an exception's repr, and none depends on the order of a set or a dict. Proof on 3.12.3, the only Python here: `ast.dump` made to print as 3.13 does fails the old row with CI's text and passes the new file; `ast.dump` refused outright passes the whole file; 143 of 143 mutants are killed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-22T11:23:32Z) #1149's first CI run failed on Python 3.14, and
Lane: openreposhape-1 (openRepoShape-1) |
|
(lane openRepoShape-1, 2026-10-22T13:04:57Z) #1149 is READY at
#1149 stays a DRAFT with no milestone. It is outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z, and lands after the tag on a later word. Lane: openreposhape-1 (openRepoShape-1) |
GitHub's update-branch wrote 8d1af88 at 18:01Z but left the pull request's head at fa67fac, so update-branch now refuses; this is the same clean merge of main dea964a, made by hand to resync the pull request. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 18 hours and 7 minutes by commenting |



What
The follow-ups r1111b found when it re-checked #430's slice Z (#1111) (its NITs 1 to 6), and r1111c's NIT 1, collected in #1134. This is a test-only change: one file,
tests/test_path_guard_hygiene.py. No shipped tool changes, and no--helptext either. It is outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0"), and it has no milestone.NIT 1: a behaviour test
test_every_stat_helper_answers_absent_false_and_raises_a_refusalasks each_is_a/is_athat_helpersfinds. That is the twelve, and any added later.How each helper is loaded: by its AST, for all twelve files. None of the twelve modules is free of side effects on import. Each one's top level runs
sys.path.insertwith its own folder, then imports sibling modules from there:adopt-project.py,update-shape.py,scripts/bump-leg.pyandscripts/family.pyimportrepo_shapeandshape_materialize(adopt-project.pyalso importspath_classifyandshape_advisory);shape-doctor.pyimportsrepo_shape,shape_materialize,path_classifyandshape_advisory, and callssys.exitif one fails;scripts/shape_advisory.py, the assembly root'svalidate-pins.py,validate-manifest.pyandbootstrap.py, and the family root'svalidate-family.pyandbootstrap.pyimport their own template'srepo_shape;siblings.pyimportsrepo_shapeand the holder'sbootstrap.So the
repo_shapecopies inscripts/and in each template'sscripts/would meet insys.modules. Importing by path is right for none of them._sliceandloaded_helper._slicetakes the module's top-level statements that bind the helper, or a name one of them reads, repeated to a fixpoint and kept in the module's own order.loaded_helpercompiles them with annotations deferred and runs them in a namespace of their own. It refuses by name an import from outside the standard library ("a helper keeps its own copy", repo_shape.py'sRefusalgives every refusal built without a remediation the assembly root's own (run git submodule update --init --recursive, then python3 scripts/validate-pins.py ... contracts/<leg>-pin.yaml), so in a family holderfamily.py addover an unparsable.gitmodules(git-failed),bumpon afamily.yamlof the wrong kind, and the holder's ownvalidate-family.pyon a shortpin.commiteach tell the operator to run a file no holder carries and to move a leg pin a holder does not have #370, Pinned shape copies: AGENTS-shape.md'sCONTRACTS_DIRsentence is false for every scaffolded root, and the chain WARNING offers remedies the scaffold lacks (one shape bump before the v1.1.0 tag) #610).Why statements and not just the function. A later
_is_a = _lenientor_ABSENT_ERRNOS += ...runs too, as it would on import. What each file's slice runs, measured:adopt-project.py,scripts/family.pyimport errno,_ABSENT_ERRNOS,_ABSENT_WINERRORS,_absent,_is_aupdate-shape.pyimport errno,import os, the two constants,_says_absent,_mode_of,_is_ashape-doctor.pyimport errno,ABSENT_ERRNOS,ABSENT_WINERRORS,refused_read,is_ascripts/bump-leg.py,scripts/shape_advisory.py, the assembly root'svalidate-pins.py,validate-manifest.pyandbootstrap.py, the family root'svalidate-family.py,bootstrap.pyandsiblings.pyimport errno, the two constants,_is_aos.statandos.lstatare patched to raise for one name only;Path.statandPath.lstatcall them. Each case below is asked withstat.S_ISREGand withstat.S_ISDIR(r1149's S2).winerrorbesideEINVAL, an errno that is not absent, must answer False.errno.errorcode(126 here:EACCES,EPERM,EIO,EINVALandESTALEamong them), andEACCESwithwinerror5 and with 32 (Windows' access denied and sharing violation), must come back raised, as the very exception thestatraised (is). A set widened after it is set is named whichever errno it adds (r1149's S1).statthat follows it refused, so itslstatanswers a link. A helper that answers the refusal from thatlstatis named (r1149's S2). The link is made in the test's folder and skipped where none can be made; it is asked without a flag only, because a flag may asklstaton purpose (bootstrap.py'slink).bootstrap.pyhaslink=False.ROUTESholds 18 changes to theHELPERsample. They are the routes tests/test_path_guard_hygiene.py: a behavioural parity test, the scanner's other alias forms, the correct helpers parity refuses, and four self-test gaps (r1111b's NITs; follow-up of #430 slice Z) #1134 lists (or isinstance(exc, PermissionError), a deadreturn Falsebefore theraise,_ABSENT_ERRNOS += (errno.EACCES,),exc.args[0] ==,exc.strerror ==,contextlib.suppress(PermissionError), a method, a lambda,_is_a = _lenient), three more: absent and refused swapped, a Windows error dropped, and a branch only a flag takes; and six from r1149:ESTALEandEINVALadded to the errno set,winerror5 and 32 added to the Windows set, and a refusal read as absent forS_ISDIRonly and forS_ISREGonly. Each is named by the error it answers wrongly, a kind route for its kind and no other.LINK_ROUTESholds 4 changes that only a link shows: a refusal answered fromos.lstat, fromPath.lstator fromos.stat(..., follow_symlinks=False), and a link to nothing read as there (path.is_symlink()). Each is right for a name nothing is at and is named for the link alone.test_the_dangling_link_is_a_link_to_nothingholds the link itself.test_a_helper_that_stats_through_os_stat_is_right_for_a_link_too:os.stat(path)andos.stat(path, follow_symlinks=True)pass, so the link's refusal is of astatthat follows it and of no other.test_a_helper_added_later_is_asked_like_the_twelve: a fileHELPERSdoes not name is asked all the same, and its wrong helper is named by its file.absent_constant_problems: the constants test is a function now, so that the right sets, a wrong set and a top-level+=or|=on_ABSENT_ERRNOS,_ABSENT_WINERRORSor the doctor's spellings are proved on planted text.test_the_behaviour_test_loads_only_what_the_helper_reads: a module'ssys.path.insert, its sibling import and itsmaindo not run, and a helper that needsrepo_shapeis refused by name.test_the_behaviour_test_asks_the_disk_too: a helper that answers the wrong way round is named by the three disk controls.adopt-project.py, and ran the hygiene file. Under base's test file (038dafba) every route survives; under this head every one is killed:adopt-project.py_absent:or isinstance(exc, PermissionError)_is_a: a deadreturn Falsebefore theraise_ABSENT_ERRNOS += (errno.EACCES,)after the constants_absent:or exc.args[0] == errno.EACCES_absent:or exc.strerror == os.strerror(errno.EPERM)_is_a:with contextlib.suppress(PermissionError): raise, thenreturn False_is_a = _lenientat the end of the module_is_a:or _R1134_POLICY.lenient(exc), a method_absent:_r1134_lenient(exc) or ..., a lambda_is_a:if not _absent(exc), absent and refused swappedr1111b's parity probe variants tell the same story. Its ten "wrong" helpers that pass static parity are all named by the behaviour test. One of its "correct" variants,
os.stat(path), is named too, because the sample has noimport osand the call raisesNameError.NIT 2: what the scanner missed
Now followed.
_os_path_namesnow returns aReach: each name, and each attribute name, bound toos, toos.path(or its twins), or to a guard. It follows:o = os;from os.path import *andfrom os import *;fortarget over a literal, awithtarget (its value, or an argument of the call that makes it), a default parameter, or a class or instance attribute;importlib.import_module,__import__andsys.modules[...]or.get(...), each by a constant name;EX = os.path.exists,chk = isfile,IS_DIR = Path.is_dir,class C: ex = os.path.exists).PLANTEDholds 23 self-tests, one or more per form (r1149's NIT 3 added nested tuples, comprehension targets, keyword-only defaults, an annotated class attribute and the bareposixpath,ntpathandgenericpath). Each plants an example and asserts the exact sites found in its file, and nothing else.importlibandsys.modules: detected. A constant name is cheap to read:_looked_upis about twenty lines. A name that is not a constant, or an alias ofimportliborsys, stays on the known-limit list in the docstring.The
_os_path_namesdocstring is now true. It lists what is not caught. It no longer says "in no shipped tool today": it names the three Windows-onlygetattr(os.path, "isjunction", lambda _: False)lookups (shape-doctor.py'sunclonable, andsiblings.py'snot_a_cloneandfamily_link). Those three are the only ones r1111b'smissforms.pyand a grep find in today's tree.Re-scan of today's 25 tools. The new scanner finds the same 190 sites in 18 files as base's does, compared site for site: none only on head, none only on base. So it makes no new finding, and no
BENIGNorKNOWN_OPENentry changes for NIT 2.Measured against r1111b's
alias_probe.py. Base misses 14 of its cases; head misses none. The 4 cases it over-flags (a parameter namedpath, a localo, a localexists, and a storedo.exists) are unchanged from base. They are the safe direction r1111b noted.NIT 3: correct helpers that parity refused
On base, r1111b's
parity_probe.pygets 6 false refusals of correct helpers. On head it gets none.test_the_parity_test_takes_a_correct_spelling_and_refuses_its_twinasks each correct spelling of both the parity test and the behaviour test. Each is taken by both. Its nearest wrong twin is still refused by parity, by name, and named by the behaviour test (a twin further off is not always refused: see the known limits):errno not inNotInis read likeInnot in (errno.ENOENT, errno.EACCES): its errnos are wrongraise exc_reraisestakesraise <the handler's name>raise FileNotFoundError(...): it does not re-raiseraise exc, against another nameother = FileNotFoundError(...); raise other(r1149's V22)frozenset({...})constant_membersunwrapsfrozenset(...)andset(...)around a literalfrozenset({...}) | {errno.EACCES}: no literal setimport errno as E_errno_namesreads every nameimport errnobindsEwhose names are allerrno.EACCES: neithererrno.<NAME>nor a numberexcept (FileNotFoundError, NotADirectoryError)HANDLERStakes the two subclasses that are absent errnosexcept (FileNotFoundError, PermissionError)except KeyErrorin a function the helper callstrythat makes a call (_calls_out), which is the way from thestatto the answer_note'stry: x.stat()withexcept PermissionErrorNIT 4: self-test gaps
SPELLINGSbindslate = earlybeforeearly = os.pathSPELLINGShastyped: Any = os.path_helperswithoutClassDeftest_a_class_or_an_async_def_by_a_helpers_name_is_held_to_parity_membersskipping an elementEACCESby name, which must be refusedR2 and R3 are r1111b's edits to code this change rewrote, so they ran adapted to the new code with the same meaning (R2a, R3a).
NIT 5: the KNOWN_OPEN owner check
FOLLOW_UPmust passre.fullmatch(r"#\d+", ...), and a comment says family.py, the family root's siblings.py, shape-doctor.py and shape_advisory.py: a name in the root that is a link into a folder this user cannot search is still read as absent at twelve sites (follow-up of #430 slice Z) #1110's body mirrorsKNOWN_OPENsite for site.FOLLOW_UP = "#1101") and T6 (assert owner) would still survive:FOLLOW_UPmust match it;entry_problems, whichtest_each_wrong_allowlist_entry_is_namedproves on planted wrong input: a wrong tag, a wrong owner (#1101), an empty reason, a count of 0, a site on both lists, one in a helper, aFOLLOW_UPof#1101(Z11) and one of1110.NIT 6
The
HELPERcomment now says what the sample is: a helper inadopt-project.py's andscripts/family.py's shape (slices H and G), with its sets as module constants asked through an_absentof its own. It also says slice A'sscripts/bump-leg.pyasks the same sets inline.r1111c's NIT 1: a
MODELtagThe two
_CloneGit.is_direntries (_PlannedRoot._into_gitand_PlannedRoot._no_directory) are now taggedMODEL: "not a filesystem call at all: a method of the run's own model in memory that shares a guard's name and asks nostat".I chose a new tag over widening
OWN.OWNclaims something about whose path a guard asks: one the tool made or owns. These two ask no path at all; the scan flags them only because the method is namedis_dir. WideningOWNwould blur that claim and hide this second class inside it. A tag of its own keeps it countable, and makes a reviewer ask "is this really not the disk?" separately.BENIGNkeeps the same 175 entries (179 sites). By tag, as entries (sites), they are now OWN 55 (56), MODEL 2 (2), DELIBERATE 63 (65), ASKED 25 (25), ROOT 20 (21), DEST 9 (9) and PINNED 1 (1).KNOWN_OPENkeeps its 11.Review round 1: r1149's findings
r1149 read the PR at
ccbd0eb0and found it READY on condition: no MUST, two SHOULDs and four NITs. This round answers each one in the same file and changes nothing else. A scratch copy of a shipped tool carries each mutant; no shipped tool changes.EACCESwithwinerror5 and 32, and expects the same exception back. The constants test also refuses a top-level+=or `on anABSENT_` constant (the optional part), because only two refused Windows errors are askedstatwas asked withS_ISREGonly, and a fallback tolstatpassedS_ISREGandS_ISDIR, and again of a real link to nothing whosestatalone is refused (skipped where no link can be made)S_ISDIR), U3 (os.lstatfallback), r1149's G4 (posix.lstat) and G5 are killed by the behaviour test on the real file. All 12 real helpers passtest_every_stat_helper_holds_the_one_errno_setinandnot in;FileNotFoundErrorandNotADirectoryErrorbesideOSErrorandValueError; andraiseorraise exctwins_probe.pyon the head: all six named)bootstrap.py'slinkexistsNIT 3's rows:
_readsignoresglobaltest_the_slice_reads_a_global_a_function_changes_and_not_a_localPLANTED["nested-tuple"]PLANTED["comprehension-target"]PLANTED["keyword-only-default"]PLANTED["annotated-class-attribute"]_membersunwraps any one-argument calltest_members_unwrap_a_frozenset_or_a_set_and_no_other_callnothingdisk controltest_the_behaviour_test_asks_the_disk_too: a helper right only for the names the test refusestest_a_helper_added_later_is_asked_like_the_twelveimport xof a non-stdlib moduletest_the_behaviour_test_loads_only_what_the_helper_reads: a plainimport repo_shapeposixpath,ntpath,genericpathPLANTED["bare-os-path-twins"]raise <another name>raise-other-nameTests
CI's first run on
cd935163(Python 3.14.8, run 39753985022) found one failure:ast.dumpleaves outctx=Load()from Python 3.13, so the text a parity row hard-coded no longer matched, and the three messages that name a piece of the helper (_memberstwice,_caught) now useast.unparse, which reads the same on every Python; the row, one row for each of the other two sites, and the_memberstable assert that text.Mutants
Each mutant was applied to a scratch clone, the hygiene file was run on it (
-x), and the file was restored. Its sha256 then matched the HEAD blob each time: 136 of 136 on head and 20 of 20 on base.143 killed of 143 on head
fa67fac7, the head after CI's 3.14 fix, with 0 survivors. The 143 are the 136 below plus B1 to B7, each of which breaks one of the threeast.unparsemessages; R13 is adapted toast.unparse. Oncd935163: 136 killed of 136 on headcd935163; no survivor. The first run of this round, on the same code before its last four tests, killed 126 of 130. The four survivors were mine (F4b: onlyS_ISDIRasked;F8b: the link made to a file that is there;F10: the+=check not applied;F15: no absent case asked of the link). Each now has a test, below, and the whole set was run again.On head the 136 are: the 74 of the first writer's run (as it ran them, but for W15, W16 and W24, adapted to code this round moved, same meaning), r1149's 22 V and 3 U, six of its wrong-helper routes (G2, G2b, G4, G5, G6, G11), and 31 of mine on this round's code (F1 to F21, with variants). On base
038dafbaI ran 20 of the first 74: r1111b's ten survivors that apply to this file (R2, R3, R7, R13, Z1, Z2, Z3, Z6, Z7, Z11) and N1 to N10. Every one survived there. Atccbd0eb0r1149 measured 11 of its 22 V killed and 0 of its 3 U.038dafbacd935163SPELLINGS(R2a, R3a); the_helpersself-test (R7); a parity row (R13)bump-leg.py,validate-pins.py,shape_advisory.py)o = os,from os.path import *inpath_classify.py)FOLLOW_UP = "#1101")entry_problems: the docstring names #1110adopt-project.py(table above)PLANTED(W1-W7, W22, W23, W25-W30), the twin table (W9, W10, W12, W14, W15, W18, W31, W32), the parity rows (W8, W13),ROUTES(W16, W17, W19, W24, W34), the loader test (W20),entry_problems(W21, W33a, W33b), the helper parity test (W11); these are the killers named onccbd0eb0, and-xreports the first failing test in file order, so a test of this round can come first (W31: the_memberstable)ccbd0eb0)adopt-project.pyccbd0eb0)+=check)ESTALE), G2b (EINVAL), G4 (posix.lstat), G5 (os.lstat), G6 (primaryposix.stat), G11 (a fresh equal exception) inadopt-project.py+=check; G11 also parity)ROUTES(F1, F3, F3a, F3b, F3c, F4, F4b, F19), a right helper given a false problem in the parity twin table (F2, F3d, F7b),LINK_ROUTES(F5, F6, F7, F15), the link test (F8, F8b, F8c), the whole behaviour test on the realbootstrap.pywith its flag (F9), the self-test ofabsent_constant_problems(F10, F11, F12, F16, F20, F21), the constants test (F13), the_showntest (F14, F14b, F14c), theos.statspellings test (F17), the added-later test (F18)Because the file is run with
-x, a mutant is killed by the first failing test in file order. U1, G2 and G2b are killed first by the+=check, which runs before the behaviour test. I ran them again with the+=check left out (only the behaviour test): U1 is named forEACCES with winerror 5, G2 forESTALEand G2b forEINVAL, each forS_ISREGandS_ISDIR. A Windows error that is neither 5 nor 32 (+= (53,)) passes the behaviour test and is named by the+=check alone: see the known limits.My first full run of the first round, on
b8f78bdb, killed 72 of 73. Its one survivor, W33, deleted the twoFOLLOW_UPasserts. They passed on the real value whether they ran or not.ccbd0eb0moves them intoentry_problems, which is proved on planted input, and W33a and W33b are killed there.Mine on the first round, in short: the walrus,
__import__, default parameters, class attributes, tuple unpacking, the two*imports,attrs[GUARD],sys.modules, the attribute lookup,for,with, a guard bound to a name, ando = os, each dropped from the scan (W1-W7, W22, W23, W25-W30). Then:_calls_outalways False or always True (W8, W9);raise excrefused or anyraisetaken (W10, W11); any<name>.<NAME>taken as errno (W12);PermissionErrorinHANDLERS(W13);not inrefused again (W14);frozensetrefused again (W31);import errno as Eignored (W32). On the behaviour side: any raise taken for a refusal (W15); no Windows cases (W16); no flag variants (W17);_slicecut to one pass (W18); anAugAssignskipped (W19); a non-stdlib import taken (W20); absent cases unchecked (W24); a later_is_a = ...dropped (W34). Last, the owner check loosened (W21), and eachFOLLOW_UPcheck dropped (W33a, W33b).Mine on this round: the refused errnos cut to the old three or made to include the absent ones (F1, F2); no refused Windows error, or only 5, or only 32, or carried by
EINVAL, or read as absent (F3 to F3d); only one kind asked (F4, F4b); the link never asked, itslstatrefused too,followsignored, inverted or defaulted to False, the link asked with the flags too (F5, F6, F7, F7b, F17, F9);_dangling_linkmaking no link, linking to a file that is there, or making the link again (F8, F8b, F8c); the+=check not applied, not limited to the top level, reading one name, or noAugAssign(F10 to F13, F20, F21); the set comparison dropped (F16);_showndropping the count, counting at the boundary or cutting nothing (F14 to F14c); the kind left out of the message (F18); the link's absent cases unchecked (F15).Gate
All runs used the probe environment (stub
ghfirst on PATH,GH_TOKENbogus, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/null,GIT_SSH_COMMAND=false,EMAILset,LANES_LANEunset), with Python 3.12.3 and git 2.43.0, every call on the final headcd935163, each under 9 minutes and at most 4 pytest calls at once:tests/test_path_guard_hygiene.py, wholecd935163tests/test_path_guard_hygiene.py, wholefa67fac7ast.unparsemessages)test_agents_md_codes,test_cli_reference,test_repo_hygienefa67fac7test_adopt_unsearchable_source.py,test_adopt_unenterable_source_and_locale.py,test_update_shape_unreadable.py,test_doctor_unreadable_paths.py,test_doctor_unreadable_pin.py,test_bump_leg_unopenable.py,test_bump_leg_unreadable.py,test_bump_leg_unsearchable.py,test_assembly_root_unreadable_paths.py,test_validate_pins_unreadable_workflow.py,test_validate_pins_unreadable_workflows_dir.py,test_validate_family_unreadable.py,test_family_siblings_bootstrap_unreadable.py,test_family_py_unreadable.pycd935163tests/test_shape_doctor.py, 4 shards of 112 node ids eachcd935163tests/test_scaffold_leg_path_collisions.pycd935163test_agents_md_codes,test_cli_reference,test_repo_hygienecd935163No call ran the full suite.
Timing
The hygiene file, three runs each, alternating, pytest's own time. The machine was not quiet (load average 4.8 to 7 from other lanes' runs), so the seconds are only comparable within the table:
038dafbacd935163That is 1.36x base, under twice base. The behaviour test asks about 135 cases for each of two kinds, of a name and of a link, for each of twelve helpers (about 2 s of the file), and
_reachedfollows attribute chains in the scan.Known limits
The scanner.
getattr,operator.methodcaller).importliborsys.forover anything but a literal,*-unpacking,globals(),setattr, or a function's return.ismount,isjunction,is_fifo,os.accesswithF_OK, a hand-writtenstatwhoseOSErroris False).The docstring says which of these the shipped tools hold today.
It still over-flags. A name bound to the module once is the module everywhere in its file. A stored
o.exists = ...is flagged as a guard, as on base.The parity test's handler rule now looks only at a
trythat makes a call. A handler around astatreached without a call (a property, say) is left to the behaviour test, which asks the helper whatever the walk sees.The parity relaxations trust a name without checking that nothing rebinds it (r1149's T1 to T6).
exc = FileNotFoundError(...); raise exc, a module-leveldef frozenset(...)that adds EACCES,import errno as Ein another function beside a top-levelclass E,FileNotFoundError = OSErrorat module level, a local name shadowing a call-free function, and an invertednot inall pass the parity test. The behaviour test names each of them (measured), and it is what names those.except (OSError, PermissionError)is still refused statically. It is the same asexcept OSErrorwhen run, and the behaviour test passes it. r1111b listed it as wrong; I left the parity rule as it was.The behaviour test needs the helper to take
(path, kind). It must also stat throughos.stat,os.lstat,Path.statorPath.lstat. A helper that bindsfrom os import statat import would answer from the real disk, and would be named, not passed.The behaviour test flips only keyword-only bool flags. A positional
quiet=False, or a non-bool keyword-only flag that swallows the refusal, passes (r1149's G9 and G10). Today only the assembly root'sbootstrap.pyhas a flag,link. It asks aPathonly, and runs only the module's top-level statements the helper reads: a branch on astrpath, or a rebind inside a functionmaincalls, passes (G8, G12).Windows errors are asked as an attribute, and only two of them are refused. Each
winerroris set on a POSIXOSError, since CI is Linux-only (ci: run the test suite on Linux only #597). The behaviour test asks every errno but onlywinerror5 and 32 withEACCES. Any other Windows error added to_ABSENT_WINERRORSafter it is set is named only by the constants test, which reads a top-level+=or|=; one added by a method call or a statement inside anifpasses.The link is asked without a flag only, since a flag may ask
lstaton purpose, and it is skipped where this system makes none (Windows without the right). On Windows a refusal answered fromlstatis therefore not asked.The Z11 check compares two spellings. It compares
FOLLOW_UPwith the docstring. Only GitHub knows family.py, the family root's siblings.py, shape-doctor.py and shape_advisory.py: a name in the root that is a link into a folder this user cannot search is still read as absent at twelve sites (follow-up of #430 slice Z) #1110's number, and the tests make no network call.Closes #1134
Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code
Summary by Sourcery
Harden path-guard hygiene tests by expanding static binding analysis, adding runtime validation for every stat helper, and closing parity and allowlist self-test gaps.
Enhancements:
Documentation:
Tests: