From 44270dcb179406cde4bb3d03dc03cde8884457d0 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Wed, 22 Jul 2026 11:22:52 -0400 Subject: [PATCH] sync: shadcn v2.3.0 fixture + record-collection goldens (F8, F9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dspack #23 added the record-collection intent (P1.1); check-sync went correctly red on the fixture and it is re-synced here. New violating goldens, fail-first (the nine-count assertion and both expected files failed before this change): F8-records-as-prose (a card of prose where records belong — rule.record-collection-requires-table) and F9-table-headless-uncaptioned (a body-only table — rule.table-carries-structure + rule.table-carries-caption). Expected reports generated by the linter and hand-verified. Two test corrections for assumptions the single-intent contract era baked in: the compiler test's "all rules render into the system prompt" loop now mirrors the compiler's intent filtering (and asserts other intents' rules do NOT leak into the destructive context — the destructive context GOLDEN is byte-stable, so downstream pins hold); the eval golden moved by exactly one line, the contract sha256. Live validation under the extended contract (gpt-oss via Spark): governance converges 5/5 — one S3-clean first pass, five genuine one-round repairs into fully compliant tables (caption + header + body, 12/12 cells populated in the rich runs). All runs then end failed-gate at EMIT: dspack-emit's shadcnProfile has no table mapping ("unknown component 'table-caption'"), a pre-existing profile gap never exercised before — no one had ever emitted a shadcn table. Reported upstream as a follow-up candidate; not addressed here. Suite: 110 passed (was 108; +2 golden cases). Co-Authored-By: Claude Fable 5 --- fixtures/golden/eval/results.fake.json | 2 +- .../F8-records-as-prose.dsurface.json | 23 ++++ .../F8-records-as-prose.expected.json | 39 ++++++ ...9-table-headless-uncaptioned.dsurface.json | 49 ++++++++ ...9-table-headless-uncaptioned.expected.json | 54 ++++++++ fixtures/shadcn.v0_4.dspack.json | 116 +++++++++++++++++- src/core/compiler.test.ts | 16 ++- src/core/lint/lint.test.ts | 5 +- 8 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 fixtures/golden/violating/F8-records-as-prose.dsurface.json create mode 100644 fixtures/golden/violating/F8-records-as-prose.expected.json create mode 100644 fixtures/golden/violating/F9-table-headless-uncaptioned.dsurface.json create mode 100644 fixtures/golden/violating/F9-table-headless-uncaptioned.expected.json diff --git a/fixtures/golden/eval/results.fake.json b/fixtures/golden/eval/results.fake.json index d7e3c3a..0c153e0 100644 --- a/fixtures/golden/eval/results.fake.json +++ b/fixtures/golden/eval/results.fake.json @@ -3,7 +3,7 @@ "contract": { "name": "shadcn/ui", "dspack": "0.4", - "sha256": "3da1761b2bb69f700a5a33b05c39b7ae5bba0db1f5b3e3d86945c2db229cb81e" + "sha256": "168421bef05f9be6ccdec93a426c2f9bb2c6af682eaf56dc022a6f8c9b879fa4" }, "maxRepairs": 2, "cells": [ diff --git a/fixtures/golden/violating/F8-records-as-prose.dsurface.json b/fixtures/golden/violating/F8-records-as-prose.dsurface.json new file mode 100644 index 0000000..9e98fff --- /dev/null +++ b/fixtures/golden/violating/F8-records-as-prose.dsurface.json @@ -0,0 +1,23 @@ +{ + "dspackSurface": "0.1", + "system": "shadcn/ui", + "intent": "record-collection", + "root": { + "component": "card", + "children": [ + { + "component": "card-header", + "children": [ + { + "component": "card-title", + "text": "Open support tickets" + } + ] + }, + { + "component": "card-content", + "text": "There are three open tickets right now: #4812 about billing exports (urgent), #4809 about late two-factor codes, and #4801 about dark mode contrast. The billing one should be handled first." + } + ] + } +} \ No newline at end of file diff --git a/fixtures/golden/violating/F8-records-as-prose.expected.json b/fixtures/golden/violating/F8-records-as-prose.expected.json new file mode 100644 index 0000000..ae9932d --- /dev/null +++ b/fixtures/golden/violating/F8-records-as-prose.expected.json @@ -0,0 +1,39 @@ +{ + "gates": [ + { + "gate": "S1", + "name": "surface-schema", + "status": "PASS" + }, + { + "gate": "S2", + "name": "contract-vocabulary", + "status": "PASS" + }, + { + "gate": "S3", + "name": "governance", + "status": "FAIL" + } + ], + "findings": [ + { + "ruleId": "rule.record-collection-requires-table", + "type": "component-choice", + "requirement": "must", + "level": "error", + "message": "Required component 'table' does not appear in the surface.", + "rationale": "The data-table-with-row-actions pattern: a record collection is built from the Table primitives, not prose or ad-hoc stacks. Records share uniform fields; a semantic table is how they stay scannable, sortable, and navigable by assistive technology.", + "location": { + "path": "$.root", + "component": "surface" + }, + "exampleIds": [ + "ex.support-ticket-queue" + ] + } + ], + "errorCount": 1, + "warnCount": 0, + "pass": false +} diff --git a/fixtures/golden/violating/F9-table-headless-uncaptioned.dsurface.json b/fixtures/golden/violating/F9-table-headless-uncaptioned.dsurface.json new file mode 100644 index 0000000..2109b4e --- /dev/null +++ b/fixtures/golden/violating/F9-table-headless-uncaptioned.dsurface.json @@ -0,0 +1,49 @@ +{ + "dspackSurface": "0.1", + "system": "shadcn/ui", + "intent": "record-collection", + "root": { + "component": "table", + "children": [ + { + "component": "table-body", + "children": [ + { + "component": "table-row", + "children": [ + { + "component": "table-cell", + "text": "#4812" + }, + { + "component": "table-cell", + "text": "Cannot export billing statement" + }, + { + "component": "table-cell", + "text": "Urgent" + } + ] + }, + { + "component": "table-row", + "children": [ + { + "component": "table-cell", + "text": "#4809" + }, + { + "component": "table-cell", + "text": "Two-factor codes arrive late" + }, + { + "component": "table-cell", + "text": "Open" + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/fixtures/golden/violating/F9-table-headless-uncaptioned.expected.json b/fixtures/golden/violating/F9-table-headless-uncaptioned.expected.json new file mode 100644 index 0000000..5e4f8b6 --- /dev/null +++ b/fixtures/golden/violating/F9-table-headless-uncaptioned.expected.json @@ -0,0 +1,54 @@ +{ + "gates": [ + { + "gate": "S1", + "name": "surface-schema", + "status": "PASS" + }, + { + "gate": "S2", + "name": "contract-vocabulary", + "status": "PASS" + }, + { + "gate": "S3", + "name": "governance", + "status": "FAIL" + } + ], + "findings": [ + { + "ruleId": "rule.table-carries-structure", + "type": "required-composition", + "requirement": "must", + "level": "error", + "message": "Required sub-component 'table-header' (min 1) not found among descendants (found 0).", + "rationale": "The table's own constraints demand semantic structure: header cells as th within a TableHeader so screen readers can associate columns, and records inside a TableBody. A table without both is a grid of divs wearing table styling.", + "location": { + "path": "$.root", + "component": "table" + }, + "exampleIds": [ + "ex.support-ticket-queue" + ] + }, + { + "ruleId": "rule.table-carries-caption", + "type": "required-composition", + "requirement": "must", + "level": "error", + "message": "Required sub-component 'table-caption' (min 1) not found among descendants (found 0).", + "rationale": "The data-table-with-row-actions pattern requires an accessible name via TableCaption or an aria-label. The pattern permits either; the contract can only govern the declarative form, so the caption is the enforceable floor — an aria-label alternative is legitimate but inexpressible in current rule types and remains guidance.", + "location": { + "path": "$.root", + "component": "table" + }, + "exampleIds": [ + "ex.support-ticket-queue" + ] + } + ], + "errorCount": 2, + "warnCount": 0, + "pass": false +} diff --git a/fixtures/shadcn.v0_4.dspack.json b/fixtures/shadcn.v0_4.dspack.json index c897b04..6ebd8d7 100644 --- a/fixtures/shadcn.v0_4.dspack.json +++ b/fixtures/shadcn.v0_4.dspack.json @@ -3,7 +3,7 @@ "dspack": "0.4", "name": "shadcn/ui", "description": "A collection of reusable components built with Radix UI and Tailwind CSS. Components are copied into your project, not installed as a dependency.", - "version": "2.2.0", + "version": "2.3.0", "metadata": { "source": "https://ui.shadcn.com", "license": "MIT" @@ -1038,6 +1038,12 @@ "name": "Destructive action", "description": "The requested UI performs an irreversible or high-consequence operation: deleting records or accounts, revoking access, removing members.", "relatedPatterns": ["destructive-action-confirmation"] + }, + { + "id": "record-collection", + "name": "Record collection", + "description": "Surfaces that present many records of the same kind — support tickets, orders, members — for scanning and action. Records share uniform fields, so the collection renders as a semantic data table built from the Table primitives; status stays readable as text (badges are appropriate, color is never the only signal). Per-row actions and destructive confirmations follow the data-table-with-row-actions pattern as guidance until the dropdown-menu item vocabulary is specified.", + "relatedPatterns": ["data-table-with-row-actions"] } ], "rules": [ @@ -1094,6 +1100,41 @@ "forbiddenCategories": ["overlay"], "rationale": "An alert dialog is a single focused interruption. Stacking another overlay (dialog, dropdown menu, another alert dialog) inside it breaks focus containment and dismiss semantics and buries the confirmation decision under a second layer.", "tags": ["accessibility", "interaction"] + }, + { + "id": "rule.record-collection-requires-table", + "type": "component-choice", + "severity": "must", + "appliesTo": { "intents": ["record-collection"] }, + "require": ["table"], + "rationale": "The data-table-with-row-actions pattern: a record collection is built from the Table primitives, not prose or ad-hoc stacks. Records share uniform fields; a semantic table is how they stay scannable, sortable, and navigable by assistive technology.", + "examples": ["ex.support-ticket-queue"], + "tags": ["structure"] + }, + { + "id": "rule.table-carries-structure", + "type": "required-composition", + "severity": "must", + "appliesTo": { "intents": ["record-collection"] }, + "component": "table", + "requiredSubComponents": [ + { "id": "table-header", "min": 1 }, + { "id": "table-body", "min": 1 } + ], + "rationale": "The table's own constraints demand semantic structure: header cells as th within a TableHeader so screen readers can associate columns, and records inside a TableBody. A table without both is a grid of divs wearing table styling.", + "examples": ["ex.support-ticket-queue"], + "tags": ["structure", "accessibility"] + }, + { + "id": "rule.table-carries-caption", + "type": "required-composition", + "severity": "must", + "appliesTo": { "intents": ["record-collection"] }, + "component": "table", + "requiredSubComponents": [{ "id": "table-caption", "min": 1 }], + "rationale": "The data-table-with-row-actions pattern requires an accessible name via TableCaption or an aria-label. The pattern permits either; the contract can only govern the declarative form, so the caption is the enforceable floor — an aria-label alternative is legitimate but inexpressible in current rule types and remains guidance.", + "examples": ["ex.support-ticket-queue"], + "tags": ["accessibility"] } ], "examples": [ @@ -1145,6 +1186,79 @@ ] } } + }, + { + "id": "ex.support-ticket-queue", + "intent": "record-collection", + "name": "Support ticket queue", + "prompt": "a queue of open support tickets with status and priority", + "description": "A semantic data table from the Table primitives: a caption naming the collection, header cells for column association, and ticket rows in the body. Status renders as a Badge whose text stays readable — the variant reflects state, color is never the only signal.", + "surface": { + "dspackSurface": "0.1", + "system": "shadcn/ui", + "intent": "record-collection", + "root": { + "component": "table", + "children": [ + { "component": "table-caption", "text": "Open support tickets, newest first." }, + { + "component": "table-header", + "children": [ + { + "component": "table-row", + "children": [ + { "component": "table-head", "text": "Ticket" }, + { "component": "table-head", "text": "Subject" }, + { "component": "table-head", "text": "Status" }, + { "component": "table-head", "text": "Priority" } + ] + } + ] + }, + { + "component": "table-body", + "children": [ + { + "component": "table-row", + "children": [ + { "component": "table-cell", "text": "#4812" }, + { "component": "table-cell", "text": "Cannot export billing statement" }, + { + "component": "table-cell", + "children": [{ "component": "badge", "props": { "variant": "destructive" }, "text": "Urgent" }] + }, + { "component": "table-cell", "text": "P1" } + ] + }, + { + "component": "table-row", + "children": [ + { "component": "table-cell", "text": "#4809" }, + { "component": "table-cell", "text": "Two-factor codes arrive late" }, + { + "component": "table-cell", + "children": [{ "component": "badge", "props": { "variant": "secondary" }, "text": "Waiting on customer" }] + }, + { "component": "table-cell", "text": "P2" } + ] + }, + { + "component": "table-row", + "children": [ + { "component": "table-cell", "text": "#4801" }, + { "component": "table-cell", "text": "Dark mode contrast on invoices" }, + { + "component": "table-cell", + "children": [{ "component": "badge", "props": { "variant": "outline" }, "text": "Open" }] + }, + { "component": "table-cell", "text": "P3" } + ] + } + ] + } + ] + } + } } ], "frameworkBindings": { diff --git a/src/core/compiler.test.ts b/src/core/compiler.test.ts index c8a613c..a4e8faf 100644 --- a/src/core/compiler.test.ts +++ b/src/core/compiler.test.ts @@ -30,10 +30,20 @@ describe("compileContext golden", () => { expect(rendered).toBe(readFileSync(GOLDEN, "utf8")); }); - it("renders all three rules with rationales into the system prompt", () => { + it("renders every applicable rule with its rationale into the system prompt", () => { + // The contract carries intent-scoped rules for more than one intent + // (v2.3.0 added record-collection); the destructive-action context + // renders exactly the rules that apply to it — unscoped rules plus its + // own — and none of another intent's. + const applies = (rule: NonNullable[number]) => + !rule.appliesTo || rule.appliesTo.intents.includes("destructive-action"); for (const rule of contract.rules!) { - expect(context.system).toContain(rule.id); - expect(context.system).toContain(rule.rationale); + if (applies(rule)) { + expect(context.system).toContain(rule.id); + expect(context.system).toContain(rule.rationale); + } else { + expect(context.system).not.toContain(rule.id); + } } expect(context.system).toContain("dialog is forbidden"); }); diff --git a/src/core/lint/lint.test.ts b/src/core/lint/lint.test.ts index bbe8262..f9eb081 100644 --- a/src/core/lint/lint.test.ts +++ b/src/core/lint/lint.test.ts @@ -26,8 +26,9 @@ const load = (path: string) => JSON.parse(readFileSync(path, "utf8")); describe("violating goldens reproduce their expected reports exactly", () => { const fixtures = readdirSync(VIOLATING).filter((f) => f.endsWith(".dsurface.json")); - it("all seven violating fixtures are present", () => { - expect(fixtures.length).toBe(7); + it("all nine violating fixtures are present", () => { + // F1-F7: destructive-action (v2.2.0). F8-F9: record-collection (v2.3.0). + expect(fixtures.length).toBe(9); }); it.each(fixtures)("%s", (fixture) => {