M1 PR-4: surface gates S1–S3 — gate runner, rule registry, all three evaluators#2
Conversation
…luators (M1 PR-4) - src/core/lint/: gate runner (S1 generic surface schema, S2 contract vocabulary, S3 governance) — independently reported, never implicit in generation (the S0 spike found Ollama's mlx engine silently ignoring `format`, which is why S2 is a check on the artifact). - rule-type registry + evaluators per spec/dspack-v0.3.md §5.3 semantics. Findings carry both severity faces (requirement: must|should, level: error|warn); rationales verbatim; locations as $.root… paths. Unknown rule types throw UnknownRuleTypeError (CLI exit 4) — never skip. - DEVIATION FROM THE M1 DIRECTIVE, flagged for review: forbidden-composition is implemented now (not M2/PR-8). Forced by a conflict discovered in implementation: the v0.3 shadcn contract carries a UNIVERSAL forbidden-composition rule (rule.button-no-interactive-descendants) and spec §5.4 forbids skipping unimplemented types — a two-evaluator linter would exit 4 on every lint of the real contract. Fixture F5 activates with it (all five golden fixtures active). - CLI `lint`: JSON report on stdout (golden-comparable), human rendering on stderr; exits 0 clean / 2 any S-gate error / 4 unknown rule type. - fixtures/golden/violating/F1-F5 + clean golden + checked-in expected reports; core-boundary test now walks recursively (lint/ included), ajv allowed as the only non-node bare import. Verify: npm test; npx tsx src/cli.ts lint --dspack fixtures/shadcn.v0_3.dspack.json --surface fixtures/golden/violating/F1-dialog-for-delete.dsurface.json (exit 2, stdout equals F1-dialog-for-delete.expected.json) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds the Milestone 1 governance linter “surface gates” pipeline (S1 schema, S2 vocabulary, S3 rules) plus a CLI entrypoint and golden-fixture acceptance coverage, so produced or hand-authored surfaces can be validated independently of generation constraints.
Changes:
- Introduces the S1–S3 gate runner (
lintSurface) with deterministic reporting and a shared surface-tree walker. - Implements the v0.3 rule-type registry + evaluators (component-choice, required-composition, forbidden-composition) with unknown types throwing
UnknownRuleTypeError. - Adds
dspack-gen lintCLI command and CI coverage (golden JSON + exit code assertions).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/core/surface-schema.ts | Vendors the v0.1 surface JSON Schema used for S1 validation. |
| src/core/lint/walk.ts | Adds deterministic surface traversal and descendant collection utilities for S2/S3. |
| src/core/lint/vocabulary.ts | Implements S2 contract-vocabulary validation (system/intent/component/props/slots). |
| src/core/lint/rules.ts | Implements S3 registry + evaluators and unknown-type hard error behavior. |
| src/core/lint/findings.ts | Defines Finding/GateReport/LintReport shapes and human rendering. |
| src/core/lint/index.ts | Adds the gate runner (lintSurface) sequencing S1→S2→S3 with skip semantics. |
| src/core/lint/lint.test.ts | Golden-fixture acceptance tests for S1–S3 behavior and registry semantics. |
| src/core/index.ts | Exposes lint APIs from the core entrypoint. |
| src/core/core-boundary.test.ts | Updates boundary scan to include nested core files and allow AJV imports. |
| src/cli.ts | Adds lint command emitting JSON to stdout and human text to stderr with exit codes. |
| fixtures/golden/clean/delete-account.dsurface.json | Adds/updates the clean golden surface fixture used in acceptance tests. |
| fixtures/golden/violating/F1-dialog-for-delete.dsurface.json | Adds violating surface fixture F1 (component-choice violations). |
| fixtures/golden/violating/F1-dialog-for-delete.expected.json | Adds expected lint report for F1. |
| fixtures/golden/violating/F2-no-confirmation.dsurface.json | Adds violating surface fixture F2 (missing required component). |
| fixtures/golden/violating/F2-no-confirmation.expected.json | Adds expected lint report for F2. |
| fixtures/golden/violating/F3-missing-cancel.dsurface.json | Adds violating surface fixture F3 (required-composition missing sub-component). |
| fixtures/golden/violating/F3-missing-cancel.expected.json | Adds expected lint report for F3. |
| fixtures/golden/violating/F4-missing-title.dsurface.json | Adds violating surface fixture F4 (required-composition missing title). |
| fixtures/golden/violating/F4-missing-title.expected.json | Adds expected lint report for F4. |
| fixtures/golden/violating/F5-nested-interactive.dsurface.json | Adds violating surface fixture F5 (forbidden-composition descendant). |
| fixtures/golden/violating/F5-nested-interactive.expected.json | Adds expected lint report for F5. |
| .github/workflows/test.yml | Adds a CI step to assert lint exit code and JSON output matches a golden. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function summarize(gates: GateReport[], findings: Finding[]): LintReport { | ||
| const errorCount = findings.filter((f) => f.level === "error").length; | ||
| const warnCount = findings.filter((f) => f.level === "warn").length; | ||
| return { | ||
| gates, | ||
| findings, | ||
| errorCount, | ||
| warnCount, | ||
| pass: gates.every((g) => g.status !== "FAIL"), | ||
| }; | ||
| } |
| /** | ||
| * forbidden-composition: for EVERY node matching `component`, no descendant | ||
| * may match any forbiddenDescendants id (finding at the offending descendant), | ||
| * and no forbiddenProps entry may hold (on the node itself, or on descendants | ||
| * matching `on`). | ||
| */ |
| expect(cancel.location.component).toBe("alert-dialog"); | ||
| }); | ||
|
|
||
| it("F5: nested interactive — forbidden descendant located at the offending node", () => { |
|
Known mirror-gap, deliberately not fixed in this stack: Copilot review on aestheticfunction/dspack#9 surfaced (and that PR now fixes) two things whose analogues exist here: 🤖 Addressed by Claude Code |
…9 review fix) Semantic alignment with the now-normative spec v0.3 §5: S2 and rule resolution work by sub-component id alone, so duplicate ids across components fail loudly — naming the id and every declaring component — before any id-dependent check, instead of resolving by object iteration order. Same error shape as the dspack validate harness; covered by a new lint test. No golden outputs change (the shadcn contract has no duplicates). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What this is
PR-4 of Milestone 1 (stacked on #1; retarget after it merges). The governance linter:
dspack.surface.v0_1), S2 contract-vocabulary, S3 governance; independently reported, never implicit in generation (the S0 spike found Ollama's mlx engine silently ignoringformat— S2 exists because constrained decoding cannot be trusted). S1 failure skips S2/S3; S2 failure still evaluates S3.spec/dspack-v0.3.md§5.3. Unknown rule types throw (exit 4) — never skip.requirement: must|should,level: error|warn; ADR-11 as amended), verbatim rationales,$.root…locations.lint: machine-readable JSON on stdout (golden-comparable), human rendering on stderr; exits 0 / 2 / 4.All three evaluators ship here —
forbidden-compositionis not deferred to M2. The v0.3 contract carries a universal forbidden-composition rule and spec §5.4 forbids skipping unimplemented types, so a two-evaluator linter would exit 4 on every lint of the real contract. Maintainer approved 2026-07-02; PR-8 is absorbed into this PR; F5 is active (all five golden fixtures gate this PR). The deviation note stays insrc/core/lint/rules.ts— hand-review item.Acceptance (in CI on this PR)
Hand-review focus
src/core/lint/rules.ts(deviation note + evaluator semantics vs spec §5.3) and theFinding/LintReportshapes insrc/core/lint/findings.ts(embedded verbatim in audit reports and repair messages downstream).ADRs: 1, 2, 6, 11.
🤖 Generated with Claude Code