Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fixtures/golden/eval/results.fake.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"contract": {
"name": "shadcn/ui",
"dspack": "0.4",
"sha256": "3da1761b2bb69f700a5a33b05c39b7ae5bba0db1f5b3e3d86945c2db229cb81e"
"sha256": "168421bef05f9be6ccdec93a426c2f9bb2c6af682eaf56dc022a6f8c9b879fa4"
},
"maxRepairs": 2,
"cells": [
Expand Down
23 changes: 23 additions & 0 deletions fixtures/golden/violating/F8-records-as-prose.dsurface.json
Original file line number Diff line number Diff line change
@@ -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."
}
]
}
}
39 changes: 39 additions & 0 deletions fixtures/golden/violating/F8-records-as-prose.expected.json
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -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
}
116 changes: 115 additions & 1 deletion fixtures/shadcn.v0_4.dspack.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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": {
Expand Down
16 changes: 13 additions & 3 deletions src/core/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof contract.rules>[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);
}
Comment on lines +44 to +46
}
expect(context.system).toContain("dialog is forbidden");
});
Expand Down
5 changes: 3 additions & 2 deletions src/core/lint/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Comment on lines +29 to 32

it.each(fixtures)("%s", (fixture) => {
Expand Down
Loading