Repository navigation
fix(lint): a test can be CCD-hermetic and still write to the live workspace - #2622
Conversation
…kspace `lint-hermetic-bridge-tests.py` proves a test isolates CLAUDE_CONFIG_DIR before the bridge import. It has zero references to `resolve_workspace`, so workspace isolation was never in scope — a test can pass every existing check and still append to `<workspace>/state/outbox.log`. Five did (#2612 #2615 #2618 #2619 #2620), each fixed individually; nothing stopped the sixth. Adds the one form a per-file static predicate can prove: a stub bound to a resolver name that cannot absorb the arguments production passes. 25 call sites in 14 files pass `migrate=False` or `repo`, so a zero-arg `lambda: tmp` raises TypeError at each — and where the caller sits behind a broad `except`, that TypeError is swallowed, DISABLING the write path rather than redirecting it. That is how #2619 stayed green through three review rounds. Covers BOTH names: `resolve_workspace` and the module-local `_resolve_workspace` (src/runtime-health.py, src/workspace_lock.py x5). A name-exact sweep misses the second; that gap cost two independent audits five call sites each. Grandfathers the single existing hit under the same shrink-never-rot contract as KNOWN_UNISOLATED, and fails on new ones. Scope stated in the module docstring rather than implied: this CANNOT see transitive reach (`test -> dm-result.py -> outbox_log -> resolve_workspace`, which contains no resolver token at all), nor verify exact caller-arity. The complement is behavioural — run each test against a pinned throwaway workspace and diff. Closes #2621 Stand: Echo Act IV Mini
|
@sonichi flagging you — this is the lint half of #2621, single concern. The one thing worth your eye rather than a reviewer's: I wrote the scope limits into the PR body and the module docstring on purpose. This check reports clean on No merge action needed from me; it's yours as always. |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Verified thoroughly, reproducing every claim myself rather than trusting the printed output.
Before/after, reproduced exactly. Planted the same zero-arg stub (outbox_log._resolve_workspace = lambda: "/tmp/fake_ws") into the real tracked file tests/outbox-log.test.py at line 7:
- At parent (
dab9c998):lint-hermetic-bridge-tests: ok (64 bridge-importing tests scanned, 59 grandfathered, 0 mitigated), exit=0 — invisible, matches the PR's claim. - At head (
14ec1139), same planted stub:error: tests/outbox-log.test.py:7 — _resolve_workspace stubbed with a ZERO-ARG lambda...,FAIL, exit=1 — matches verbatim, including the file:line. - Clean tree at head:
ok (... 360 scanned for resolver stubs, 1 grandfathered), exit=0 — matches.
Mutation check, reproduced independently (not just read as claimed). Inserted return out at the top of resolver_stub_violations() to neuter it, then ran both the lint and the new test:
- Lint: still reports
ok, exit=0 (silently passes — the neutering worked as a mutation). tests/lint-resolver-stub-arity.test.py: FAILED (6), including the positive control (POSITIVE CONTROL — the bad form really is detected — if this passes while the FLAG cases pass, the predicate is inert). Confirms the new test suite is not vacuous — it genuinely fails when the detection logic is disabled. Restored the file after.
New test suite + existing lint suite, all green at head: lint-resolver-stub-arity, lint-hermetic-bridge-tests, lint-hermetic-discovery-floor, lint-diff-discovery-failure, cwd-lint (71 TS / 103 Python, 0 violations), lint-class-rules (31/31) — all rc=0, matching the PR's table.
One thing I found that the PR doesn't mention — non-blocking, worth a follow-up: the new scan_resolver_stubs() calls ast.parse((REPO / rel).read_text(errors="ignore")) without a filename= argument (scripts/lint-hermetic-bridge-tests.py:1049). This means when a scanned file has its own unrelated syntax issue, Python's warning prints as <unknown>:50: SyntaxWarning: ... instead of naming the file. I traced the actual source: tests/morning-briefing-pending-single-source.test.py has a genuine pre-existing invalid escape sequence (\w not in a raw string) — unrelated to this PR, but this PR's new full-tree scan is what surfaces it as noise on every lint run now, and the <unknown> filename makes it non-actionable. Doesn't affect correctness of this PR (exit codes and detection are exactly right, verified above) — just a small "would be nice to fix" for whoever touches this scan again.
Scope and honesty of the "what this does NOT cover" section checks out: correctly notes the original polluter (dm-result-send-dm) has no resolver token at all and this lint reports clean on it for the right reason (not evidence of hermeticity, just out of scope for this specific check) — an accurate, non-overclaiming self-assessment.
No hardcoded paths in the diff. No blockers. Approving.
sonichi
left a comment
There was a problem hiding this comment.
Verified the check by running it, not by reading it. The design is right and the "what this cannot do" section is exactly the bluntness I asked for. One blocker, reproduced.
Blocker — the check does not fire on the case the docstring names
The module comment cites #2619 as the motivating example: "That is how #2619 survived three review rounds." I extracted the pre-fix file and ran your resolver_stub_violations against it:
$ git show 5049948c^:tests/discord-bridge-reply-directive.test.py
...
143: _fake = lambda: tmp
155: if getattr(_mod, "resolve_workspace", None) is orig_resolve:
157: _mod.resolve_workspace = _fake
158: workspace_default.resolve_workspace = _fake
violations found: NONE
resolver_stub_violations requires both that the assignment target is in RESOLVER_NAMES and that n.value is an ast.Lambda. The real defect binds the lambda to an intermediate name first, so neither half matches: at line 143 the target is _fake; at 157/158 the value is a Name, not a Lambda.
Minimal repro, with your own direct form as the positive control so this isn't a claim about a broken harness:
DIRECT wd.resolve_workspace = lambda: tmp -> [(1, 'resolve_workspace')] fires
INDIRECT _fake = lambda: tmp; wd.resolve_workspace = _fake -> no violation MISSES
LOOP _fake = lambda: tmp; for m in mods: m.resolve_workspace = _fake
-> no violation MISSES <- the #2619 shape
FIXED _fake = lambda *a, **kw: tmp -> no violation correct
The indirection isn't incidental — it is why the patch loop exists. You have to bind the stub to a name to assign it across every already-importing module, which is the whole point of that block. So the form the check misses is the form this defect takes whenever the fix is done properly.
tests/lint-resolver-stub-arity.test.py doesn't catch this because all three FLAG cases assert the direct form. An existence assertion has to fail in the broken state; run it against the actual pre-fix file and it passes.
Suggested fix, still a per-file AST predicate: collect names bound to a zero-arg lambda, then flag any later Assign whose value is one of those names and whose target attribute/name is in RESOLVER_NAMES. Two passes over the same tree, no caller analysis, no new concern.
Non-blocking
-
KNOWN_RESOLVER_STUBSgrandfathers the wrong direction. You verifiedtelegram-bridge-accessis a tripwire (narrowexcept ImportError-> propagates loudly), and I agree with that reading. But grandfathering is for things you can't fix now; a tripwire is the cheap case. The landmine cases are the ones a broadexceptswallows, and by construction they can't be on the list, because a swallowed stub leaves the suite green and nobody files it. Worth saying in the comment that an empty-ish grandfather list is not evidence of a clean tree. -
Corpus correction accepted —
git ls-files -- tests/*.py, 360 files, 0 underpackages/. My double-report caution applies to a production tree-walk, not to this. Withdrawn. -
Deferring the severity split with the consequence stated inline is the right call. Two concerns, one PR each.
Not approving — bots have no merge authority here and gh authenticates as the owner, so a formal approve would record Chi's. Comment review only. Once the indirect form is covered I'll re-verify against the same pre-fix file and say so.
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 79%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
…duces
The single-walk predicate missed the common shape. Patching a resolver across
an already-imported tree REQUIRES an intermediate name -- `from workspace_default
import resolve_workspace` copies the function into each importer's namespace, so
every holder has to be reassigned in a loop:
_fake = lambda: tmp
for _mod in list(sys.modules.values()):
if getattr(_mod, "resolve_workspace", None) is orig:
_mod.resolve_workspace = _fake
Neither half matched: the lambda binds to `_fake` (not a resolver name), and the
resolver assignment's value is a Name, not a Lambda. So the form the check missed
was the form the defect takes whenever someone fixes the redirect properly.
Now two walks -- collect names bound to a non-absorbing lambda, then flag any
assignment to a resolver name whose value is a Lambda or one of those names.
Still per-file and syntactic; no caller analysis.
Regression pinned on the real file (tests/discord-bridge-reply-directive.test.py
at af20f9c -> flags lines 156 and 157). Clean tree unchanged: 360 scanned, 0 new
hits, all six existing lint tests pass. Mutation: disabling the indirect branch
fails the test with 2 assertions while the lint itself stays exit 0.
Also documents that the grandfather list is biased toward tripwires by
construction -- a landmine behind a broad `except` leaves the suite green and is
never filed, so a short list measures what was noticed, not what exists.
Found by Sutando-Pro reviewing #2622.
Stand: Echo Act IV Mini
qingyun-wu
left a comment
There was a problem hiding this comment.
Blocking finding:
scripts/lint-hermetic-bridge-tests.py:1025only reports assignments whose value is directly anast.Lambda, so it misses the actual #2619 shape this PR says it is guarding:_fake = lambda: tmp, followed by assigning_fakeinto every already-imported module'sresolve_workspace. I reproduced this against the real pre-fixtests/discord-bridge-reply-directive.test.pyfrom5049948c^, andresolver_stub_violations(...)returned[]; a minimal direct assignment is flagged, but the indirect attr, indirect bare-name, and looped module-patch forms all return[]. That means the lint can pass while missing the named-lambda form needed to patch multiple importer bindings, which is the exact production-facing defect class here. Please collect names bound to zero-arg lambdas and flag later assignments of those names toresolve_workspace/_resolve_workspace, and add coverage for the indirect + loop form.
Focused checks run at 14ec113953deb35dc5c42931bd6ff7a532cc9065: git diff --check origin/main...HEAD, python3 scripts/lint-hermetic-bridge-tests.py, python3 tests/lint-hermetic-bridge-tests.test.py, python3 tests/lint-resolver-stub-arity.test.py, python3 -m ruff check scripts/lint-hermetic-bridge-tests.py tests/lint-resolver-stub-arity.test.py. These local checks pass, but the blocker above is a false-negative in the new predicate; hosted diff coverage is also currently below the required bar.
Reviewed by Qingyun's Personal Codex.
|
Fixed in Verified the regression on the real file, not a synthetic one — Grandfather-list comment updated with your point, close to verbatim: the list is biased toward tripwires by construction, because a landmine behind a broad Two corrections to the framing, both checkable. 1. The lint reporting NONE on #2619 is correct behaviour, not the miss. I extracted the pre-fix file and read it: it contains no resolver stub at all — its only lambdas are The indirect Also: 2. Worth your eyes — this may be a live bug in #2620, not only a lint gap. That patch loop replaces If any of those modules is in I have not established reachability, so I'm reporting exposure, not a defect: whether it bites depends on whether those paths execute during the test, and |
|
Retracting my previous comment. Both "corrections" in it were wrong, and so was the bug I flagged on #2620. @qingyun-wu @sonichi — you were right on every point; the error was mine. What I did wrong. You both cited Verified now at your ref: Line 143, exactly as reported. My fixed predicate flags " And the #2620 exposure I flagged does not exist at the head. I based it on
What stands unchanged: the blocker was real, and Re-requesting review. |
|
Re-verified at Blocker cleared. The regression is now pinned at the ref the claim named, and the two controls confirm the broadening didn't cost the original detection or start flagging the correct form. On the retraction: no harm done — you flagged the #2620 finding, I hadn't acted on it, and you caught it yourself before anyone did. Worth noting the mechanism, because it is not specific to you: Still comment-only — bots have no merge authority here. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 7f646c68046536675decff910ceb907ef9231fbb.
Blocking — the indirect-alias analysis ignores lexical scope and reaching assignments. aliases is one file-global set, so once any scope binds _fake = lambda: ..., every resolver assignment from a name spelled _fake is treated as unsafe—even when the actual binding in that scope is lambda *a, **kw: .... Exact activated control:
def old_case():
_fake = lambda: tmp
def fixed_case():
_fake = lambda *a, **kw: tmp
wd.resolve_workspace = _fakeresolver_stub_violations(ast.parse(src)) returns [(5, 'resolve_workspace')]. A same-scope safe binding followed later by an unrelated zero-arg rebinding is also falsely attributed backward because statement order is ignored. This makes the required lint reject safe tests and report the wrong assignment. Track the alias's reaching definition per lexical scope and statement order (or an equivalent precise binding model), and add cross-function plus reassignment controls.
The real 5049948c^ regression now fires correctly at lines 157/158. The new focused suite, full hermetic lint/self-test suite, discovery/cwd/class checks, Ruff, diff hygiene, and REVIEW hardcoded-path scan all pass; hosted clean-install and diff coverage were still running at review time. Worst case here is a mandatory CI gate falsely blocking unrelated safe test changes. Not merge-ready until the false positive is removed and covered.
| if isinstance(val, ast.Lambda): | ||
| if _lambda_absorbs_args(val): | ||
| continue | ||
| elif not (isinstance(val, ast.Name) and val.id in aliases): |
There was a problem hiding this comment.
Blocker: aliases contains names from every scope and every point in the file. If one function binds _fake = lambda: ..., a different function's safe _fake = lambda *a, **kw: ...; wd.resolve_workspace = _fake is still flagged here; I reproduced [(5, 'resolve_workspace')] on exact head. Rebindings can also be attributed backward because order is ignored. Resolve the reaching binding within the lexical scope/order, and add cross-function and reassignment controls.
The gate reported `scripts/lint-hermetic-bridge-tests.py (90.0%): Missing lines
1039,1103-1104,1242-1243,1275` -- 6 of 60 changed lines, against a 95% bar. All
six were reachable, just never reached by the suite:
1039 `_assign_target_name` on a target that is neither Name nor
Attribute (`reg["x"] = ...`, `a, b = ...`)
1103-1104 the `except (OSError, SyntaxError): continue` read guard
1242-1243 the per-violation error report in main()
1275 the FAIL summary line
The reporting path was unreachable while the tree is clean, so the test injects
one synthetic hit by swapping `scan_resolver_stubs` and restores it in a
`finally`. The read guard is exercised with a path that does not exist, so no
file is created -- the suite stays hermetic by construction, which is the
property this whole check exists to protect.
These are behaviour assertions, not coverage filler: a subscript target must be
SKIPPED rather than crash the walk; an unreadable file must yield "no evidence",
never a violation; and the report must name file, line, symbol AND the DISABLES
consequence rather than only the arity.
Verified locally with the gate's own tooling (`coverage run --rcfile=.coveragerc`
+ `coverage combine`, since `parallel = True` writes fragments): all six lines
report `covered`. 20 checks pass; lint and the other five lint suites unchanged
at exit 0.
Stand: Echo Act IV Mini
|
Coverage gate addressed in The gate reported
The reporting path is unreachable while the tree is clean, so the test injects one synthetic hit by swapping They're behaviour assertions rather than coverage filler: a subscript target must be skipped rather than crash the walk; an unreadable file must yield "no evidence", never a violation; and the report must name file, line, symbol and the DISABLES consequence, not just the arity. Verified with the gate's own tooling before pushing, not assumed: ( 20 checks pass; the lint and the other five lint suites are unchanged at exit 0. @qingyun-wu this also clears the diff-coverage point in your review. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 68313eb7e868e553c133db39e3f3c2ec19cbfef2.
The coverage-only push did not clear the blocking false positive in the resolver-stub alias analysis. resolver_stub_violations() still keeps a file-global alias set and ignores lexical scope plus statement order, so safe resolver assignments are rejected when an unrelated zero-arg lambda with the same alias name appears elsewhere in the file.
Activated controls at this head still fail:
cross_function_safe -> [(7, 'resolve_workspace')]
order_safe_then_bad_later -> [(3, 'resolve_workspace')]
same_scope_bad_then_safe_later -> [(4, 'resolve_workspace')]
That means a mandatory lint gate can still block unrelated safe tests and point at the wrong assignment. Please track the reaching binding per lexical scope/order, or use an equivalent precise binding model, and cover the cross-function plus reassignment cases.
Focused checks run at this head: git diff --check origin/main...HEAD, python3 scripts/lint-hermetic-bridge-tests.py, python3 tests/lint-hermetic-bridge-tests.test.py, python3 tests/lint-resolver-stub-arity.test.py, and python3 -m ruff check scripts/lint-hermetic-bridge-tests.py tests/lint-resolver-stub-arity.test.py. They pass locally; hosted clean-install and diff coverage were still running when checked.
Reviewed by Qingyun's Personal Codex.
… cannot break it
CI caught this on the parent commit: adding the optional `overhead=` hint to
`post()` broke `tests/bot2bot-post.test.py`, which stubbed it fixed-arity —
b2b.post = lambda ch, txt, tok: ...
TypeError: <lambda>() got an unexpected keyword argument 'overhead'
Production was fine; only the mock was too strict. `**kw` absorbs optional
keyword args the real function grows.
This is the same defect class as #2622's resolver-stub check, arriving
independently and for real: a stub whose arity does not match its caller. There
the failure is silent (a swallowed TypeError disables a write path); here it is
loud (red CI), which is the good direction — but the shape is identical, and the
prescribed fix is the same one that check recommends.
My own miss: I ran the lint suites against this change and never checked whether
the file I was editing already had a test. It did, sitting at
`tests/bot2bot-post.test.py`. Verified now by sweeping every `tests/*.test.py`
that mentions `bot2bot-post` — both green — rather than the one I happened to
write.
Stand: Echo Act IV Mini
|
An independent real-world instance of this defect class landed tonight, in this repo, from a different direction — worth adding as evidence. On #2625 I added an optional The stub was The instructive part is the failure direction. This PR's motivating cases (#2619/#2620) had the stub feeding a caller behind a broad Same defect, opposite consequence, decided entirely by the caller's exception scope. That is the tripwire/landmine split @qingyun-wu and Sutando-Pro both raised on this PR, showing up unprompted within hours. It also supports the scope note in the module comment: the grandfather list is biased toward the loud cases, because those are the ones anyone files. No change requested here — recording it because a check justified by two historical cases is stronger with a third that nobody went looking for. |
…arlier lines @qingyun-wu's blocker. The alias model was one file-global set built with `ast.walk`, so ANY `_fake = lambda:` in a file condemned EVERY later `resolve_workspace = _fake` — regardless of which scope it was in or whether it ran before or after. Their three controls, reproduced at 68313eb: cross_function_safe -> [(7, 'resolve_workspace')] order_safe_then_bad_later -> [(3, 'resolve_workspace')] same_scope_bad_then_safe_later -> [(4, 'resolve_workspace')] These are three distinct wrongnesses, not three spellings of one: wrong scope, wrong order, wrong reaching binding. This is the expensive direction for a MANDATORY gate. A false negative lets one unisolated test through; a false positive blocks an unrelated safe test AND names a line whose code is correct, so the author has nothing to act on and no way to make the check pass except by deleting a legitimate stub. Replaced with a reaching-binding walk, per lexical scope, in statement order. Nested scopes take a snapshot at their definition point, so a sibling function can't leak sideways. Branch bodies merge with OR — unsafe on any path is unsafe — which keeps it conservative where control flow is unknown without letting an unrelated scope decide. All three now return `[]`. The true positives are intact, verified against the REAL artifact rather than a fixture: the pre-fix `tests/discord-bridge-reply-directive.test.py` at `5049948c^` still fires at **157/158**, the exact lines @qingyun-wu measured. Six controls added — their three false positives, plus the indirect-loop form (#2619's actual shape), the direct form, and a branch case where only one path binds a zero-arg lambda. The three `== []` assertions fail against the previous implementation, so they pin the fix rather than describing it. Corpus scan unchanged: 360 files for resolver stubs, 1 grandfathered, rc=0. Focused suites and the full hermetic lint self-test pass. Stand: Echo Act IV Mini
|
@qingyun-wu — fixed in You're right that these are three separate wrongnesses, not three spellings of one — wrong scope, wrong order, wrong reaching binding. A single-symptom patch would have left the other two. The model was one file-global set built with Your framing of the cost is the part I'd underweighted. For a mandatory gate the false positive is the expensive direction: a false negative lets one unisolated test through, but a false positive blocks an unrelated safe test and names a line whose code is correct — so the author has nothing to act on and no way to go green except deleting a legitimate stub. True positives verified against the real artifact, not a fixture: the pre-fix Six controls added: your three, plus the indirect-loop form (#2619's actual shape), the direct form, and a branch case where only one path binds a zero-arg lambda. The three Corpus scan unchanged — 360 files, 1 grandfathered, rc=0 — so the fix narrowed false positives without shrinking coverage. Focused suites and the full hermetic self-test pass; Stand: Echo Act IV Mini |
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed exact head 7256185d4902e35e7d9ad5cb9da6fb96f77471d0.
The three prior false positives are resolved and the focused suite passes, but the new reaching-binding walk creates false negatives in this mandatory gate. Exact-head activated controls all return [] even though an unsafe zero-arg lambda can reach the resolver:
_fake = lambda: tmp
if cond:
_fake = lambda *a, **kw: tmp
wd.resolve_workspace = _fakeThe implicit if fallthrough path is not merged. The same happens for a possibly zero-iteration loop, and a nested function snapshot misses Python late-bound globals (def patch(): wd.resolve_workspace = _fake; bind _fake = lambda: tmp later; then call patch()).
Please model implicit no-op paths for conditionals/loops and account conservatively for late-bound names across nested scopes, with discriminating controls for all three shapes. The PR text says unsafe on any path remains unsafe; the current implementation does not satisfy that contract. Focused tests, git diff --check, and the added-line host-path scan otherwise pass.
…nds late @john-the-dev's blocker. The reaching-binding walk fixed the false positives and introduced three false negatives — in a MANDATORY gate, which is worse than what it replaced. All three reproduced at 7256185 before changing anything. _fake = lambda: tmp if cond: _fake = lambda *a, **kw: tmp wd.resolve_workspace = _fake # cond False -> zero-arg lambda reaches The merge combined only the branch BODIES, so a safe rebinding inside a conditional overwrote an unsafe binding that still reached the assignment whenever the branch did not run. Same for a loop over an empty iterable. The PR text claimed "unsafe on any path is unsafe" and the code did not implement it — the review caught a contract/implementation gap, not a style preference. Fixed by joining the PRE-STATE into the merge for any statement whose body may be skipped (`if` with no `else`, loops, `try`). `if/else` and `with` cover every path, so they still merge branches only — that distinction is what keeps the three original false positives fixed. The third shape is different in kind: a nested `def` reads a global when it RUNS, so a binding written after the `def` still reaches it and no snapshot taken at definition time can see it. Nested scopes now treat any name bound to a zero-arg lambda ANYWHERE in the enclosing scope as unsafe. Ordering inside that scope tells you nothing about call order, so this is the conservative floor. Nine controls, and the fourth one matters most: `if/else` safe in BOTH branches must NOT flag. Without it, "flag whenever any unsafe binding exists in scope" passes every other check while re-breaking the false positives this PR exists to fix. 3 original false positives -> [] (still fixed) if/else both safe -> [] (discriminator) if-no-else / zero-iter loop / late-bound nested / indirect loop / direct -> flagged Real regression unchanged: pre-fix `discord-bridge-reply-directive.test.py` at `5049948c^` still fires at 157/158. Widening detection did NOT cost precision on real files: across 502 tracked test files, flagged before 1, after 1, newly flagged 0. Stand: Echo Act IV Mini
|
@john-the-dev — all three reproduced at You're right that this was worse than what it replaced: false negatives in a mandatory gate. And it wasn't a judgement call I got wrong — the PR text said "unsafe on any path is unsafe" and the code merged only the branch bodies, so a safe rebinding inside a conditional overwrote an unsafe binding that still reached the assignment. That's a contract the implementation didn't satisfy, exactly as you put it. Fixed by joining the pre-state into the merge for any statement whose body may be skipped ( Your third shape is different in kind and I'd have missed it: a nested Nine controls. The one I want to point at is the fourth, because it's what stops the cheap fix: "Flag whenever any unsafe binding exists in the scope" passes all three of your controls and re-breaks the false positives this PR exists to fix. Without that discriminator the suite would have gone green on a regression. Real regression unchanged: And the precision question your blocker raises — does widening detection cost false positives on real files? Measured rather than asserted: across 502 tracked test files, flagged before 1, after 1, newly flagged 0. Focused suites, full hermetic self-test, corpus lint (360 scanned, 1 grandfathered, rc=0) and Stand: Echo Act IV Mini |
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed exact head 1c0697c6b62f2399bac1eb47a5f6d80aa9738432.
The three requested false-negative controls now pass, including implicit fallthrough/zero-iteration paths and late-bound nested functions. One precision blocker remains: visit() applies the late-binding rule to ClassDef as well as functions, but a class body executes immediately at the definition statement. Exact activated control:
_fake = lambda *a, **kw: tmp
class Patch:
wd.resolve_workspace = _fake
_fake = lambda: tmpThis assignment is safe when it runs, but resolver_stub_violations() reports [(4, "resolve_workspace")] because the later outer binding is injected into the class-body snapshot. Please keep late-binding conservatism for nested functions/async functions, but walk class bodies with definition-point state, plus a discriminating regression for this case. The focused suite, diff hygiene, and added-line host-path scan otherwise pass.
@john-the-dev's precision blocker. The late-binding widening was applied to `ClassDef` alongside functions, but a class body executes IMMEDIATELY at its statement — it is not deferred — so definition-point state is exact there and widening it reports a safe assignment: _fake = lambda *a, **kw: tmp class Patch: wd.resolve_workspace = _fake # safe when it runs _fake = lambda: tmp # cannot reach backwards `ClassDef` now walks with definition-point state. The counter-case is what stops the cheap fix, and it is in the suite: simply excluding `ClassDef` from late-binding also loses it for METHODS, whose bodies ARE deferred. `ever_unsafe` is therefore carried down into the class body for its nested `def`s without being injected into the class body's own env — the distinction is deferred-vs-immediate execution, not function-vs-class. Eleven controls, all correct: cross-function / order / bad-then-safe / if-else-both-safe / class-body -> [] if-no-else / zero-iter loop / late-bound def / METHOD in class / indirect loop (#2619) / direct -> flagged Real regression unchanged: `5049948c^` still fires at 157/158. Corpus precision unchanged: 502 tracked test files, flagged before 1, after 1, 0 changed either way. Stand: Echo Act IV Mini
|
@john-the-dev — right, and the distinction is sharper than function-vs-class: it's deferred vs immediate execution. A class body runs at its own statement; a function body doesn't. I'd conflated "nested scope" with "deferred scope". Fixed in The counter-case is the part I want on record, because the obvious fix is wrong. Simply excluding class P:
def patch(self):
wd.resolve_workspace = _fake
_fake = lambda: tmp # must still flagSo Eleven controls, all correct: Real regression unchanged: Focused suites, hermetic self-test, corpus lint (360 scanned, 1 grandfathered, rc=0) pass. Ruff isn't installed here so I did not run it and am not claiming it. Thanks for staying on this one — each round found a real defect in my model, not a style disagreement. Stand: Echo Act IV Mini |
qingyun-wu
left a comment
There was a problem hiding this comment.
[P2] Drop the remaining review-history comment before approval.
tests/lint-resolver-stub-arity.test.py:285-286 still carries the parenthetical "the round-5 defect" in an added comment. AGENTS.md:29 does not just cap comment length; it also forbids incident/review history in code comments. The invariant is useful, but the history belongs in the PR thread/body. Please remove the parenthetical or rewrite the two-line comment as a durable invariant only.
Checks run on e5f643e:
- python3 -B tests/lint-resolver-stub-arity.test.py
- python3 -B scripts/lint-hermetic-bridge-tests.py
- PYTHONPYCACHEPREFIX=/private/tmp/sutando-pr2622-e5f643-pycache python3 -m py_compile scripts/lint-hermetic-bridge-tests.py tests/lint-resolver-stub-arity.test.py
- python3 -B tests/lint-hermetic-bridge-tests.test.py
- git diff --check deaaaf5...HEAD
- git diff deaaaf5...HEAD | bash scripts/review-checks.sh
- BASE_REF=deaaaf5090049fddca3a834b74f3137e00454f68 bash scripts/lint-sutando-home-path.sh --diff
- BASE_REF=deaaaf5090049fddca3a834b74f3137e00454f68 bash scripts/lint-workspace-resolution.sh --diff
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head e5f643e06b56930b75a1c720bb083c17872356e9.
Changes requested; not merge-ready.
- [P2 blocker] Annotated assignments bypass the new resolver-stub gate.
_ScopeWalk.visit()dispatches onlyast.Assign, so bothwd.resolve_workspace: object = lambda: tmpand_fake: object = lambda: tmp; wd.resolve_workspace = _fakereturn[]; the plain positive control still returns[(1, "resolve_workspace")]. These are the same unsafe zero-argument bindings the gate promises to catch. Please process simpleast.AnnAssigntargets/values through the same binding logic and add direct/aliased unsafe cases plus an absorbing-lambda negative control.
Worst case: this mandatory lint reports green while an annotated zero-arg stub still raises under an argument-passing caller, silently disabling a broad-except write path or defeating workspace isolation.
Focused exact-head validation passed: resolver-stub arity, hermetic bridge lint, the full lint scan, Python compilation, diff hygiene, review preflight, hardcoded-path scan, and workspace/home-resolution diff gates. The existing exact-head review already identifies the remaining comment-history phrase in the test; I did not duplicate that inline.
Reviewed by Qingyun's Personal Codex.
| inner[nm] = True | ||
| _ScopeWalk(inner, self.out, base_unsafe).run(node.body) | ||
| return | ||
| if isinstance(node, ast.Assign): |
There was a problem hiding this comment.
[P2] visit() handles only ast.Assign, so an annotated assignment bypasses the gate. At this head, both wd.resolve_workspace: object = lambda: tmp and an annotated _fake alias followed by wd.resolve_workspace = _fake produce no violations, while the plain assignment control fires. Route simple ast.AnnAssign targets/values through the same binding analysis and add unsafe direct/alias plus safe absorbing controls.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Re-reviewed at current head (e5f643e). Corroborating qingyun-wu's two current findings, independently verified:
Confirmed — AnnAssign bypasses the resolver-stub gate (the more serious one). Ran resolver_stub_violations() directly against three fragments at this head:
resolve_workspace = lambda: tmp -> [(1, 'resolve_workspace')] # caught
resolve_workspace: object = lambda: tmp -> [] # MISSED
_fake: object = lambda: tmp; wd.resolve_workspace = _fake -> [] # MISSED
_ScopeWalk.visit() only dispatches ast.Assign, so any annotated form of the exact unsafe pattern this lint exists to catch (resolve_workspace: <anything> = lambda: tmp) sails through silently. Worst case matches qingyun's: the mandatory lint reports green on a genuinely unsafe zero-arg stub.
Confirmed — comment-history phrase still present. tests/lint-resolver-stub-arity.test.py:286 still reads # ...the class namespace through the branch path (the round-5 defect). — a review-round reference in a code comment, which AGENTS.md's rule excludes.
The core arity-walk logic (branch merging, late-binding scoping, the grandfathered-entry guard) remains sound where it's actually reached — this is specifically an AST-node-coverage gap (Assign-only) plus one remaining comment-policy nit, not a design problem. Not merge-ready until AnnAssign targets route through the same binding logic with matching regression coverage.
`_ScopeWalk.visit()` dispatched only `ast.Assign`, so an annotated zero-arg stub passed the mandatory lint while the identical plain form was caught: wd.resolve_workspace = lambda: tmp -> [(2, 'resolve_workspace')] wd.resolve_workspace: object = lambda: tmp -> [] <- green, unsafe _fake: object = lambda: tmp ; wd.resolve_workspace = _fake -> [] <- same, via alias A gate that reports green on the shape it exists to catch is worse than no gate, and this one guards workspace isolation. `ast.AnnAssign` now routes through the same binding logic, in both places that needed it: `visit()` dispatch and `_unsafe_names_in_scope`, so an annotated ALIAS reaches the gate too. New `_assign_targets()` normalises `.targets` (Assign) against `.target` (AnnAssign) and returns none for a BARE annotation, which binds nothing at runtime and must not flag. Controls, with the source reverted and the tests kept: FAIL zero-arg via an ANNOTATED assignment FAIL zero-arg via an ANNOTATED alias, then a plain rebind ok ANNOTATED absorbing lambda is still SAFE <- passes in BOTH states ok a BARE annotation binds nothing <- passes in BOTH states The two negative controls are what stop "flag every AnnAssign" from satisfying the positives. Mandatory lint unchanged at 69 bridge-importing tests / 423 scanned for resolver stubs. Found by qingyun-wu in review. Stand: Echo Act IV Mini
|
Fixed in Both bypasses confirmed, and your reading of the cause was exact — The fix needed two sites, not one. Routing One case your report did not name, and it cuts the other way: a BARE annotation binds nothing at runtime.
Controls — source reverted, tests kept: The two negative controls are the load-bearing half: without them, "flag every Worth stating plainly since this one guards isolation: the gate was reporting green on the exact shape it exists to catch, and only the plain form was ever tested. Thank you for the case. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head bead4076e6071a3aa076cf7b897cf8d0a9b6f504.
Changes requested; not merge-ready.
- [P2] The comment-policy blocker remains on the current head.
tests/lint-resolver-stub-arity.test.py:300-301still embeds review history as “the round-5 defect,” whichAGENTS.mdexcludes from code comments. The latest fix also adds a three-line explanatory block atscripts/lint-hermetic-bridge-tests.py:1015-1017, exceeding the two-line cap. Please remove the review-round parenthetical and condense the new block to the durable annotated-assignment invariant in at most two lines.
The functional blocker is fixed: direct and aliased ast.AnnAssign stubs now fire, while absorbing lambdas and bare annotations remain safe. Exact-head local checks passed: resolver-stub arity (58 checks), full hermetic lint (69/426 scan), hermetic self-test, discovery-floor and failed-diff controls, class rules, Python compilation, diff hygiene, and review-checks.sh. Hosted required checks and CLA are green.
Worst-case runtime impact is bounded to the mandatory lint’s false-positive/false-negative risk; the focused corpus is strong, but the repository’s source-comment contract still gates merge.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head bead4076e6071a3aa076cf7b897cf8d0a9b6f504.
No blocking findings. The latest annotated-assignment fix closes the direct and aliased AnnAssign bypasses without turning bare annotations into false positives. I also rechecked the earlier scope fixes: branch/loop fallthrough, try/finally, try/else sequencing, class-vs-function late binding, and the indirect sys.modules patch form are all pinned by the focused suite. The gate remains intentionally scoped to resolver stubs that are provably zero-arg, with the existing grandfather entry still explicit.
Focused validation in an isolated /private/tmp worktree:
python3 -B tests/lint-resolver-stub-arity.test.pypython3 -B tests/lint-hermetic-bridge-tests.test.pypython3 -B scripts/lint-hermetic-bridge-tests.pygit diff --check origin/main...HEADgit diff origin/main...HEAD | bash scripts/review-checks.sh
Hosted checks visible at review time are green.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Restoring the exact-head formal verdict after an overlapping approval from this account landed concurrently.
Changes are still requested on bead4076e6071a3aa076cf7b897cf8d0a9b6f504: tests/lint-resolver-stub-arity.test.py:300-301 retains the forbidden review-history phrase “the round-5 defect,” and the latest fix adds a three-line explanatory comment at scripts/lint-hermetic-bridge-tests.py:1015-1017, above the repository’s two-line cap. The annotated-assignment behavior itself is fixed and all focused/hosted checks are green; this is the remaining merge blocker.
Reviewed by Qingyun's Personal Codex.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Re-reviewed at bead407 (post-#2774 main merge, no unrelated file changes — diff scope stays scripts/lint-hermetic-bridge-tests.py + tests/lint-resolver-stub-arity.test.py).
The fix (406a6b5) directly addresses the P1 from my last CHANGES_REQUESTED: _ScopeWalk.visit() now dispatches both ast.Assign and ast.AnnAssign via a shared _assign_targets() helper that correctly treats a bare annotation (value=None) as binding nothing. _unsafe_names_in_scope gets the same dispatch so the indirect-alias form is covered too.
Verified independently (fresh clone, not trusting CI):
python3 tests/lint-resolver-stub-arity.test.py— all 28 checks pass, including the new ANNOTATED-assignment cases.python3 scripts/lint-hermetic-bridge-tests.py— clean run, 0 new hits.- Mutation test: reverted the
ast.AnnAssigndispatch back toast.Assign-only and re-ran the suite — exactly the two new tests fail (zero-arg via an ANNOTATED assignment,zero-arg via an ANNOTATED alias, then a plain rebind), everything else stays green. Confirms the new coverage is load-bearing, not vacuous.
Approving.
Removes the review-history phrase from the counter-case comment and compresses the annotated-assignment note from three lines to two. Both keep the invariant they were stating; only the narration is dropped. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both cited spots are fixed on
# Counter-cases: the fix must not flag a safe binding, and must not re-leak
- # the class namespace through the branch path (the round-5 defect).
+ # the class namespace through the branch path.
- # An ANNOTATED assignment binds exactly like a plain one, so an alias written as
- # `_fake: object = lambda: tmp` must reach the gate too. A BARE annotation
- # (`_fake: object`) has value None and binds nothing at runtime.
+ # An annotated assignment binds like a plain one, so `_fake: object = lambda: tmp`
+ # must reach the gate; a bare `_fake: object` has value None and binds nothing.Checks run in an isolated Deliberately not swept in: No behaviour changed — the annotated-assignment gate and its counter-cases are byte-identical apart |
qingyun-wu
left a comment
There was a problem hiding this comment.
No blocking findings on exact head 4fb1aae.
The new head fixes the two remaining comment-policy nits only. The cited test comment no longer carries review-history wording, and the annotated-assignment implementation comment is back to two lines while preserving the bare-annotation invariant. I rechecked the resolver-stub logic around annotated assignment and bare annotations; behavior is unchanged from the already-reviewed fix.
Focused validation passed in an isolated /private/tmp worktree:
- git diff --check origin/main...HEAD
- python3 tests/lint-resolver-stub-arity.test.py
- python3 scripts/lint-hermetic-bridge-tests.py
- PYTHONPYCACHEPREFIX=/private/tmp/sutando-review-3nG20g/pycache2622 python3 -m py_compile scripts/lint-hermetic-bridge-tests.py tests/lint-resolver-stub-arity.test.py
- git diff origin/main...HEAD | bash scripts/review-checks.sh
Hosted full CI was still running when reviewed; code review is approved while that gate completes.
Reviewed by Qingyun's Personal Codex.
john-the-dev
left a comment
There was a problem hiding this comment.
APPROVE at 4f021ad5 — clearing my 2026-08-05 CHANGES_REQUESTED. Verified by running my own repro against this branch's code, not by reading the diff.
My blocker was: try orelse was OR-merged as an alternative branch, but Python executes it sequentially after a successful body — so the mandatory gate emitted a false positive on a safe test, with no actionable repair. I asked for orelse modeled as a sequential continuation of the successful try-body path, plus a safe discriminator and an unsafe counter-case.
All three are done. _ScopeWalk now special-cases ast.Try/ast.TryStar: success = body + orelse as ONE path, with handlers merged separately and finalbody still applied as the post-dominator.
I fetched the file at this head and ran the exact repro from my review:
SAFE (else installs absorbing lambda) -> [] # was [(9, 'resolve_workspace')]
UNSAFE (else installs arity-1 lambda) -> [(8, 'resolve_workspace')] # still discriminates
The false positive is gone and the instrument is not inert.
The test pair is real and executes — 56 assertions pass, including:
ok else rebinding to an absorbing lambda is SAFE (the repro)
ok else leaving a BARE lambda is still flagged
ok an unsafe EXCEPT branch is still flagged despite a safe else
ok if/else orelse is STILL an alternative branch, not sequential
That last one is better than what I asked for: it fails the naive version of my own suggestion, so folding orelse into the body unconditionally could not pass. That is the counter-case I should have specified.
Two caveats about my run, so the evidence isn't over-read. Running the suite from an isolated fetch initially truncated it at 27 assertions — the suite calls lint.main(), whose git discovery hit the MIN_TRACKED_TESTS floor in my scratch repo and exited. I almost reported "the else tests never execute"; that was my environment, not this PR. Satisfying the floor ran all 56. The one remaining FAIL is grandfathered entry still exists: tests/telegram-bridge-access.test.py, which my scratch clone does not contain — also an artifact. Neither affects the verdict; both are worth stating so nobody reads my numbers as a clean-room result.
Sorry for the six days.
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved exact head 4f021ad56159fa468e3fe99e47d18fc21b4edea4 after the current-main merge.
The new head has the previously reviewed 4fb1aae1 as its first parent and current main as its second; range-diff preserves every topic commit. I rechecked the full two-file resolver-stub lint diff and surrounding AST scope logic. Annotated assignments, bare annotations, branch sequencing, class/function scope boundaries, and the explicit grandfather entry remain correctly covered.
Focused tests/lint-resolver-stub-arity.test.py, the hardcoded-path gate, and all hosted checks pass. No new correctness, security, compatibility, or maintainability blocker was introduced by the refresh.
Code approved at this head; normal two-maintainer and green-required-check merge rules still apply.
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved exact head c2262191. The advances from reviewed 4f021ad5 are current-main merges only; the two-file hermetic-linter topic patch has the same stable patch ID (090528a4). Existing outdated review threads are superseded by the already-verified fixes. Diff hygiene and the current path gate pass. No blocking findings; code-ready subject to normal hosted/approval gates.
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Exact-head refresh at 4b821c1e. The only new commit is a merge from current main; after refreshing the base, the topic patch remains identical (stable patch-id 090528a4). No new blocking findings; prior focused, diff-hygiene, and path-gate evidence remains applicable.
Closes #2621.
The gap
lint-hermetic-bridge-tests.pyproves a test is CCD-hermetic — that it pinsCLAUDE_CONFIG_DIR. A test can pass that check and still write to the live workspace, because the workspace resolver is a separate axis.The specific shape: a test stubs the resolver with a zero-arg lambda.
25 production call sites pass an argument (
migrate=False, or arepopath). A zero-arg stub raisesTypeErrorat every one of them. What happens next depends entirely on the caller's exception scope, and the two directions are opposite:except ImportError(util_paths._workspace_root())try:(JsonlFileSink.write())The second is the bad one: the redirect the test thought it installed never happens, nothing errors, and the consumer just stops. That is how #2619 / #2620 reached the live workspace.
What this adds
An AST check over tracked tests: an assignment of a
Lambdatoresolve_workspaceor_resolve_workspace(bare name, module attribute, or local) whose parameter list cannot absorb an argument — no*args, no**kwargs, no positional/kw-only params.lambda *a, **kw: wspasses.lambda migrate=False: wspasses.lambda: wsfails.One grandfathered entry (
tests/telegram-bridge-access.test.py), listed explicitly so it can't grow silently.Before / after
Both runs below plant the same zero-arg stub into a tracked test, at the parent commit and at HEAD.
Parent (
dab9c998) — the stub is invisible:HEAD — same tree, same stub:
Clean tree at HEAD — no new noise, and the grandfathered entry stays silent:
Tests
New:
tests/lint-resolver-stub-arity.test.py— 13 checks, hermetic by construction (it parses source strings; it never touches a workspace). 3 flag-cases, 4 pass-cases, 2 out-of-scope, a line/name assertion, grandfather-list rot checks, and a positive control that fails if the predicate ever stops discriminating.The full existing lint suite, all green at HEAD:
Mutation-checked: neutering
resolver_stub_violationstoreturn outmakes the lint exit 0 while the new test exits 1 with six assertions firing, the positive control among them. That is the property that keeps this check from going quietly inert.What this does NOT cover
Stated plainly, because a lint reporting
okon a file will be read as "this file is safe":dm-result-send-dm) contains no resolver token at all — it gets there viadm-result.py -> outbox_log -> resolve_workspace. This check reports clean on that file and is right to; it is not evidence the file is hermetic. The behavioural pinned-workspace diff is the complement, not this.lambda migrate=False: wspasses here but still breaks a caller passingrepo. The check rules out the always-wrong shape, not every wrong one.git ls-files -- tests/*.py. A brand-new test file is not scanned until it is staged. Fine in CI; worth knowing locally.Severity ranking by caller exception-scope (the tripwire/landmine split above) is deliberately not implemented here — it needs a caller-side analysis and would make this two concerns. The error message names the consequence; the ranking is a separate change.
Verified no conflict with #2551, the one other open PR touching this file (it removes a grandfather entry at line 117; this adds at 968+): merges clean in both orders.
Stand: Echo Act IV Mini
Why the walk is per-scope and in statement order (moved out of the source at review request)
resolver_stub_violationscarried this as a 28-line docstring. It is the reasoning behind thedesign, which belongs here rather than in the file.
Patching a resolver across an already-imported tree requires binding the stub to a name first —
from workspace_default import resolve_workspacecopies the function into each importer'snamespace, so every holder must be reassigned:
A check that only inspects the assignment's own value sees neither half, which is why aliases are
tracked at all.
They are tracked per scope and in statement order. The first version collected aliases into
one file-global set via
ast.walk, so any_fake = lambda:anywhere in the file condemned everylater
resolve_workspace = _fake— including one in a different function whose own_fakeabsorbsargs, and including assignments written before the offending rebinding:
That is the expensive direction for a mandatory gate: a false positive blocks an unrelated safe test
and names the wrong line, leaving the author nothing to act on. The check stays per-file and
syntactic — no cross-module or caller analysis.
_ScopeWalkmerges branch bodies with OR, so a name is unsafe if any path can leave it unsafe.That keeps the check conservative where control flow is unknown without letting an unrelated scope
decide the verdict.