feat(review): which lenses gate a pull request is a decision of its own - #457
Conversation
in-lockstep review — intentsucceeded · $0.0819 · 3 in / 881 out tokens This change introduces
|
in-lockstep review — performancesucceeded · $0.0724 · 3 in / 233 out tokens This change adds a No findings. |
in-lockstep review — securitysucceeded · $0.0731 · 3 in / 274 out tokens This change introduces a No findings. |
in-lockstep review — testssucceeded · $0.0778 · 3 in / 608 out tokens This change introduces
|
`AiReview(lenses=...)` was answering two questions at once. It says which lenses a repository HAS, and `chatops.aspect_from` resolves `/review <lens>` against exactly that map -- deliberately, so a repository that added a lens can name it on a thread. But it was also the only lever on which lenses the required check runs, so keeping a lens off the check meant dropping it from the map, which put it out of reach of the comment asking for it. The ordinary want -- five lenses available, two gating -- was not expressible at all. `review_workflows.register(gating=...)` is the second lever. It is on the registration rather than beside `lenses=` because it is a property of the CHECK: the call site reads worse and the semantics read better, and a second check over one adapter can now choose differently. A lens that is bound but not gating is not run and is still nameable on the thread, which is the property the obvious implementation breaks and which has its own test from the chat-ops side, asserted after a gating run over the same adapter rather than before one. Not a `--arg`, and not by accident: `register` re-declares the workflow's signature instead of using `functools.wraps`, because `wraps` sets `__wrapped__`, `inspect.signature` follows it, and `in-lockstep run` builds its `--arg` surface by introspecting the registered callable -- so a wrapped `gating` parameter would have been the trampoline lens list that GATE-REVIEW-5 closed, coming back in through the signature. Asserted structurally. A gating name the adapter does not declare refuses the run by name, listing the unknown lens beside the set that exists, resolved at run time because a module may call `register` before it binds `Review`; run time is still before the fan-out, so nothing has spent. `gating=()` is refused separately: absent and empty are opposite intentions, and the silent reading of the second is a required check that can never fail. Both are `blocked`, which exits 3, so a typo is a red check rather than a green one over nothing. Here all four gate, by decision and not by default -- O10 exists so the shipped lenses are not things we ask adopters to trust on our word -- and GATE-REVIEW-9 fails the build if an argument appears in this repository's own registration. GATE-REVIEW-5's claim is rescoped to say which half holds where. Closes #449. Unexercised-Config: the change to .lockstep/lockstep.py is a comment and nothing else -- zero non-comment lines differ, and the call it sits above is `review_workflows.register()` before and after. There is no behaviour here to exercise: what the comment records is the ABSENCE of a gating argument, and GATE-REVIEW-9 asserts that absence in-process by running this module's own registration over a five-lens stub and requiring five branches, which is a stronger check than the merge would be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90911a1 to
842e035
Compare
…e it came from Two lenses read `test_..._this_repository_gates_on_every_lens_it_binds` as asserting nothing about this repository, because it ended on `assert module is not None` -- a line written only to stop a lint complaining about an unused variable. The test did enforce the claim, since `_registered()` returns the closure `.lockstep/lockstep.py` installed, but a test two independent readers misread is a test the third one deletes. Underneath the misreading was a real hole: nothing asserted the registration came from THIS load rather than from a neighbouring test's leftover. `register` builds a fresh `Registered` per call, so comparing the entry across `load` says it. A `.lockstep/lockstep.py` that stopped registering the check now fails here instead of silently asserting over somebody else's closure. `module` is load-bearing now rather than asserted-and-discarded: the stub's five branches say the registration narrows nothing, and the repository's own lens set says what it is not narrowing. The ordering property the tests lens called uncovered was already covered -- the selection is named in reverse on purpose and the steps assert declared order -- but nothing said so, so an assertion that looked incidental now states its point. Declined: the closure captures `selection` by reference rather than by value. It is never rebound after the closure is built, which the lens raising it said itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…not described The intent lens caught a comment doing a test's job. `register` leaves the closure's `__qualname__` alone so that a host loading a `lockstep.py` twice in one process -- a long-lived worker, a test harness, two CLI invocations sharing an interpreter -- re-registers rather than colliding with itself, and that was stated in prose beside the code and asserted nowhere. It matters twice over here, because a `register` built the obvious way with `functools.partial` has no `__qualname__` at all and raises DuplicateWorkflow on the second load. Asserted in both directions. A selection that could not be taken back off is the worse of the two failures, so the no-argument call after a narrowing one is the first case and the later-selection-wins case is the second. The unknown-lens refusal is now asserted with more than one unknown name, which the singular case could not distinguish from a bare name: somebody renaming lenses gets several wrong at once, and the join is what they read. Declined, both repeats of round one: that the branch ordering follows the adapter's declaration rather than the selection's spelling is covered -- the selection is named in reverse on purpose and the steps assert declared order, and a control ordering by the selection fails that test. And the gating_empty refusal is asserted to be BLOCKED, on the same line that asserts its reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… spelling A partial carries no __qualname__, and _same_declaration compares module and qualname, so the obvious way to bind the argument would raise DuplicateWorkflow against itself on the second load of a lockstep.py in one process. The row said only why functools.wraps was wrong; the control firing on partial broke three properties at once, which is worth the row saying. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #449. Serves O9 — a contributed lens that cannot be treated differently from a shipped one is only half first-class — and O8, at the one file an adopter is meant to edit.
What was wrong
AiReview(lenses=...)was answering two questions at once.It says which lenses a repository has, and
chatops.aspect_fromresolves/review <lens>against exactly that map — deliberately, so a repository that added a lens can name it on a thread. But it was also the only lever on which lenses the required check runs. So keeping a lens off the check meant dropping it from the map, which put it out of reach of the comment asking for it. The ordinary want — five lenses available, two gating — was not expressible at all.The shape of the fix
review_workflows.register(gating=("security", "tests"))is the second lever, and the two are now orthogonal:lenses=is what exists and what the thread can reach,gating=is what the check runs.It is on the registration rather than beside
lenses=because it is a property of the check. Besidelenses=reads better at the call site and worse semantically — a second check over one adapter could not then choose differently.A lens that is bound but not gating is not run and is still nameable on the thread. That is the property the obvious implementation breaks, so it has its own test from the chat-ops side — asserted after a gating run over the same adapter object rather than before one, because narrowing the adapter is as easy to write at run time as at binding time and a check made first would not see it.
Not a
--arg, and not by accidentregisterre-declares the workflow's signature instead of usingfunctools.wraps.wrapssets__wrapped__,inspect.signaturefollows it, andin-lockstep runbuilds its--argsurface by introspecting the registered callable — so a wrappedgatingparameter would have been the trampoline lens list thatGATE-REVIEW-5closed, coming back in through the signature. Asserted structurally rather than by grepping the YAML.Refusals
A gating name the bound adapter does not declare refuses the run by name, listing the unknown lens beside the set that exists, with zero model calls. Resolved at run time, not at registration: a module may call
registerbefore it bindsReviewand there is no container yet. Run time is still before the fan-out, which is the property that matters.gating=()is refused separately — absent and empty are opposite intentions, and the silent reading of the second is a required check that can never fail.Both are
blocked, which exits 3, so a typo is a red check rather than a green one over nothing.Here, all four gate — by decision
O10 exists so the shipped lenses are not things we ask adopters to trust on our word, and
intent,performanceandtestsalready spent a release shipped-but-never-run once (#241).GATE-REVIEW-9asserts it by running the workflow this repository's own module registered over a stub declaring five lenses and requiring five branches, so an argument appearing in.lockstep/lockstep.pyfails the build.GATE-REVIEW-5's claim is rescoped to say which half holds universally and which is now an adopter's choice.Decisions taken
Two of the issue's three open questions were the owner's calls, and both went to the registration and to "a non-gating lens does not run at all" rather than "runs but cannot fail". The third — refuse an unbound name — was taken as settled by the issue's own argument.
Verification
rm -rf .mypy_cache && make checkfrom cold: 3063 passed, 6 skipped.make cov: 92%, floor 90, inside the two-sided band.gating=()read as "everything";registerwrapping instead of re-declaring;registerbinding withfunctools.partial; this repository narrowing its own check; this repository not registering the check at all; the check narrowing the adapter's declaration at run time; and branches ordered by how the selection was typed.Review rounds
Three rounds of this repository's own lenses. Acted on: a test that two lenses independently misread because of a vestigial
assert module is not None, and which had a real hole underneath it (nothing proved the registration came from that load); the__qualname__re-registration property, which was prose in a comment; and the unknown-lens refusal, previously asserted with only one unknown name.Declined, with reasons: that the branch ordering is uncovered (it is covered — the selection is named in reverse on purpose, the steps assert declared order, and a control ordering by the selection fails that test; raised three times); that
gating_emptyis not assertedBLOCKED(it is, on the same line as its reason); that the closure capturesselectionby reference (never rebound, as the lens raising it said); and two notes on assertions that are exact as written.Not proven
Nothing here has run in a real job. The refusals, the
--argsurface, and this repository's own all-four registration are asserted in-process. The first pull request after merge exercises the default path; the narrowing path has no adopter to exercise it.🤖 Generated with Claude Code