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 diff --git a/eval/build-eval-profile.mjs b/eval/build-eval-profile.mjs index f94f722..5ed8795 100644 --- a/eval/build-eval-profile.mjs +++ b/eval/build-eval-profile.mjs @@ -38,10 +38,10 @@ 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 = "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")); @@ -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/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 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..40d64e1 100644 --- a/eval/shadcn-v3.eval.profile.json +++ b/eval/shadcn-v3.eval.profile.json @@ -773,45 +773,63 @@ "commons": [ "ComponentCommon" ], - "structural": { - "children": { - "schema": { - "$ref": "#/$defs/ChildList" + "structural": {}, + "required": [], + "surface": { + "transparent": { + "donate": [ + { + "from": "sub(field-label).text", + "to": "prop:label" + } + ] + }, + "subs": { + "field-content": "transparent", + "field-title": { + "asText": "h4" }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.delete-project-confirmation'." + "field-description": { + "asText": "caption" + }, + "field-error": { + "drop": "validation state is runtime data the declarative surface does not carry (mirrors form-message)" + } } - }, - "required": [ - "children" + } + }, + { + "a2ui": "FieldSet", + "dspackId": "field-set", + "commons": [ + "ComponentCommon" ], - "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": { + "transparent": true, + "subs": { + "field-legend": { + "asText": "h4" + } } - }, + } + }, + { + "a2ui": "FieldGroup", + "dspackId": "field-group", + "commons": [ + "ComponentCommon" + ], + "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" } - ] + } } }, { @@ -879,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", @@ -899,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" + } + } } }, { @@ -991,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" } - ] + } } }, { @@ -1248,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": { @@ -1304,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": { @@ -1336,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": { @@ -1483,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": { @@ -1529,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", @@ -1548,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)" + } + } } }, { @@ -1574,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" ], @@ -1873,14 +1917,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,10 +1998,10 @@ "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.", + "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 c79b3cc..5b6af62 100644 --- a/src/eval-fixture.test.ts +++ b/src/eval-fixture.test.ts @@ -12,11 +12,16 @@ * scaffold of the pinned v3 contract, declares zero casualties, and lets * every open decision show as open: * - * 106 sub-components under 19 mapped compounds - * 37 resolved — the transplanted families (card 5, table 7, + * 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) + * 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) - * 69 unresolved — real, deliberate, and the measure of the T4–T5 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 @@ -53,19 +58,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: 50 unresolved decisions, each pathed", () => { try { transformFromJson(contract, { profile: loadProfile(structuredClone(fixtureJson)) }); - expect.unreachable("69 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(69); + expect(issues).toHaveLength(50); 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, 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; @@ -86,8 +91,8 @@ describe("the production-v3 evaluation fixture", () => { } } } - expect(resolved + unresolved).toBe(106); - expect(unresolved).toBe(69); - expect(Object.fromEntries(resolvedByCompound)).toEqual({ card: 5, table: 7, "alert-dialog": 8, form: 6, "radio-group": 1, select: 7, tabs: 3 }); + expect(resolved + unresolved).toBe(104); + 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).", + }, +};