SW-26: Check 8 cross-file contract lint (wave 1) - #25
Merged
Conversation
The engine's shell was heavily verified; its product - the command prompts, agents and skills - had zero automated verification. Every relationship between those files was asserted in prose and checked by human review, and that gap had already shipped statically-detectable defects. Check 7 closed the inventory drift class. This closes the contract drift class: 17 rules across CL0xx reference resolution, CL3xx gate integrity and CL9xx suppression hygiene, as twin pure-file-ops scripts that emit TSV and nothing else. Severity lives only in the manifest registry, so a BLOCK/WARN divergence between the two implementations is structurally impossible, and a registry parity guard in each linter exits 2 when the rules it dispatches and the registry disagree. Gate counts become published claims: the manifest seeds Check 7 quantities, giving README <- manifest there and manifest <- disk in CL302, hence transitively README == disk with no gate parser duplicated into validate. Two real violations were fixed before the linter landed, so the first green run is distinguishable from a linter that never fires: architecture.md listed four items against a count of three, and a setup gate had no literal STOP. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes SW-26 wave 1. Waves 2-4 are filed as SW-32, SW-33, SW-34, SW-35.
Why
The engine's shell is heavily verified - 7 validator checks, 65 hook conformance fixtures, two negative self-tests. The engine's product - the command prompts, agents and skills - had zero automated verification. Every relationship between those files was asserted in prose and checked by human review.
That gap has already shipped defects, each statically detectable the whole time:
sd-code-explorertold to APPEND with noWritetool - silently starved03-decisions.mdIMPACT_REFnever passed tosd-implementermcp__atlassian__searchJiraIssues- not a real tool namedocs/architecture.mdsaid anotherCheck 7 closed the inventory drift class. This closes the contract drift class.
What
scripts/contract-lint.{ps1,sh}- twin pure-file-ops linters, no subagent, no model. 17 rules:sd-references,skills:entries with no folder, missing template paths,/sd:references with no command, unknown spec artifacts, orphan skills, uninvoked agentsOutput is TSV on stdout and nothing else. Exit
0clean,1on BLOCK,2cannot run - separate on purpose, because a validator that cannot tell "clean" from "crashed" is worthless.Design decisions worth reviewing
Severity lives only in the manifest registry, never in a rule's code, so a BLOCK/WARN divergence between the two implementations is structurally impossible. A registry parity guard inside each linter exits 2 when the rules it dispatches and
contractLint.rulesdisagree - which is what forces waves 2-4 to be complete additions rather than half-landed ones.The manifest now stores a number, against its own "stores NO counts" charter. That is deliberate and documented in a new
$contractLintComment. The charter governs inventory; the test is: can a script count it from disk with no judgement calls? "How many command files exist" - yes, so it must be derived. "How many hard gates/sd:featuredeclares" - no, nothing on disk is a second source, and a derived value would make CL302 compare disk against itself and pass vacuously forever. Feeding those counts into Check 7 as quantities givesREADME <- manifestthere andmanifest <- diskhere, hence transitivelyREADME == disk, with no gate parser duplicated intovalidate.Gate classification needs no exclusion list.
Gatefollowed by a lowercase word is never a gate, which is what makes## Gate activityinvisible without anyone maintaining a list that goes stale.CL305 is scoped to a gate's option set, never its prose. An override is a listed choice, not a described consequence -
release.mdmay say "the user may override the version at this gate" without tripping it. The prose form needs a per-gate declared-exception surface and is deferred to CL306 (SW-34).Two real violations fixed BEFORE the linter landed
Otherwise the first green run is indistinguishable from a linter that never fires:
docs/architecture.mdlisted four items against a gate count of 3 for/sd:feature, one of them naming a per-task review gate removed when the workflow moved to batch review.commands/setup.md's detected-facts gate had no literalSTOP(the nearest one belonged to the migration gate above it), and neither setup gate offered a machine-readable option set.commands/perf.mdcarries the repo's first two suppressions, with reasons: its HARD baseline gate genuinely offersproceed anywayin the already-at-goal branch, and that buys a logged constitution exception rather than a way past the baseline requirement.Testing
tests/contract-lint/- a minimal valid mini-engine (_base) plus one overlay per case. Goldens pin a seed marker, never a line number: a literal rots the moment a line above it shifts and the case then passes vacuously.23 cases: 17 must-fire, 5 must-stay-silent (the shapes that occur in the real engine -
## Gate activity, a bold pseudo-gate, a sub-gate authored before its parent, a HARD gate whose prose mentions an override), and 1 must-still-bite (Phase 0 STOPs must never satisfy a gate that has none).run-selftest.ps1drives both implementations in one process, so parity is asserted rather than inferred from two green CI jobs, and it compares message text even though the goldens never pin it. Five harness invariants, each a hard failure rather than a skip - including "every rule appears in a fixture" and "the registry matchesdocs/contract-lint.md, both directions".-SelfTestswaps in a linter that reports nothing and asserts the harness notices.Adding a rule means four edits - registry, both implementations, a fixture, a doc row - and each edge is guarded by a different mechanism, so it cannot be half-done.
Verified locally
-SelfTestraises 51 failures against a stub lintercontract-lint.ps1andrun-selftest.ps1are pure ASCII (Check 1 scans them)Both negative directions confirmed by hand: a wrong README number fails Check 7, and a manifest gate count that disagrees with disk fails CL302.
Reviewer notes
scanScopeis deliberatelycommands/ agents/ skills/. Widening it todocs/**produces ~18 CL001 false positives on day one -CLAUDE.mduses sandbox paths anddocs/architecture.mdcarries a frontmatter example. It is commented as load-bearing..mdfiles are walked by Check 7's undeclared-claim scan, so fixture prose deliberately carries no inventory-claim vocabulary. The fallback - excludingtests/- would silently drop the directory from the scan.🤖 Generated with Claude Code