Skip to content

M1 PR-4: surface gates S1–S3 — gate runner, rule registry, all three evaluators#2

Merged
ryandmonk merged 2 commits into
feat/context-compilerfrom
feat/surface-gates
Jul 2, 2026
Merged

M1 PR-4: surface gates S1–S3 — gate runner, rule registry, all three evaluators#2
ryandmonk merged 2 commits into
feat/context-compilerfrom
feat/surface-gates

Conversation

@ryandmonk

Copy link
Copy Markdown
Contributor

What this is

PR-4 of Milestone 1 (stacked on #1; retarget after it merges). The governance linter:

  • Gate runner — S1 surface-schema (vendored dspack.surface.v0_1), S2 contract-vocabulary, S3 governance; independently reported, never implicit in generation (the S0 spike found Ollama's mlx engine silently ignoring format — S2 exists because constrained decoding cannot be trusted). S1 failure skips S2/S3; S2 failure still evaluates S3.
  • Rule-type registry (the thesis-bearing seam) + evaluators per spec/dspack-v0.3.md §5.3. Unknown rule types throw (exit 4) — never skip.
  • Findings carry both severity faces (requirement: must|should, level: error|warn; ADR-11 as amended), verbatim rationales, $.root… locations.
  • CLI lint: machine-readable JSON on stdout (golden-comparable), human rendering on stderr; exits 0 / 2 / 4.

⚠️ Approved deviation (recorded in the plan, revision record 2)

All three evaluators ship here — forbidden-composition is 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 in src/core/lint/rules.ts — hand-review item.

Acceptance (in CI on this PR)

npm test                                  # F1–F5 reproduce their checked-in expected reports exactly;
                                          # clean golden passes; S1/S2 independence; warn tier; unknown-type throw
# CI also runs the CLI gate directly:
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 == expected.json

Hand-review focus

src/core/lint/rules.ts (deviation note + evaluator semantics vs spec §5.3) and the Finding/LintReport shapes in src/core/lint/findings.ts (embedded verbatim in audit reports and repair messages downstream).

ADRs: 1, 2, 6, 11.

🤖 Generated with Claude Code

…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lint CLI 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.

Comment thread src/core/lint/index.ts
Comment on lines +70 to +80
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"),
};
}
Comment thread src/core/lint/rules.ts
Comment on lines +167 to +172
/**
* 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", () => {
@ryandmonk

Copy link
Copy Markdown
Contributor Author

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: src/core/lint/vocabulary.ts's subComponentIndex has the same silent last-writer-wins behavior on duplicate sub-component ids, and S2 doesn't reject the ambiguity. The spec delta now makes document-wide sub-component id uniqueness normative, so the fix here is mechanical (mirror the dspack validate.mjs duplicate detection into the S2 gate + a golden fixture). Queued with the post-merge fixture-sync follow-up rather than churning this reviewed stack — flag if you'd rather have it as a commit on this branch now.

🤖 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>
@ryandmonk ryandmonk merged commit 43432d5 into feat/context-compiler Jul 2, 2026
1 check passed
ryandmonk added a commit that referenced this pull request Jul 2, 2026
Land the M1 stack on main (stacked PRs #2#5 merged into their bases)
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.

2 participants