From c43a12146578ff34e68583a3d52461180ed42969 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 17:19:13 -0400 Subject: [PATCH 1/4] =?UTF-8?q?eval:=20re-pin=20the=20fixture=20to=20contr?= =?UTF-8?q?act=203.2.0=20=E2=80=94=20the=20dspack#40=20field=20correction,?= =?UTF-8?q?=20consumed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The corrected contract (dspack d50f049) declares the field family right-side-up: field-set and field-group are compounds of their own (1 sub each), field keeps its 5 inner parts. The scaffold derives the same shapes mechanically; nothing was resolved by hand. Denominator honesty: 106 -> 104 subs under 21 mapped compounds; resolved stays 37; unresolved 69 -> 67. The two-sub delta is a REMODEL of the ledger's denominator, not resolution work — recorded as such so the series (86 -> 80 -> 72 -> 69 -> 67*) keeps its meaning. Fixture reproducible (rebuilt == committed) at the new pin sha256 55a02863…; suite 280/280. Co-Authored-By: Claude Fable 5 --- eval/build-eval-profile.mjs | 4 +- eval/shadcn-v3.dspack.json | 114 ++++++++++++++++++++----------- eval/shadcn-v3.eval.profile.json | 72 +++++++++++++++++-- src/eval-fixture.test.ts | 19 +++--- 4 files changed, 154 insertions(+), 55 deletions(-) diff --git a/eval/build-eval-profile.mjs b/eval/build-eval-profile.mjs index f94f722..f272fd6 100644 --- a/eval/build-eval-profile.mjs +++ b/eval/build-eval-profile.mjs @@ -40,8 +40,8 @@ const { scaffoldProfile } = await import(join(root, "dist/transform/scaffold.js" const { shadcnProfile } = await import(join(root, "dist/transform/profiles.js")); const { SHADCN_V2_SURFACES, SHADCN_V3_T1_SURFACES, SHADCN_V3_T2_PLANS, SHADCN_V3_T3_PLANS } = await import(join(root, "dist/transform/shadcn-v2-respelling.js")); -export const CONTRACT_SHA256 = "ea87346f85965937cb2b18e3998f19da7ba3cee41b41572638d657f20b3f5565"; -export const CONTRACT_COMMIT = "48643ff (aestheticfunction/dspack, merged as b573637 / PR #35)"; +export const CONTRACT_SHA256 = "55a02863af330bde1af15e896aac93d6d78109a3bdbbc27ad253ea210a858c93"; +export const CONTRACT_COMMIT = "bd2851b (aestheticfunction/dspack, merged as d50f049 / PR #42 — contract 3.2.0, the dspack#40 field correction)"; export function buildEvalProfile() { const contractBytes = readFileSync(join(root, "eval/shadcn-v3.dspack.json")); diff --git a/eval/shadcn-v3.dspack.json b/eval/shadcn-v3.dspack.json index 838263e..1163a5f 100644 --- a/eval/shadcn-v3.dspack.json +++ b/eval/shadcn-v3.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": "3.0.0", + "version": "3.2.0", "metadata": { "source": "https://ui.shadcn.com", "license": "MIT" @@ -2042,34 +2042,6 @@ }, "composition": { "subComponents": [ - { - "id": "field-set", - "name": "FieldSet", - "description": "A semantic fieldset that groups controls answering one question — a checkbox group, a radio group, an address block. Pair it with a FieldLegend so the group has an accessible name, and put the individual Fields in a FieldGroup inside it.", - "acceptsChildren": "components", - "categories": [ - "form" - ] - }, - { - "id": "field-legend", - "name": "FieldLegend", - "description": "The legend element that names a FieldSet and is announced with each control inside it. Takes a variant prop of \"legend\" (default, section-sized) or \"label\" (label-sized, for nested fieldsets or when the group reads as a single field, such as a checkbox group).", - "slot": "legend", - "acceptsChildren": "text", - "categories": [ - "form" - ] - }, - { - "id": "field-group", - "name": "FieldGroup", - "description": "A layout wrapper that stacks Fields with consistent spacing and establishes the @container/field-group container query that orientation=\"responsive\" resolves against. It carries no semantics, so it never substitutes for FieldSet. Add data-slot=\"checkbox-group\" when it holds a checkbox group so the spacing tightens.", - "acceptsChildren": "components", - "categories": [ - "form" - ] - }, { "id": "field-content", "name": "FieldContent", @@ -2119,18 +2091,9 @@ "categories": [ "form" ] - }, - { - "id": "field-separator", - "name": "FieldSeparator", - "description": "A divider between sections of a FieldGroup, optionally with inline text such as \"Or continue with\". Use it sparingly — every separator is a section boundary a screen reader user has to interpret.", - "acceptsChildren": "text", - "categories": [ - "form" - ] } ], - "notes": "The nesting the docs prescribe is FieldSet > FieldLegend + FieldDescription + FieldGroup > Field, and inside a Field: FieldLabel, the control, FieldDescription, FieldError — in that reading order, with FieldError immediately after the control it describes. When the label sits beside the control, wrap the label, description, and error in FieldContent so they stay aligned. Any labelable control belongs inside a Field: Input, Textarea, Select, Checkbox, RadioGroupItem, Switch, Slider, InputGroup, or your own. No sub-component is marked required, deliberately: in the documented choice-card and radio patterns FieldLabel is an ancestor of Field rather than a descendant, so a structural must-contain rule would be false — the labeling requirement is carried as a constraint instead. FieldGroup is required in practice only for orientation=\"responsive\", which resolves against the container query FieldGroup establishes." + "notes": "The nesting the docs prescribe is FieldSet > FieldLegend + FieldGroup > Field, and inside a Field: FieldLabel, the control, FieldDescription, FieldError — in that reading order, with FieldError immediately after the control it describes. FieldSet and FieldGroup are top-level components in this contract (they wrap Fields, they do not live inside one); upstream also allows a set-level FieldDescription, which this contract deliberately narrows to Field scope — every shipped example uses it there, and a set-level summary belongs to the legend. When the label sits beside the control, wrap the label, description, and error in FieldContent so they stay aligned. Any labelable control belongs inside a Field: Input, Textarea, Select, Checkbox, RadioGroupItem, Switch, Slider, InputGroup, or your own. No sub-component is marked required, deliberately: in the documented choice-card and radio patterns FieldLabel is an ancestor of Field rather than a descendant, so a structural must-contain rule would be false — the labeling requirement is carried as a constraint instead. FieldGroup is required in practice only for orientation=\"responsive\", which resolves against the container query FieldGroup establishes." }, "constraints": [ { @@ -2211,6 +2174,59 @@ "form" ] }, + "field-set": { + "name": "FieldSet", + "description": "A semantic fieldset that groups controls answering one question — a checkbox group, a radio group, an address block. Pair it with a FieldLegend so the group has an accessible name, and put the individual Fields in a FieldGroup inside it.", + "status": "stable", + "props": {}, + "categories": [ + "form" + ], + "tags": [ + "form", + "grouping" + ], + "composition": { + "subComponents": [ + { + "id": "field-legend", + "name": "FieldLegend", + "description": "The legend element that names a FieldSet and is announced with each control inside it. Takes a variant prop of \"legend\" (default, section-sized) or \"label\" (label-sized, for nested fieldsets or when the group reads as a single field, such as a checkbox group).", + "slot": "legend", + "acceptsChildren": "text", + "categories": [ + "form" + ] + } + ] + } + }, + "field-group": { + "name": "FieldGroup", + "description": "A layout wrapper that stacks Fields with consistent spacing and establishes the @container/field-group container query that orientation=\"responsive\" resolves against. It carries no semantics, so it never substitutes for FieldSet. Add data-slot=\"checkbox-group\" when it holds a checkbox group so the spacing tightens.", + "status": "stable", + "props": {}, + "categories": [ + "form" + ], + "tags": [ + "form", + "grouping" + ], + "composition": { + "subComponents": [ + { + "id": "field-separator", + "name": "FieldSeparator", + "description": "A divider between sections of a FieldGroup, optionally with inline text such as \"Or continue with\". Use it sparingly — every separator is a section boundary a screen reader user has to interpret.", + "acceptsChildren": "text", + "categories": [ + "form" + ] + } + ] + } + }, "form": { "name": "Form", "description": "The react-hook-form wrapper family. Form is a re-export of react-hook-form's FormProvider, FormField wraps its Controller and publishes the field name on context, and FormItem generates one React id per field from which every other id is derived: the control gets `${id}-form-item`, the description `${id}-form-item-description`, the message `${id}-form-item-message`. FormControl is a Radix Slot that clones the single control inside it and stamps on id, aria-describedby, and aria-invalid, so the label/description/error association is generated rather than hand-written. Form itself renders no DOM — you still write the native form element and pass form.handleSubmit(onSubmit) to its onSubmit.", @@ -7043,6 +7059,26 @@ "interaction", "form" ] + }, + { + "id": "rule.form-control-carries-control", + "type": "required-composition", + "severity": "must", + "component": "form-control", + "requiredCategories": [ + { + "id": "interactive", + "min": 1 + } + ], + "rationale": "A form-control represents the location of the user-editable control in a field. It must contain an approved interactive control; text-only or empty form controls are not meaningful form composition.", + "examples": [ + "ex.expense-report-form" + ], + "x-source": { + "evidence": "dspack-emit eval/t1-build-matrix*.json — T1 Build runs generated lint-clean form-controls carrying literal text and no control; every gate passed and the emitter correctly refused", + "amendment": "spec v0.4 §4.3 (2026-08-07), the first §6 ceiling item lifted by measurement" + } } ], "examples": [ diff --git a/eval/shadcn-v3.eval.profile.json b/eval/shadcn-v3.eval.profile.json index 0b86922..56eac64 100644 --- a/eval/shadcn-v3.eval.profile.json +++ b/eval/shadcn-v3.eval.profile.json @@ -814,6 +814,64 @@ ] } }, + { + "a2ui": "FieldSet", + "dspackId": "field-set", + "commons": [ + "ComponentCommon" + ], + "structural": { + "children": { + "schema": { + "$ref": "#/$defs/ChildList" + }, + "description": "Child component IDs, in order.", + "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.notification-preferences'." + } + }, + "required": [ + "children" + ], + "surface": { + "routes": [ + { + "from": [ + "children" + ], + "to": "slots:children" + } + ] + } + }, + { + "a2ui": "FieldGroup", + "dspackId": "field-group", + "commons": [ + "ComponentCommon" + ], + "structural": { + "children": { + "schema": { + "$ref": "#/$defs/ChildList" + }, + "description": "Child component IDs, in order.", + "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.notification-preferences'." + } + }, + "required": [ + "children" + ], + "surface": { + "routes": [ + { + "from": [ + "children" + ], + "to": "slots:children" + } + ] + } + }, { "a2ui": "Form", "dspackId": "form", @@ -1873,14 +1931,16 @@ "sheet-close" ], "field": [ - "field-set", - "field-legend", - "field-group", "field-content", "field-label", "field-title", "field-description", - "field-error", + "field-error" + ], + "field-set": [ + "field-legend" + ], + "field-group": [ "field-separator" ], "form": [ @@ -1952,8 +2012,8 @@ "purpose": "Reproducible derived-coverage measurement of the production shadcn v3 contract against the current foundation.", "notStudioProfile": "Downstream Studio consumes the pinned v2.3.0 contract with the shipped v1 profile; this fixture is measurement-only.", "contract": { - "commit": "48643ff (aestheticfunction/dspack, merged as b573637 / PR #35)", - "sha256": "ea87346f85965937cb2b18e3998f19da7ba3cee41b41572638d657f20b3f5565" + "commit": "bd2851b (aestheticfunction/dspack, merged as d50f049 / PR #42 — contract 3.2.0, the dspack#40 field correction)", + "sha256": "55a02863af330bde1af15e896aac93d6d78109a3bdbbc27ad253ea210a858c93" }, "derivation": "scaffoldProfile(v3) + the six shipped plans transplanted with their byte-proven v2 re-spelling + the T1 transparent-identity resolutions (src/t1.test.ts) + the T2 item-mode collection plans (src/t2.test.ts) + the T3 declared-join plans (src/t3.test.ts); zero casualties declared, zero fresh judgment.", "unresolvedAreDeliberate": "Every unresolved sub-component listed by the coverage report is a real, open representation decision — do not resolve them here to make a number look better." diff --git a/src/eval-fixture.test.ts b/src/eval-fixture.test.ts index c79b3cc..78ac8a0 100644 --- a/src/eval-fixture.test.ts +++ b/src/eval-fixture.test.ts @@ -12,11 +12,14 @@ * scaffold of the pinned v3 contract, declares zero casualties, and lets * every open decision show as open: * - * 106 sub-components under 19 mapped compounds + * 104 sub-components under 21 mapped compounds (contract 3.2.0: the + * dspack#40 correction moved the field wrapper trio right-side-up — + * field keeps 5 inner parts; field-set and field-group are compounds + * of their own with 1 sub each, so the denominator moved 106 → 104) * 37 resolved — the transplanted families (card 5, table 7, * alert-dialog 8), T1's form family (6), T2's * radio-group (1) and select (7), T3's tabs (3) - * 69 unresolved — real, deliberate, and the measure of the T4–T5 work + * 67 unresolved — real, deliberate, and the measure of the A0/T4 work * * The v2 contract gate refusing this fixture against the v3 contract is not a * failure; it is the fatal coverage gate doing on the production corpus @@ -53,19 +56,19 @@ describe("the production-v3 evaluation fixture", () => { expect(profile.casualtyComponents).toEqual([]); }); - it("the fatal coverage gate refuses it against the v3 contract: 69 unresolved decisions, each pathed", () => { + it("the fatal coverage gate refuses it against the v3 contract: 67 unresolved decisions, each pathed", () => { try { transformFromJson(contract, { profile: loadProfile(structuredClone(fixtureJson)) }); - expect.unreachable("69 open representation decisions must refuse, not emit"); + expect.unreachable("67 open representation decisions must refuse, not emit"); } catch (e) { expect(e).toBeInstanceOf(ProfileContractError); const issues = (e as ProfileContractError).issues; - expect(issues).toHaveLength(69); + expect(issues).toHaveLength(67); expect(issues.every((i) => i.message.includes("unresolved"))).toBe(true); } }); - it("derived coverage: 106 subs, 37 resolved (transplants + T1-T3), 69 open", () => { + it("derived coverage: 104 subs, 37 resolved (transplants + T1-T3), 67 open", () => { const profile = loadProfile(structuredClone(fixtureJson)); const byId = new Map(profile.components.map((p: ComponentPlan) => [p.dspackId, p])); let resolved = 0; @@ -86,8 +89,8 @@ describe("the production-v3 evaluation fixture", () => { } } } - expect(resolved + unresolved).toBe(106); - expect(unresolved).toBe(69); + expect(resolved + unresolved).toBe(104); + expect(unresolved).toBe(67); expect(Object.fromEntries(resolvedByCompound)).toEqual({ card: 5, table: 7, "alert-dialog": 8, form: 6, "radio-group": 1, select: 7, tabs: 3 }); }); }); From e467068b134b8a06316c3d89958f697ee96a3a34 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 17:29:50 -0400 Subject: [PATCH 2/4] =?UTF-8?q?feat(a0):=20the=20zero-new-capability=20aut?= =?UTF-8?q?horing=20pass=20=E2=80=94=20production=20judgment=20on=20the=20?= =?UTF-8?q?shipped=20vocabulary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six compound plans and five label additions, every decision grounded in measured usage (14 worked examples, contract 3.2.0) and proven by src/a0.test.ts against production subtrees verbatim: - alert: title/description lifted from their subs (5/5 examples); alert-action dropped WITH RECORD naming T4 as the mechanism it waits for (one production instance; the flatten ledger marks the loss lossy); - avatar: fallback lifted; avatar-image dropped naming the contract gap (an EMPTY node — no src/alt declared; dspack#39 class); the three unused subs dropped pending usage evidence; - scroll-area: T1 transparency; scrollbar chrome dropped; - field (per the dspack#40 correction): its own donating boundary — field-label donates onto the single control; description/title asText; error dropped like form-message; content transparent; - field-set: transparent, legend asText h4; field-group: transparent, separator dropped; - label additions: switch, checkbox, textarea, progress, select gain the structural label slot the T1 donation lands on (input already had it). One composition BUG found by the re-measurement and fixed (not a new primitive): the T3 joined-children path fed raw children to emitNode, skipping the parent-style rewrite that dissolves transparent children — a transparent scroll-area inside a tabs panel refused as an unroutable instance (ex.project-workspace-panels). The join path now mirrors the transparent-root host pattern (rewriteChildren over an empty model), pinned in a0.test.ts. Measured results: - ledger 67 -> 50 unresolved (alert 3, avatar 5, scroll-area 2, field 5, field-set 1, field-group 1); resolved 37 -> 54 of 104; - example emission 2/14 -> 9/14, every loss recorded (no silent approximation); the five remaining refusals are the named classes: standalone label (deferred), dialog + sheet (T4), breadcrumb + tooltip (T6 blocked-upstream); - renderer honesty: none of Alert/Avatar/Textarea/Checkbox/Switch/ Progress/Separator/Spinner/Skeleton has a studio renderer yet; ScrollArea/Field/FieldSet/FieldGroup need none (they emit nothing). Suite 289/289; fixture reproducible; v1 byte pins and the v2 keystone untouched. Co-Authored-By: Claude Fable 5 --- eval/build-eval-profile.mjs | 22 +- eval/shadcn-v3.eval.profile.json | 300 ++++++++++++-------------- src/a0.test.ts | 235 ++++++++++++++++++++ src/eval-fixture.test.ts | 20 +- src/targets/a2ui/surface.ts | 24 ++- src/transform/shadcn-v2-respelling.ts | 159 ++++++++++++++ 6 files changed, 587 insertions(+), 173 deletions(-) create mode 100644 src/a0.test.ts diff --git a/eval/build-eval-profile.mjs b/eval/build-eval-profile.mjs index f272fd6..5ed8795 100644 --- a/eval/build-eval-profile.mjs +++ b/eval/build-eval-profile.mjs @@ -38,7 +38,7 @@ import { fileURLToPath } from "node:url"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const { scaffoldProfile } = await import(join(root, "dist/transform/scaffold.js")); const { shadcnProfile } = await import(join(root, "dist/transform/profiles.js")); -const { SHADCN_V2_SURFACES, SHADCN_V3_T1_SURFACES, SHADCN_V3_T2_PLANS, SHADCN_V3_T3_PLANS } = await import(join(root, "dist/transform/shadcn-v2-respelling.js")); +const { SHADCN_V2_SURFACES, SHADCN_V3_T1_SURFACES, SHADCN_V3_T2_PLANS, SHADCN_V3_T3_PLANS, SHADCN_V3_A0_PLANS, SHADCN_V3_A0_LABEL_ADDITIONS } = await import(join(root, "dist/transform/shadcn-v2-respelling.js")); export const CONTRACT_SHA256 = "55a02863af330bde1af15e896aac93d6d78109a3bdbbc27ad253ea210a858c93"; export const CONTRACT_COMMIT = "bd2851b (aestheticfunction/dspack, merged as d50f049 / PR #42 — contract 3.2.0, the dspack#40 field correction)"; @@ -84,9 +84,27 @@ export function buildEvalProfile() { // T3 resolutions (proven in src/t3.test.ts): declared key joins. const t3 = SHADCN_V3_T3_PLANS[plan.dspackId]; if (t3) return structuredClone(t3); + // A0 authoring resolutions (proven in src/a0.test.ts): production + // profile judgment on the shipped primitive vocabulary — no new + // capability. Compound plans replace; leaves keep their scaffold. + const a0 = SHADCN_V3_A0_PLANS[plan.dspackId]; + if (a0) return structuredClone(a0); return plan; }); + // A0 label additions: the five field-control leaves gain the structural + // `label` slot the T1 donation lands on (input already declares it). + // Applied after every replacement so select's T2 plan receives it too. + for (const [id, note] of Object.entries(SHADCN_V3_A0_LABEL_ADDITIONS)) { + const plan = profile.components.find((c) => c.dspackId === id); + if (plan && !(plan.structural ?? {}).label) { + plan.structural = { + ...(plan.structural ?? {}), + label: { schema: { type: "string" }, description: note.description, synthNote: note.synthNote }, + }; + } + } + profile["x-eval"] = { purpose: "Reproducible derived-coverage measurement of the production shadcn v3 contract against the current foundation.", @@ -94,7 +112,7 @@ export function buildEvalProfile() { "Downstream Studio consumes the pinned v2.3.0 contract with the shipped v1 profile; this fixture is measurement-only.", contract: { commit: CONTRACT_COMMIT, sha256: CONTRACT_SHA256 }, derivation: - "scaffoldProfile(v3) + the six shipped plans transplanted with their byte-proven v2 re-spelling + the T1 transparent-identity resolutions (src/t1.test.ts) + the T2 item-mode collection plans (src/t2.test.ts) + the T3 declared-join plans (src/t3.test.ts); zero casualties declared, zero fresh judgment.", + "scaffoldProfile(v3) + the six shipped plans transplanted with their byte-proven v2 re-spelling + the T1 transparent-identity resolutions (src/t1.test.ts) + the T2 item-mode collection plans (src/t2.test.ts) + the T3 declared-join plans (src/t3.test.ts) + the A0 authoring resolutions and label additions (src/a0.test.ts — production profile JUDGMENT on the shipped vocabulary, the one deliberate exception to zero-fresh-judgment, each decision grounded in measured usage and proven by test); zero casualties declared.", unresolvedAreDeliberate: "Every unresolved sub-component listed by the coverage report is a real, open representation decision — do not resolve them here to make a number look better.", }; diff --git a/eval/shadcn-v3.eval.profile.json b/eval/shadcn-v3.eval.profile.json index 56eac64..40d64e1 100644 --- a/eval/shadcn-v3.eval.profile.json +++ b/eval/shadcn-v3.eval.profile.json @@ -773,45 +773,29 @@ "commons": [ "ComponentCommon" ], - "structural": { - "children": { - "schema": { - "$ref": "#/$defs/ChildList" - }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.delete-project-confirmation'." - } - }, - "required": [ - "children" - ], - "propMap": { - "orientation": { - "a2ui": "orientation", - "kind": "enum", - "targetEnum": [ - "vertical", - "horizontal", - "responsive" - ], - "default": "vertical", - "description": "How the label and control are arranged. Also exposed on the element as data-orientation, which FieldDescription reads to balance long helper text in horizontal layouts." - }, - "data-invalid": { - "a2ui": "data-invalid", - "kind": "boolean", - "description": "Switches the whole field block into the error treatment, including the label and description. It is styling only and is never exposed to assistive technology, so it does not replace aria-invalid on the control itself — set both. With react-hook-form this is data-invalid={fieldState.invalid}." - } - }, + "structural": {}, + "required": [], "surface": { - "routes": [ - { - "from": [ - "children" - ], - "to": "slots:children" + "transparent": { + "donate": [ + { + "from": "sub(field-label).text", + "to": "prop:label" + } + ] + }, + "subs": { + "field-content": "transparent", + "field-title": { + "asText": "h4" + }, + "field-description": { + "asText": "caption" + }, + "field-error": { + "drop": "validation state is runtime data the declarative surface does not carry (mirrors form-message)" } - ] + } } }, { @@ -820,27 +804,15 @@ "commons": [ "ComponentCommon" ], - "structural": { - "children": { - "schema": { - "$ref": "#/$defs/ChildList" - }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.notification-preferences'." - } - }, - "required": [ - "children" - ], + "structural": {}, + "required": [], "surface": { - "routes": [ - { - "from": [ - "children" - ], - "to": "slots:children" + "transparent": true, + "subs": { + "field-legend": { + "asText": "h4" } - ] + } } }, { @@ -849,27 +821,15 @@ "commons": [ "ComponentCommon" ], - "structural": { - "children": { - "schema": { - "$ref": "#/$defs/ChildList" - }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.notification-preferences'." - } - }, - "required": [ - "children" - ], + "structural": {}, + "required": [], "surface": { - "routes": [ - { - "from": [ - "children" - ], - "to": "slots:children" + "transparent": true, + "subs": { + "field-separator": { + "drop": "a visual section boundary; its optional inline text is presentation of the divider, and no worked example carries one" } - ] + } } }, { @@ -937,17 +897,14 @@ "ComponentCommon" ], "structural": { - "children": { + "fallback": { "schema": { - "$ref": "#/$defs/ChildList" + "type": "string" }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.order-detail-summary'." + "description": "Initials shown while no image is available.", + "synthNote": "Lifted from the avatar-fallback sub's text." } }, - "required": [ - "children" - ], "propMap": { "size": { "a2ui": "size", @@ -957,19 +914,35 @@ "default", "lg" ], - "default": "default", - "description": "Preset avatar diameter. AvatarBadge and AvatarGroupCount scale from this value, so change the size here rather than overriding the box with utility classes." + "default": "default" } }, + "required": [ + "fallback" + ], "surface": { "routes": [ { "from": [ - "children" + "sub(avatar-fallback).text" ], - "to": "slots:children" + "to": "prop:fallback" } - ] + ], + "subs": { + "avatar-image": { + "drop": "the contract declares no src/alt on avatar-image — an empty node carries nothing to project (contract-gap class, cf. dspack#39)" + }, + "avatar-badge": { + "drop": "no worked example exercises it; no measured shape to project — revisit with usage evidence" + }, + "avatar-group": { + "drop": "no worked example exercises it; a grouped-avatars shape is repetition to be measured, not guessed" + }, + "avatar-group-count": { + "drop": "no worked example exercises it; belongs to the unmeasured avatar-group shape" + } + } } }, { @@ -1049,56 +1022,18 @@ "commons": [ "ComponentCommon" ], - "structural": { - "children": { - "schema": { - "$ref": "#/$defs/ChildList" - }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.project-workspace-panels'." - } - }, - "required": [ - "children" - ], - "propMap": { - "type": { - "a2ui": "type", - "kind": "enum", - "targetEnum": [ - "hover", - "scroll", - "auto", - "always" - ], - "default": "hover", - "description": "When the custom scrollbars are shown. Anything other than auto or always hides the strongest signal that there is more content, so pair hover or scroll with another cue." - }, - "scrollHideDelay": { - "a2ui": "scrollHideDelay", - "kind": "number", - "description": "Milliseconds to wait after scrolling stops or the pointer leaves before hiding the scrollbars. Applies only when type is \"hover\" or \"scroll\"." - }, - "dir": { - "a2ui": "dir", - "kind": "enum", - "targetEnum": [ - "ltr", - "rtl" - ], - "default": "ltr", - "description": "Reading direction, which decides which side the vertical scrollbar sits on and where horizontal scrolling begins. Inherit it from a DirectionProvider where you can." - } - }, + "structural": {}, + "required": [], "surface": { - "routes": [ - { - "from": [ - "children" - ], - "to": "slots:children" + "transparent": true, + "subs": { + "scroll-area-scrollbar": { + "drop": "scrollbar chrome: presentation of the scroll container, not content" + }, + "scroll-area-thumb": { + "drop": "the scrollbar's drag indicator: chrome, not content" } - ] + } } }, { @@ -1306,7 +1241,15 @@ "commons": [ "ComponentCommon" ], - "structural": {}, + "structural": { + "label": { + "schema": { + "type": "string" + }, + "description": "The label naming the long-text entry.", + "synthNote": "Sourced by the T1 field-label/form-label donation, exactly as on input." + } + }, "required": [], "propMap": { "value": { @@ -1362,7 +1305,15 @@ "commons": [ "ComponentCommon" ], - "structural": {}, + "structural": { + "label": { + "schema": { + "type": "string" + }, + "description": "The label naming what checking the box asserts.", + "synthNote": "Sourced by the T1 form-label donation (ex.expense-report-form's billable field)." + } + }, "required": [], "propMap": { "disabled": { @@ -1394,7 +1345,15 @@ "commons": [ "ComponentCommon" ], - "structural": {}, + "structural": { + "label": { + "schema": { + "type": "string" + }, + "description": "The label naming what the switch turns on or off.", + "synthNote": "Sourced by the T1 field-label/form-label donation; 4/4 worked-example switches sit inside a field." + } + }, "required": [], "propMap": { "checked": { @@ -1541,6 +1500,13 @@ }, "description": "The selectable options.", "synthNote": "A2UI models repeated options as data on the control (T2 item-mode collection)." + }, + "label": { + "schema": { + "type": "string" + }, + "description": "The label naming the choice being made.", + "synthNote": "Sourced by the T1 field-label/form-label donation when the select is a field's control (ex.invite-teammates-dialog)." } }, "propMap": { @@ -1587,17 +1553,21 @@ "ComponentCommon" ], "structural": { - "children": { + "title": { "schema": { - "$ref": "#/$defs/ChildList" + "type": "string" }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.delete-project-confirmation'." + "description": "The one-line heading naming what happened.", + "synthNote": "Lifted from the alert-title sub (5/5 worked examples carry it there)." + }, + "description": { + "schema": { + "type": "string" + }, + "description": "Supporting detail under the title.", + "synthNote": "Lifted from the alert-description sub." } }, - "required": [ - "children" - ], "propMap": { "variant": { "a2ui": "variant", @@ -1606,24 +1576,32 @@ "default", "destructive" ], - "default": "default", - "description": "Visual treatment. shadcn ships only these two variants — there is no success or warning variant — so other tones are built by adding utility classes through className." - }, - "className": { - "a2ui": "className", - "kind": "string", - "description": "Additional CSS classes. This is the sanctioned way to produce tones shadcn does not ship a variant for (for example bg-amber-50 dark:bg-amber-950 for a warning) and to constrain the alert's width; the component is w-full and fills its parent." + "default": "default" } }, + "required": [ + "title" + ], "surface": { "routes": [ { "from": [ - "children" + "sub(alert-title).text" ], - "to": "slots:children" + "to": "prop:title" + }, + { + "from": [ + "sub(alert-description).text" + ], + "to": "prop:description" } - ] + ], + "subs": { + "alert-action": { + "drop": "an action slot holds component children (a button), and routing a sub's children to a slot is T4's mechanism — dropped WITH RECORD until T4, not approximated (one production instance: ex.import-run-status)" + } + } } }, { @@ -1632,7 +1610,15 @@ "commons": [ "ComponentCommon" ], - "structural": {}, + "structural": { + "label": { + "schema": { + "type": "string" + }, + "description": "The label naming what is progressing.", + "synthNote": "Sourced by the T1 field-label donation; 2/2 worked-example progress bars sit inside a field." + } + }, "required": [ "value" ], @@ -2015,7 +2001,7 @@ "commit": "bd2851b (aestheticfunction/dspack, merged as d50f049 / PR #42 — contract 3.2.0, the dspack#40 field correction)", "sha256": "55a02863af330bde1af15e896aac93d6d78109a3bdbbc27ad253ea210a858c93" }, - "derivation": "scaffoldProfile(v3) + the six shipped plans transplanted with their byte-proven v2 re-spelling + the T1 transparent-identity resolutions (src/t1.test.ts) + the T2 item-mode collection plans (src/t2.test.ts) + the T3 declared-join plans (src/t3.test.ts); zero casualties declared, zero fresh judgment.", + "derivation": "scaffoldProfile(v3) + the six shipped plans transplanted with their byte-proven v2 re-spelling + the T1 transparent-identity resolutions (src/t1.test.ts) + the T2 item-mode collection plans (src/t2.test.ts) + the T3 declared-join plans (src/t3.test.ts) + the A0 authoring resolutions and label additions (src/a0.test.ts — production profile JUDGMENT on the shipped vocabulary, the one deliberate exception to zero-fresh-judgment, each decision grounded in measured usage and proven by test); zero casualties declared.", "unresolvedAreDeliberate": "Every unresolved sub-component listed by the coverage report is a real, open representation decision — do not resolve them here to make a number look better." } } diff --git a/src/a0.test.ts b/src/a0.test.ts new file mode 100644 index 0000000..f12338a --- /dev/null +++ b/src/a0.test.ts @@ -0,0 +1,235 @@ +/** + * A0: the zero-new-capability authoring milestone — production-v3 profile + * judgment expressed entirely in the shipped Identity/Routing/Repetition + * vocabulary (T1 transparency + donation, routes, drops-with-record). No + * new primitive; every decision grounded in measured usage and recorded + * with fidelity. + * + * Fail-first: before this overlay the six compound families sat in the + * 67-unresolved ledger (alert 3, avatar 5, scroll-area 2, field 5, + * field-set 1, field-group 1 — eval-fixture.test.ts pins the 67), and the + * touched examples refused end-to-end (ex.order-detail-summary on + * 'avatar', ex.delete-project-confirmation on 'alert', …). + * + * The two honest losses, dropped WITH RECORD, never approximated: + * - alert-action holds component children; routing a sub's children to a + * slot is T4's mechanism — one production instance affected; + * - avatar-image is an EMPTY node (the contract declares no src/alt — + * the dspack#39 contract-gap class, named on the drop). + */ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { transformFromJson } from "./transform/index.js"; +import { emitSurface } from "./targets/a2ui/surface.js"; +import { loadProfile } from "./transform/profile-load.js"; +import { SHADCN_V3_A0_PLANS, SHADCN_V3_A0_LABEL_ADDITIONS } from "./transform/shadcn-v2-respelling.js"; +import type { DspackDoc, DspackSurface } from "./types.js"; + +const repo = (p: string) => fileURLToPath(new URL(`../${p}`, import.meta.url)); +const contract = JSON.parse(readFileSync(repo("eval/shadcn-v3.dspack.json"), "utf8")) as DspackDoc & { + examples: Array<{ id: string; surface: DspackSurface }>; +}; +const fixture = JSON.parse(readFileSync(repo("eval/shadcn-v3.eval.profile.json"), "utf8")); + +/** The builder's A0 merge, hermetically: keep a family set, apply the overlay. */ +const a0Profile = (keep: string[]) => { + const base = structuredClone(fixture) as Record & { + components: Array & { dspackId: string; structural?: Record }>; + }; + base.components = base.components.map((plan) => { + const a0 = SHADCN_V3_A0_PLANS[plan.dspackId]; + return a0 ? (structuredClone(a0) as typeof plan) : plan; + }); + for (const [id, note] of Object.entries(SHADCN_V3_A0_LABEL_ADDITIONS)) { + const plan = base.components.find((c) => c.dspackId === id); + if (plan && !(plan.structural ?? {}).label) { + plan.structural = { ...(plan.structural ?? {}), label: { schema: { type: "string" }, description: note.description, synthNote: note.synthNote } }; + } + } + const keepSet = new Set(keep); + base.intentionallyOmitted = base.components.filter((c) => !keepSet.has(c.dspackId)).map((c) => c.dspackId).sort(); + base.components = base.components.filter((c) => keepSet.has(c.dspackId)); + delete (base as Record)["x-scaffold"]; + return loadProfile(base); +}; + +const surfaceOf = (root: unknown, intent = "record-detail"): DspackSurface => + ({ dspackSurface: "0.1", system: contract.name as string, intent, root }) as DspackSurface; + +/** Find the first subtree of a component in a worked example, verbatim. */ +const fromExample = (exampleId: string, component: string): Record => { + const ex = contract.examples.find((e) => e.id === exampleId)!; + let hit: Record | undefined; + const walk = (n: Record): void => { + if (hit) return; + if (n.component === component) { hit = n; return; } + for (const c of (n.children as Record[] | undefined) ?? []) walk(c); + }; + walk(ex.surface.root as unknown as Record); + expect(hit, `${component} in ${exampleId}`).toBeDefined(); + return structuredClone(hit!); +}; + +const firstInstance = (messages: unknown[], component: string): Record => { + const comps = (messages[1] as { updateComponents: { components: Array> } }).updateComponents.components; + const inst = comps.find((c) => c.component === component); + expect(inst, `an emitted ${component}`).toBeDefined(); + return inst!; +}; + +describe("A0 — alert (routes over the shipped vocabulary)", () => { + it("the production delete-project alert emits {title, description}", () => { + const profile = a0Profile(["card", "alert", "badge", "button"]); + const { messages } = emitSurface(surfaceOf(fromExample("ex.delete-project-confirmation", "alert")), contract, { profile }); + const alert = firstInstance(messages, "Alert"); + expect(typeof alert.title).toBe("string"); + expect((alert.title as string).length).toBeGreaterThan(0); + expect(typeof alert.description).toBe("string"); + const check = transformFromJson(contract, { profile, surface: { messages } }); + expect(check.validation.gates.find((g) => g.name === "instance")?.pass).toBe(true); + }); + + it("alert-action's loss is recorded, never silent: the flatten ledger marks it lossy and the coverage detail names T4", () => { + const profile = a0Profile(["card", "alert", "badge", "button"]); + const { messages, warnings, fidelity } = emitSurface( + surfaceOf(fromExample("ex.import-run-status", "alert")), contract, { profile }); + firstInstance(messages, "Alert"); + // Runtime record: the consumed subtree is ledgered lossy (the engine's + // consumesSubtree record — the action button is inside it). + expect(warnings.some((w) => w.code === "surface-composition-flattened")).toBe(true); + expect(fidelity.some((f) => f.kind === "flattened" && f.class === "lossy" && f.source.includes("alert"))).toBe(true); + // Documented judgment: the coverage ledger carries the drop reason, + // naming T4 as the mechanism the slot waits for. + const out = transformFromJson(contract, { profile }); + const row = out.mapping.coverage.find((c) => c.id === "alert.alert-action"); + expect(row?.disposition).toBe("mapped"); + expect(row?.detail).toContain("T4"); + }); +}); + +describe("A0 — avatar", () => { + it("the production order-detail avatar emits {size, fallback}; the empty avatar-image drop names the contract gap", () => { + const profile = a0Profile(["card", "avatar", "badge"]); + const { messages, fidelity } = emitSurface(surfaceOf(fromExample("ex.order-detail-summary", "avatar")), contract, { profile }); + const avatar = firstInstance(messages, "Avatar"); + expect(avatar.fallback).toBe("AO"); + expect(avatar.size).toBe("lg"); + // The consumed subtree (with the empty avatar-image inside) is ledgered + // lossy; the drop's dspack#39 rationale lives on the coverage row. + expect(fidelity.some((f) => f.kind === "flattened" && f.class === "lossy")).toBe(true); + const out = transformFromJson(contract, { profile }); + expect(out.mapping.coverage.find((c) => c.id === "avatar.avatar-image")?.detail).toContain("dspack#39"); + const check = transformFromJson(contract, { profile, surface: { messages } }); + expect(check.validation.gates.find((g) => g.name === "instance")?.pass).toBe(true); + }); +}); + +describe("A0 — scroll-area dissolves (T1 vocabulary, no instance)", () => { + it("children rise; scrollbar chrome drops with record", () => { + const profile = a0Profile(["card", "scroll-area", "badge"]); + const { messages } = emitSurface( + surfaceOf({ + component: "card", + children: [{ component: "scroll-area", children: [{ component: "badge", text: "42 open" }] }], + }), + contract, + { profile }, + ); + const comps = (messages[1] as { updateComponents: { components: Array> } }).updateComponents.components; + expect(comps.some((c) => c.component === "ScrollArea")).toBe(false); + expect(comps.some((c) => c.component === "Badge")).toBe(true); + }); +}); + +describe("A0 — the corrected field family (dspack#40) emits end-to-end", () => { + it("the production notification-preferences field-set subtree: legend becomes text, groups dissolve, labels donate onto switches", () => { + const profile = a0Profile(["card", "field", "field-set", "field-group", "switch", "radio-group", "separator", "badge", "button"]); + const subtree = fromExample("ex.notification-preferences", "field-set"); + const { messages, fidelity } = emitSurface(surfaceOf(subtree, "preference-settings"), contract, { profile }); + const comps = (messages[1] as { updateComponents: { components: Array> } }).updateComponents.components; + // No wrapper survives. + for (const gone of ["FieldSet", "FieldGroup", "Field"]) { + expect(comps.some((c) => c.component === gone), `${gone} must not emit`).toBe(false); + } + // The legend text survives as a Text instance. + expect(comps.some((c) => c.component === "Text")).toBe(true); + // Every switch carries its donated label. + const switches = comps.filter((c) => c.component === "Switch"); + expect(switches.length).toBeGreaterThan(0); + for (const sw of switches) expect(typeof sw.label).toBe("string"); + // Donation provenance is ledgered. + expect(fidelity.some((f) => f.note?.includes("donated by 'field-label'"))).toBe(true); + const check = transformFromJson(contract, { profile, surface: { messages } }); + expect(check.validation.gates.find((g) => g.name === "instance")?.pass).toBe(true); + }); + + it("a field's textarea and select receive the donated label (the invite-teammates shapes)", () => { + const profile = a0Profile(["card", "field", "field-set", "field-group", "textarea", "select", "badge"]); + const t = emitSurface( + surfaceOf({ component: "field", children: [{ component: "field-label", text: "Personal note" }, { component: "textarea", props: { rows: 3 } }] }), + contract, { profile }); + expect(firstInstance(t.messages, "Textarea").label).toBe("Personal note"); + + const s = emitSurface( + surfaceOf({ + component: "field", + children: [ + { component: "field-label", text: "Workspace role" }, + { component: "select", children: [{ component: "select-content", children: [{ component: "select-item", text: "Editor" }] }] }, + ], + }), + contract, { profile }); + const sel = firstInstance(s.messages, "Select"); + expect(sel.label).toBe("Workspace role"); + expect(sel.options).toEqual([{ label: "Editor" }]); + }); +}); + +describe("A0 — shipped primitives compose: transparency inside a joined panel", () => { + it("a transparent scroll-area inside tabs-content dissolves instead of refusing (the ex.project-workspace-panels shape)", () => { + // Discovered by the A0 re-measurement: the T3 joined-children path fed + // raw children to emitNode, skipping the parent-style rewrite that + // dissolves transparent children — a composition bug between shipped + // primitives, fixed by mirroring the transparent-root host pattern. + const profile = a0Profile(["card", "tabs", "scroll-area", "badge"]); + const { messages } = emitSurface( + surfaceOf({ + component: "tabs", + children: [ + { component: "tabs-list", children: [{ component: "tabs-trigger", id: "t1", text: "Overview" }] }, + { component: "tabs-content", id: "t1", children: [{ component: "scroll-area", children: [{ component: "badge", text: "42 open" }] }] }, + ], + }), + contract, { profile }); + const comps = (messages[1] as { updateComponents: { components: Array> } }).updateComponents.components; + expect(comps.some((c) => c.component === "ScrollArea")).toBe(false); + const tabs = comps.find((c) => c.component === "Tabs")!; + const sections = tabs.sections as Array<{ child: string }>; + expect(sections).toHaveLength(1); + const badge = comps.find((c) => c.component === "Badge")!; + expect(sections[0].child).toBe(badge.id); + const check = transformFromJson(contract, { profile, surface: { messages } }); + expect(check.validation.gates.find((g) => g.name === "instance")?.pass).toBe(true); + }); +}); + +describe("A0 — ledger and determinism", () => { + it("the six authored families derive zero unresolved subs", () => { + const profile = a0Profile(["card", "alert", "avatar", "scroll-area", "field", "field-set", "field-group", "switch", "badge", "button"]); + const out = transformFromJson(contract, { profile }); + const unresolved = out.mapping.coverage.filter( + (c) => c.disposition === "unclassified" && + ["alert.", "avatar.", "scroll-area.", "field.", "field-set.", "field-group."].some((p) => c.id.startsWith(p)), + ); + expect(unresolved).toEqual([]); + }); + + it("emission is deterministic across runs", () => { + const profile = a0Profile(["card", "field", "field-set", "field-group", "switch", "badge"]); + const subtree = fromExample("ex.notification-preferences", "field-set"); + const a = emitSurface(surfaceOf(subtree, "preference-settings"), contract, { profile }); + const b = emitSurface(surfaceOf(subtree, "preference-settings"), contract, { profile }); + expect(JSON.stringify([a.messages, a.warnings, a.fidelity])).toBe(JSON.stringify([b.messages, b.warnings, b.fidelity])); + }); +}); diff --git a/src/eval-fixture.test.ts b/src/eval-fixture.test.ts index 78ac8a0..5b6af62 100644 --- a/src/eval-fixture.test.ts +++ b/src/eval-fixture.test.ts @@ -16,10 +16,12 @@ * dspack#40 correction moved the field wrapper trio right-side-up — * field keeps 5 inner parts; field-set and field-group are compounds * of their own with 1 sub each, so the denominator moved 106 → 104) - * 37 resolved — the transplanted families (card 5, table 7, + * 54 resolved — the transplanted families (card 5, table 7, * alert-dialog 8), T1's form family (6), T2's - * radio-group (1) and select (7), T3's tabs (3) - * 67 unresolved — real, deliberate, and the measure of the A0/T4 work + * radio-group (1) and select (7), T3's tabs (3), and + * the A0 authoring pass (alert 3, avatar 5, + * scroll-area 2, field 5, field-set 1, field-group 1) + * 50 unresolved — real, deliberate, and the measure of the T4+ work * * The v2 contract gate refusing this fixture against the v3 contract is not a * failure; it is the fatal coverage gate doing on the production corpus @@ -56,19 +58,19 @@ describe("the production-v3 evaluation fixture", () => { expect(profile.casualtyComponents).toEqual([]); }); - it("the fatal coverage gate refuses it against the v3 contract: 67 unresolved decisions, each pathed", () => { + it("the fatal coverage gate refuses it against the v3 contract: 50 unresolved decisions, each pathed", () => { try { transformFromJson(contract, { profile: loadProfile(structuredClone(fixtureJson)) }); - expect.unreachable("67 open representation decisions must refuse, not emit"); + expect.unreachable("50 open representation decisions must refuse, not emit"); } catch (e) { expect(e).toBeInstanceOf(ProfileContractError); const issues = (e as ProfileContractError).issues; - expect(issues).toHaveLength(67); + expect(issues).toHaveLength(50); expect(issues.every((i) => i.message.includes("unresolved"))).toBe(true); } }); - it("derived coverage: 104 subs, 37 resolved (transplants + T1-T3), 67 open", () => { + it("derived coverage: 104 subs, 54 resolved (transplants + T1-T3 + A0), 50 open", () => { const profile = loadProfile(structuredClone(fixtureJson)); const byId = new Map(profile.components.map((p: ComponentPlan) => [p.dspackId, p])); let resolved = 0; @@ -90,7 +92,7 @@ describe("the production-v3 evaluation fixture", () => { } } expect(resolved + unresolved).toBe(104); - expect(unresolved).toBe(67); - expect(Object.fromEntries(resolvedByCompound)).toEqual({ card: 5, table: 7, "alert-dialog": 8, form: 6, "radio-group": 1, select: 7, tabs: 3 }); + expect(unresolved).toBe(50); + expect(Object.fromEntries(resolvedByCompound)).toEqual({ card: 5, table: 7, "alert-dialog": 8, form: 6, "radio-group": 1, select: 7, tabs: 3, alert: 3, avatar: 5, "scroll-area": 2, field: 5, "field-set": 1, "field-group": 1 }); }); }); diff --git a/src/targets/a2ui/surface.ts b/src/targets/a2ui/surface.ts index a7e76ff..5264ff8 100644 --- a/src/targets/a2ui/surface.ts +++ b/src/targets/a2ui/surface.ts @@ -864,11 +864,25 @@ class SurfaceEmitter { if ("kind" in jSel && jSel.kind === "joined-children") { // The slot-valued field: the counterpart's children emit as // instances; the record carries the reference. Repetition's - // ratified slot form — not T4 multi-slot routing. - const kids = collectChildren(cp.n); - if (kids.length === 0) continue; - const ids = kids.map((k, ki) => - this.emitNode(k.node, `${cp.nPath}${k.suffix}[${ki}]`, [...treePath, Band.Children, records.length]), + // ratified slot form — not T4 multi-slot routing. The child + // list goes through the SAME rewriting as any parent's + // (mirroring the transparent-root host pattern), so a + // transparent top-level child inside a joined panel + // dissolves instead of refusing as an unroutable instance. + const spliced = this.rewriteChildren(cp.n, emptySurfaceModel(), cp.nPath, treePath); + if (spliced.length === 0) continue; + const ids = spliced.map((k, ki) => + "textVariant" in k + ? this.emitTextPrimitive( + k.text, + `${slug(key)}_${slug(k.textVariant)}`, + cp.nPath, + [...treePath, Band.Children, records.length, ki], + Phase.TextChild, + 0, + k.textVariant, + ) + : this.emitNode(k.node, `${cp.nPath}${k.suffix}[${ki}]`, [...treePath, Band.Children, records.length], k.donations ?? []), ); record[fieldName] = ids.length === 1 ? ids[0] : this.wrapInColumn(ids, `${slug(key)}`, cp.nPath, treePath); diff --git a/src/transform/shadcn-v2-respelling.ts b/src/transform/shadcn-v2-respelling.ts index e4e70fa..5ddf5fd 100644 --- a/src/transform/shadcn-v2-respelling.ts +++ b/src/transform/shadcn-v2-respelling.ts @@ -187,3 +187,162 @@ export const SHADCN_V3_T2_PLANS: Record> = { }, }, }; + +/** + * A0 authoring resolutions (the zero-new-capability milestone): production + * profile judgment for the families the shipped Identity/Routing/Repetition + * model already supports, proven by src/a0.test.ts. Six compound plans plus + * label-slot additions on the five controls that appear as a field's single + * control (the T1 donation destination). No new primitive anywhere. + * + * Grounded in measured usage across the 14 worked examples (contract 3.2.0): + * alert-title 5/5 + alert-description 5/5 carry the content; avatar-fallback + * carries text while avatar-image is an EMPTY node (the contract declares no + * src/alt — the dspack#39 class, recorded on the drop); scroll-area's two + * subs are scrollbar chrome; the field family follows the corrected + * declaration (dspack#40) with field as its own donating boundary. + */ +export const SHADCN_V3_A0_PLANS: Record> = { + alert: { + a2ui: "Alert", + dspackId: "alert", + commons: ["ComponentCommon"], + structural: { + title: { + schema: { type: "string" }, + description: "The one-line heading naming what happened.", + synthNote: "Lifted from the alert-title sub (5/5 worked examples carry it there).", + }, + description: { + schema: { type: "string" }, + description: "Supporting detail under the title.", + synthNote: "Lifted from the alert-description sub.", + }, + }, + propMap: { + variant: { a2ui: "variant", kind: "enum", targetEnum: ["default", "destructive"], default: "default" }, + }, + required: ["title"], + surface: { + routes: [ + { from: ["sub(alert-title).text"], to: "prop:title" }, + { from: ["sub(alert-description).text"], to: "prop:description" }, + ], + subs: { + "alert-action": { + drop: + "an action slot holds component children (a button), and routing a sub's children to a slot is T4's mechanism — dropped WITH RECORD until T4, not approximated (one production instance: ex.import-run-status)", + }, + }, + }, + }, + avatar: { + a2ui: "Avatar", + dspackId: "avatar", + commons: ["ComponentCommon"], + structural: { + fallback: { + schema: { type: "string" }, + description: "Initials shown while no image is available.", + synthNote: "Lifted from the avatar-fallback sub's text.", + }, + }, + propMap: { size: { a2ui: "size", kind: "enum", targetEnum: ["sm", "default", "lg"], default: "default" } }, + required: ["fallback"], + surface: { + routes: [{ from: ["sub(avatar-fallback).text"], to: "prop:fallback" }], + subs: { + "avatar-image": { + drop: "the contract declares no src/alt on avatar-image — an empty node carries nothing to project (contract-gap class, cf. dspack#39)", + }, + "avatar-badge": { drop: "no worked example exercises it; no measured shape to project — revisit with usage evidence" }, + "avatar-group": { drop: "no worked example exercises it; a grouped-avatars shape is repetition to be measured, not guessed" }, + "avatar-group-count": { drop: "no worked example exercises it; belongs to the unmeasured avatar-group shape" }, + }, + }, + }, + "scroll-area": { + a2ui: "ScrollArea", + dspackId: "scroll-area", + commons: ["ComponentCommon"], + structural: {}, + required: [], + surface: { + transparent: true, + subs: { + "scroll-area-scrollbar": { drop: "scrollbar chrome: presentation of the scroll container, not content" }, + "scroll-area-thumb": { drop: "the scrollbar's drag indicator: chrome, not content" }, + }, + }, + }, + field: { + a2ui: "Field", + dspackId: "field", + commons: ["ComponentCommon"], + structural: {}, + required: [], + surface: { + transparent: { donate: [{ from: "sub(field-label).text", to: "prop:label" }] }, + subs: { + "field-content": "transparent", + "field-title": { asText: "h4" }, + "field-description": { asText: "caption" }, + "field-error": { drop: "validation state is runtime data the declarative surface does not carry (mirrors form-message)" }, + }, + }, + }, + "field-set": { + a2ui: "FieldSet", + dspackId: "field-set", + commons: ["ComponentCommon"], + structural: {}, + required: [], + surface: { + transparent: true, + subs: { "field-legend": { asText: "h4" } }, + }, + }, + "field-group": { + a2ui: "FieldGroup", + dspackId: "field-group", + commons: ["ComponentCommon"], + structural: {}, + required: [], + surface: { + transparent: true, + subs: { + "field-separator": { drop: "a visual section boundary; its optional inline text is presentation of the divider, and no worked example carries one" }, + }, + }, + }, +}; + +/** + * A0 label additions: the five controls that appear as a field's (or + * form-item's) single control across the worked examples — input already + * declares `label`; these gain the same structural slot so the T1 donation + * has a declared destination. Merged onto the scaffolded plans (select's T2 + * plan) by the fixture builder; nothing else on those plans changes. + */ +export const SHADCN_V3_A0_LABEL_ADDITIONS: Record = { + switch: { + description: "The label naming what the switch turns on or off.", + synthNote: "Sourced by the T1 field-label/form-label donation; 4/4 worked-example switches sit inside a field.", + }, + checkbox: { + description: "The label naming what checking the box asserts.", + synthNote: "Sourced by the T1 form-label donation (ex.expense-report-form's billable field).", + }, + textarea: { + description: "The label naming the long-text entry.", + synthNote: "Sourced by the T1 field-label/form-label donation, exactly as on input.", + }, + progress: { + description: "The label naming what is progressing.", + synthNote: "Sourced by the T1 field-label donation; 2/2 worked-example progress bars sit inside a field.", + }, + select: { + description: "The label naming the choice being made.", + synthNote: "Sourced by the T1 field-label/form-label donation when the select is a field's control (ex.invite-teammates-dialog).", + }, +}; From 52dbc13028a4c194cc3abb513d12622406718cf7 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 18:20:52 -0400 Subject: [PATCH 3/4] =?UTF-8?q?eval:=20containment=20re-baseline=20?= =?UTF-8?q?=E2=80=94=20the=20terminal=20placement=20class=20is=20gone=20fr?= =?UTF-8?q?om=20the=20frontier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same six intents, model, and maxRepairs as the T1/T3 series; contract 3.2.0, gen 0.3.0 (S2 containment repairable in-loop), pre-A0 profile (the c43a121 fixture) — the authoritative post-containment baseline. The ratified success condition, met: - out-of-context placement fell 19% -> 2% of generated nodes, and ZERO violations survive to any run's final attempt; - preference-settings: attempts 0-1 each carried 3 S2 containment findings; the repair loop received them and attempt 2 placed every sub correctly (the class that was previously a terminal emit refusal); - overlay-task: one containment finding, repaired in ONE round to a fully lint-clean surface (S1/S2/S3 all PASS) that reached the emitter and refused honestly on unmapped 'dialog' — the T4 frontier; - no run anywhere died terminally on placement. Still 0/6 end-to-end; the frontier is now named S3 governance findings the model cannot satisfy within two repairs (missing interactive controls, missing alert-dialog-cancel) plus T4. First-attempt gates: S1 6/6, S2 4/6, S3 1/6. Co-Authored-By: Claude Fable 5 --- eval/containment-rebaseline-matrix.json | 223 ++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 eval/containment-rebaseline-matrix.json diff --git a/eval/containment-rebaseline-matrix.json b/eval/containment-rebaseline-matrix.json new file mode 100644 index 0000000..7943cae --- /dev/null +++ b/eval/containment-rebaseline-matrix.json @@ -0,0 +1,223 @@ +{ + "model": "qwen3.6:35b", + "contract": "3.2.0 @ dspack d50f049", + "gen": "0.3.0 (S2 containment)", + "emit": "0.5.0+rebaseline (feat/containment-rebaseline c43a121)", + "intents": [ + "destructive-action", + "record-collection", + "structured-input", + "preference-settings", + "record-detail", + "overlay-task" + ], + "runs": [ + { + "intent": "destructive-action", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 17, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 17, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 18, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required sub-component 'alert-dialog-cancel' (min 1) not found among descendants (found 0).", + "seconds": 1430 + }, + { + "intent": "record-collection", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 34, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 34, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 34, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required category 'interactive' (min 1) not found among descendants (found 0) — no descendant of this node is a 'interactive' member; member", + "seconds": 938 + }, + { + "intent": "structured-input", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 25, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 25, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 25, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required category 'interactive' (min 1) not found among descendants (found 0) — no descendant of this node is a 'interactive' member; member", + "seconds": 413 + }, + { + "intent": "preference-settings", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:FAIL S3:PASS", + "containment": { + "nodes": 39, + "violations": 3 + }, + "s2ContainmentFindings": 3 + }, + { + "index": 1, + "gates": "S1:PASS S2:FAIL S3:PASS", + "containment": { + "nodes": 30, + "violations": 3 + }, + "s2ContainmentFindings": 3 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 23, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required sub-component 'alert-dialog-cancel' (min 1) not found among descendants (found 0).", + "seconds": 265 + }, + { + "intent": "record-detail", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 28, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 28, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 28, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required category 'interactive' (min 1) not found among descendants (found 0) — no descendant of this node is a 'interactive' member; member", + "seconds": 295 + }, + { + "intent": "overlay-task", + "outcome": "failed-gate", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:FAIL S3:FAIL", + "containment": { + "nodes": 14, + "violations": 1 + }, + "s2ContainmentFindings": 1 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:PASS", + "containment": { + "nodes": 10, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 1, + "firstBlocker": "emit refusal: unknown component 'dialog': not a mapped component of the 'shadcn/ui v3 — coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-compo", + "seconds": 257 + } + ] +} \ No newline at end of file From 7f2bf38fbcf9dbf2b990065189d472b8d796aded Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 18:45:35 -0400 Subject: [PATCH 4/4] =?UTF-8?q?eval:=20post-A0=20Build=20matrix=20?= =?UTF-8?q?=E2=80=94=20the=20series'=20first=20end-to-end=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same six intents/model/config; contract 3.2.0, gen 0.3.0, the A0 profile (23 mapped families). Committed alongside the containment re-baseline as the authoritative post-authoring measurement. - preference-settings PASSED end-to-end — the first 1/6 in the entire post-T1 series, first-attempt clean (S1/S2/S3 PASS, emitted, A-gates green): the radio-group + field + switch vocabulary the T-series and A0 built finally composes in Build. - structured-input: 4 containment findings repaired in ONE round to a fully lint-clean surface that emitted and failed gate A3 — honest instance arbitration (a control without its required label), a class that can only exist now that surfaces get this far. - containment: 0.9% of nodes (4/436), all repaired where they appeared, zero in any final attempt. - No emit refusal anywhere; no placement death anywhere. The remaining frontier is model-vs-S3 content governance (4/6: missing interactive controls, missing form-labels, label without htmlFor) — a generation capability ceiling, not a representation gap. Co-Authored-By: Claude Fable 5 --- eval/a0-build-matrix.json | 205 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 eval/a0-build-matrix.json diff --git a/eval/a0-build-matrix.json b/eval/a0-build-matrix.json new file mode 100644 index 0000000..baa6948 --- /dev/null +++ b/eval/a0-build-matrix.json @@ -0,0 +1,205 @@ +{ + "model": "qwen3.6:35b", + "contract": "3.2.0 @ dspack d50f049", + "gen": "0.3.0 (S2 containment)", + "emit": "0.5.0+A0 (feat/containment-rebaseline e467068)", + "intents": [ + "destructive-action", + "record-collection", + "structured-input", + "preference-settings", + "record-detail", + "overlay-task" + ], + "runs": [ + { + "intent": "destructive-action", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 21, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 12, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 9, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required component 'alert-dialog' does not appear in the surface.", + "seconds": 281 + }, + { + "intent": "record-collection", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 54, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 54, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 54, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: 'form-item' contains no 'form-label' to carry the required content.", + "seconds": 375 + }, + { + "intent": "structured-input", + "outcome": "failed-gate", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:FAIL S3:PASS", + "containment": { + "nodes": 39, + "violations": 4 + }, + "s2ContainmentFindings": 4 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:PASS", + "containment": { + "nodes": 19, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 1, + "firstBlocker": "gate A3 failed", + "seconds": 167 + }, + { + "intent": "preference-settings", + "outcome": "passed", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:PASS", + "containment": { + "nodes": 9, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 0, + "firstBlocker": null, + "seconds": 123 + }, + { + "intent": "record-detail", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 40, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 40, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 40, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required category 'interactive' (min 1) not found among descendants (found 0) — no descendant of this node is a 'interactive' member; member", + "seconds": 244 + }, + { + "intent": "overlay-task", + "outcome": "failed-lint-exhausted", + "attempts": [ + { + "index": 0, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 15, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 1, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 15, + "violations": 0 + }, + "s2ContainmentFindings": 0 + }, + { + "index": 2, + "gates": "S1:PASS S2:PASS S3:FAIL", + "containment": { + "nodes": 15, + "violations": 0 + }, + "s2ContainmentFindings": 0 + } + ], + "repairs": 2, + "firstBlocker": "lint S3 governance: Required prop 'htmlFor' is not present on 'label' itself.", + "seconds": 227 + } + ] +} \ No newline at end of file