Skip to content

SW-26: Check 8 cross-file contract lint (wave 1) - #25

Merged
developzoneio merged 1 commit into
develop/v1from
feat/contract-lint
Jul 29, 2026
Merged

SW-26: Check 8 cross-file contract lint (wave 1)#25
developzoneio merged 1 commit into
develop/v1from
feat/contract-lint

Conversation

@developzoneio

Copy link
Copy Markdown
Owner

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:

Shipped defect Now caught by
sd-code-explorer told to APPEND with no Write tool - silently starved 03-decisions.md CL200 (wave 3)
IMPACT_REF never passed to sd-implementer CL102 (wave 2)
mcp__atlassian__searchJiraIssues - not a real tool name CL202 (wave 3)
README said one gate count, docs/architecture.md said another CL302, this PR

Check 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:

  • CL0xx reference resolution - unresolved sd- references, skills: entries with no folder, missing template paths, /sd: references with no command, unknown spec artifacts, orphan skills, uninvoked agents
  • CL3xx gate integrity - a gate must halt, must offer an option set, must match the declared count, must number contiguously, must declare its conditional gates, and a HARD gate must not list an override as a choice
  • CL9xx suppression hygiene - a suppression needs a real reason, a real rule id, and something to suppress

Output is TSV on stdout and nothing else. Exit 0 clean, 1 on BLOCK, 2 cannot 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.rules disagree - 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:feature declares" - 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 gives README <- manifest there and manifest <- disk here, hence transitively README == disk, with no gate parser duplicated into validate.

Gate classification needs no exclusion list. Gate followed by a lowercase word is never a gate, which is what makes ## Gate activity invisible 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.md may 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.md listed 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 literal STOP (the nearest one belonged to the migration gate above it), and neither setup gate offered a machine-readable option set.

commands/perf.md carries the repo's first two suppressions, with reasons: its HARD baseline gate genuinely offers proceed anyway in 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.ps1 drives 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 matches docs/contract-lint.md, both directions". -SelfTest swaps 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

  • both validators green on Windows and bash (73 published doc claims)
  • fixture suite 23/23; -SelfTest raises 51 failures against a stub linter
  • bash and PowerShell output identical line-for-line on a tree seeded with findings
  • hook conformance 65/65 unaffected; docs self-test 6/6; hook smoke 20/20
  • install -> uninstall round-trip clean
  • contract-lint.ps1 and run-selftest.ps1 are 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

  • scanScope is deliberately commands/ agents/ skills/. Widening it to docs/** produces ~18 CL001 false positives on day one - CLAUDE.md uses sandbox paths and docs/architecture.md carries a frontmatter example. It is commented as load-bearing.
  • Internal record tables use US (0x1f), not TAB. TAB is an IFS whitespace character, so bash collapses runs of it and one empty middle field shifts every later field left. The TSV output still uses TAB.
  • Fixture .md files are walked by Check 7's undeclared-claim scan, so fixture prose deliberately carries no inventory-claim vocabulary. The fallback - excluding tests/ - would silently drop the directory from the scan.

🤖 Generated with Claude Code

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>
@developzoneio
developzoneio merged commit a5cb04b into develop/v1 Jul 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant