Repository navigation
chore(skills): probe distinctions and refusals; tighten reviews - #1422
Merged
Merged
Conversation
A check that tells two things apart by a property, such as the `$id` base two texts are read against, matters only where something depends on that property, here a reference resolved against the base. No rule asked a change adding such a check to compile an input where nothing does, so fixtures that all held the dependency could not show the check firing without it, and mutants reddening tests said nothing about that input. Add `distinction-where-nothing-depends` beside `test-guards-fail-closed` in the verifying-changes assertions reference: delete the dependency from one of the check's fixtures, keep the property, compile it on main and on the branch, and require the same document and diagnostics. The probe passes on main by design, so its control is a build whose check fires without the dependency, which must differ. Point the review's `code-before-claims` probe at the rule.
The Why of `distinction-where-nothing-depends` said every spec testing #1406's `$id` base checks wrote a `$ref`. The tests #1406 added with its fix write none, so the sentence was false of the PR as it merges. Say instead that every spec wrote one while the checks fired on any text under another `$id`.
Deleting what a check depends on shows the check firing where nothing depends on it, but not a check that looks for the dependency anywhere in its input rather than in what it compares. Both forms show a defect at #1406's a4dda68: a type lost the `minLength` of an inline `allOf` branch writing no `$ref`, and the `description` of one whose only `$ref` sat in `properties`, though a description reads against no base. Ask for the second form too, the dependent moved outside what the check compares, and give the rule both symptoms as its evidence.
This was referenced Nov 3, 2026
A Why could pin a count only to an issue, a merged PR or a revision, but nothing held a universal to the same, so "every spec testing #1406's checks wrote a `$ref`" went false as #1406 moved (#1425). The curating rule now pins any claim about a PR that way, and the claim sweep refuses one resting on an open PR's head. The two #1406 Whys this branch added now name a4dda68, and `claims.md` no longer says what the open #1235 removes. The neighbours review found rules by the act they govern, so a rule probing `$id` base checks was never read against `text-decides-under-one-base`, which governs the same construct (#1426). It now greps for the constructs an edited rule names too, and `posted-findings` holds a suggestion to both. A refusal's findings are held to `SourceTable`, which names no document a `$ref` read, and only `compilers.Detection.Compile` checks that, so a refusal made after the load phase can pass every compiler-level test and fail in the engine (#1432). A new rule, `refusal-findings-in-its-table`, says how to probe it. The bound rule points at it and now covers bounds in lowering, which is where the refusal that showed this sits, and the openapi skill lists it. The curating SKILL.md stays at 500 words through four rewordings that keep their meaning.
Deleting or moving what a check depends on misses a check that counts the dependent however it is spelled, though one spelling does not depend on the property: an absolute `$ref` names one schema under any base. At #1406's 1855a63, where both earlier forms matched `main`, an inline `allOf` branch under another `$id` whose `not` wrote an absolute `$ref` still went to `Unmodeled` with its `description`; f5db471 matches `main` there too. Ask for that third form, and say text depends on a base only through a relative reference. The heading said nothing depends on what the check compares, where the rule means that what it compares ignores the property; the heading and the pointer in `code-before-claims` now say so. The claim sweep's command also reads a PR's head, as #1425 proposed.
A refusal's findings fall outside its source table only once a `$ref` has read a document, since that document is the one entry the table lacks. The rule and the openapi skill's bullet said "after the load phase", which is where the refusal that showed this sits, but a refusal made in the load phase after such a read breaks the same way. Scope the rule, its heading and the bullet by the read instead.
The rule gave examples of the acts an edited rule governs but none of the constructs it names, so a reader taking "construct" for a spec keyword alone would not grep for a field or a check such as `SourceTable`. Name a keyword, a field and a check, and shorten the closing sentence to keep the rule within its cap.
Widening the rule from "load or binding" to "load, binding or lowering" reached the expansion budget that sits in lowering, but also sent every other lowering bound to `source-budgets` or `references`. Lowering keeps bounds those entries hold no kind of in the entry of the construct they bound: the redeclared-property fold judges 64 declarations, and a null read's budget in the disjoint-conjuncts check reports nothing. Scope the rule to a bound of a kind either entry holds, in whichever phase, which keeps the fixed scan bounds and the expansion budget and leaves those to their own entries.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four gaps in the skills, each found while a check or a review passed over a defect.
A distinction probed where nothing depends on it
Some checks tell two things apart by a property, such as the
$idbase two texts are read against. Such a check matters only where something depends on that property: a base matters only to a reference resolved against it. No rule inmorphic-verifying-changesormorphic-reviewing-prsasked a change adding such a check to compile an input where nothing depends on the property. The guard rules (test-guards-fail-closed,build-guards-see-later-writes,split-guard-per-site) ask whether a guard catches what it targets, not whether it also fires where its premise has nothing to act on.#1406 showed the cost. At its a4dda68, the
$idbase checks fired on any text under another$id, and this spec writes no$ref:On
main(268945f),Sgets the constraints{minLength: 1, maxLength: 5}. At #1406's a4dda68 it gets{maxLength: 5}, the branch is kept whole underopenapi:allOf/0, and an info says the branch "reads its $refs against another base", though it writes none. CI passed on a4dda68, and the mutants #1406 listed reddened tests. Neither could show the defect, because at a4dda68 every spec #1406 had added to test the checks wrote a$ref. The same build also moved toUnmodeledthedescriptionof a branch whose only$refsat inproperties, though a description reads against no base. A probe that only deletes the$refwould also pass a check that looks for a reference anywhere in the branch rather than in what it compares, so the rule asks for a second form. A later #1406 round found a third. At 1855a63, where both forms matchedmain, a branch whosenotwrote an absolute$refstill went toUnmodeledwith itsdescription, though an absolute$refnames one schema under any base. So the rule also asks for the dependent made inert.This change:
distinction-where-nothing-dependsto.claude/skills/morphic-verifying-changes/references/assertions-and-reach.md, besidetest-guards-fail-closed. Compile a fixture in three forms, onorigin/mainand on the branch: with what depends on the property deleted, moved outside what the check compares, and made inert (an absolute$ref). The documents and diagnostics must match unless the change says why they don't, and test rows pin each form. Its example says text depends on a base only through a relative reference.mainby design, somain-control("keep only those that fail there") would throw it away. Its control is instead a build whose check fires without the dependency, which must differ (planted-control). For fix(compilers/openapi): judge union branches against the body #1406, a4dda68 is that build.code-before-claimsin.claude/skills/morphic-reviewing-prs/references/review-probes.mdask for this probe in a review's first round, citing the rule, and adds fix(compilers/openapi): judge union branches against the body #1406 to its Why.Three things are deliberately left as they are here:
morphic-verifying-changes/SKILL.md's claim → probe table gets no row, because its body is at the 500-word cap. The skill's References list already routes toassertions-and-reach.md, andcode-before-claims, which the review's second pass cites, now names the rule.text-decides-under-one-baseinmorphic-openapi-compiler/references/openapi-lessons.mdis unchanged here. Onmainit trusts a textual "equal" only under one base, whether or not the text writes a reference, which is the over-firing this rule probes for. At f5db471, fix(compilers/openapi): judge union branches against the body #1406 rewrites it to trust text writing no relative$refor$dynamicRefunder any base, and to probe the$ref-deleted form againstmain. That fix belongs with the checks it describes. fix(compilers/openapi): judge union branches against the body #1406 also editsopenapi-lessons.md, where this PR addsrefusal-findings-in-its-table, but in other rules: at 7ffeebb, whose skills change is f5db471's, the two merge without conflict, and the skills lint is clean on the merge.near-missin the review probes is unchanged. It adds a keyword the issue's example does not expect, to test a predicate deciding whether a new path applies. This rule deletes, moves or neutralises what a check depends on, to see whether the check fires where it should not. They perturb the input in opposite directions, so neither covers the other.Claims about an open PR (#1425)
The curating rule let a Why's evidence count only if an issue, a merged PR or a revision pins it, and the review's claim sweep refused "a count resting on an open PR". Neither covered a universal. This PR's first Why said "every spec testing #1406's
$idbase checks wrote a$ref". That held at a4dda68 and was false at #1406's 1855a63, whose fix adds a spec writing none (TestAllOf_ABranchWritingNoReferenceFoldsUnderAnyBase), and the claim sweep passed it.morphic-curating-skills/SKILL.mdnow pins "any claim about a PR" the same way. The body stays at its 500-word cap through four rewordings that keep their meaning ("needs" for "depends on", "as an agent follows a summary, not the body", "takes a rule" for "takes a new rule", and "The gate does not run it" in place of naming the push checklist, which the shipping skill already does).sweep-each-claim-classrefuses "a claim resting on an open PR's head" in place of "a count resting on an open PR". A claim naming the revision it held at passes, and the sweep's command now asks forheadRefOidas well asstate.claims.mdsaid a test's twoCONNECTs are what "test(compilers/openapi): keep the external order oracle offline #1235 removes", and test(compilers/openapi): keep the external order oracle offline #1235 is open, so the clause is gone and the Why keeps its evidence, test: the external-corpus order oracle fetches example.com over the network #1146. The other lines citing test(compilers/openapi): keep the external order oracle offline #1235 describe review work that already happened or scope themselves in time, so no push to test(compilers/openapi): keep the external order oracle offline #1235 can make them false.Neighbours found by act alone (#1426)
edited-rule-against-its-neighboursnamed the acts an edited rule governs and read the rules on the same acts. That is how this PR's first round missedtext-decides-under-one-base: it governs the same construct, text compared under$idbases, through another act.posted-findingshad the same by-act test for a committable suggestion ("breaks no rule governing the same act") and now says "the same act or construct".A refusal's findings and its source table (#1432)
A compile returning no Document has its findings held to
SourceTable, which names the source and its overlay and no document a$refread. Onlycompilers.Detection.Compilechecks that, under the engine, so a test callingopenapi.New().Compilenever meets it. #1432 records a refusal on #1430's 185c594, made after the load phase, that passed every compile-level test. On the same commit,morphic validatefails with a broken contract on a document whose$refreads one that draws a load finding.refusal-findings-in-its-tableinmorphic-openapi-compiler/references/openapi-lessons.md, says every finding of a refusal made after a$refhas read a document must index that table. It is scoped by that read rather than by a phase, since the read is what puts a finding outside the table: a refusal made in the load phase after one would break the same way. Onmainnone does: the load phase refuses only in the steps up to building the model, beforeresolveRefsreads another document (stepsincompilers/openapi/internal/load/phases.go), and a document past a budget fails the$refs naming it instead (docs/compilers/openapi.md#source-budgets). It says to probe it through the engine on a split document and to holdir.FirstOutsideTable(diags, len(SourceTable(...)))false in a test. It is its own rule because the bound rule could not hold it under 150 words, and because a refusal that is not a bound would never reach a Probe inside the bound rule.bound-outcome-from-the-rulespoints to it, and its scope moves from a phase, "load or binding", to a kind: a bound of a kindsource-budgetsorreferencesholds, in whichever phase. At 185c594, fix(compilers/openapi): budget the copies each $ref lowers #1430's bound,MaxReferenceExpansion, is anopenapi.Limitsbudget enforced ininternal/loweringthat joinsdocs/compilers/openapi.md#source-budgets, so the old wording would not have reached the change that showed the gap. A list of phases ("load, binding or lowering") would instead send to those two entries a lowering bound of a kind they do not hold, whichdocs/compilers/openapi.mdkeeps in its construct's entry:allof-redeclared-propertyjudges a field's first 64 declarations, and inallof-disjoint-conjunctsa read of null past its budget reports nothing. The narrower wording dates from the skills' first import, and no issue records a decision behind it.$refreads" among the classescode-before-claimssays the corpus cannot contain. The corpus already holds such specs (git grep -lE '\$ref: *.?\./[^#]+\.(ya?ml|json)' 268945f4d -- testdatalists seven), so that list would be wrong. What it lacks is a refusal meeting a finding in one of them, and the new rule is where a change adding a refusal will meet that.Closes #1420
Closes #1425
Closes #1426
Closes #1432
Test plan
python3.14 .claude/skills/morphic-curating-skills/scripts/skills_lint.pyis clean. That holds every edited rule to the 150-word cap, both editedSKILL.mdbodies to 500 words, and each new citation to its key. The three skills' script tests pass (python3.14 -m unittest discover -s .claude/skills/<skill>/scripts).morphic compile, built atmain268945f, at a4dda68, and at fix(compilers/openapi): judge union branches against the body #1406's 1855a63, which fixes the defects a4dda68 shows.mainand 1855a63 give byte-identical IR and diagnostics. a4dda68 differs:minLengthleavesS's constraints, and the info cites$refsthe branch does not write.main268945f, 1855a63 and fix(compilers/openapi): judge union branches against the body #1406's f5db471, on a spec whoseMbranch, under another$id, writesdescription: dandnot: {$ref: 'https://ex.test/b/n.json'}. 1855a63 keeps the branch as($id, description, not)underUnmodeled.mainand f5db471 are byte-identical, keeping only$idand the validation-onlynot.Mbranch, under another$id, writesdescription: dand keeps its only$refinproperties. At a4dda68 the branch is kept as($id, description)underUnmodeled, with the same "reads its $refs against another base" info.mainand 1855a63 keep only$idthere, and are byte-identical.code-before-claimscitation: the lint reportsassertions-and-reach.md holds no rule distinction-nowhereand exits 1. Wrong keys in the openapi SKILL.md bullet, the bound rule's pointer and theplanted-controlcitation, and the new rule pushed past 150 words, each draw one finding too.skills_lint.pyis clean on the merge. No other open PR edits a file this one does.docs/compilers/openapi.mdnames. The load budgets, the scan's three fixed bounds, the rule table's budget and fix(compilers/openapi): budget the copies each $ref lowers #1430's expansion budget at 185c594 fall tosource-budgets, the enum member budget through its pointer toenum-const, and binding's work budget and the merge-chain depth toreferences. The fold's 64 declarations, the null read's budget and detection's bound on a root's merge depth inyaml-decodingstay with their own entries.git diff -U0 e85202bc4 a4dda68a -- '*_test.go' 'testdata/*.yaml' | grep '^+.*\$id'lists the$ids fix(compilers/openapi): judge union branches against the body #1406 added. Every spec among them that reaches the base checks writes a$ref, including the case its comment says shows the rule holding "for a keyword with no $ref in it". The other lines are comments, a keyword list, and resource-binding fixtures inbind, which do not reach these checks.morphic validate --opt max-reference-expansion=1 root.yaml, whereroot.yaml's one operation has one parameter,$ref: './ext.yaml#/P', andext.yamlholdsP: {name: q, in: query}. 185c594 exits 2 with "diagnostic openapi/validation/validation-required-field indexes source 1 of 1: compiler broke its contract". ae66021 exits 1 with the refusal (openapi/budget-exceeded) alone. Without the option both exit 1 and reportext.yaml's finding.mainthe mechanism the new refusal rule states.Detection.checkcallscheckIndexes(out.Diagnostics, len(out.Sources))when there is no Document.out.Sourcesis then theSourceTable, and(*Compiler).SourceTablelists the sources and the overlay alone.CLAUDE.mdandCONTRIBUTING.mdfor the number of every PR then open (gh pr list --state open --limit 1000 --json number). The hits are test(compilers/openapi): keep the external order oracle offline #1235's lines and this PR's two fix(compilers/openapi): judge union branches against the body #1406 Whys, handled as above, andtest_subjects.py's use of ci: run coverage in set mode and -race in a run of its own #1311 as a sample number for a subject's length, which says nothing of ci: run coverage in set mode and -race in a run of its own #1311.docs/,README.mdand the Go sources cite none.SourceTable,FirstOutsideTable, contract, refusal, bound, open PR).explicit-guard-not-incidental,check-stays-silent-on-valid-inputand the exit-code note indifferentials.mdagree with them. For the distinction rule's constructs ($id,$ref, base, distinction),same-construct-at-every-position, which asks for$refs written relative and absolute under another$id, andverify-the-stated-reason, which removes what a comment credits, agree with it;text-decides-under-one-baseis the exception above.main:main-control,planted-control,better-than-main,near-miss,text-decides-under-one-base, and the "no regression against main" row of the claim → probe table. Themain-controlcase is resolved inside the rule, andtext-decides-under-one-baseby fix(compilers/openapi): judge union branches against the body #1406, as above.better-than-maindoes not conflict: matchingmainon these forms is required, not taken as proofmainis right.