From 3aa0f8e054ea03104078e2918c756d80df0aca12 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 12:38:34 -0400 Subject: [PATCH 1/2] feat(t2): homogeneous repeated sub-structure -> data array (item-mode collection) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second representation capability, scoped by measurement of the production shapes it targets. Fail-first captured before any code (/tmp/t2-failfirst.txt): radio-group-item and select's seven subs sat in the 80-unresolved eval ledger, the item-mode spelling was refused by the v2 grammar, and ex.notification-preferences refused end-to-end. ITEM-MODE COLLECTS. A collect may declare `item: { field: selector }` instead of the table-mode shape: each descendant matching `of` (document order) becomes ONE RECORD whose fields resolve against that item's own node — self.text, self.props., and the new item-local self.id. Absent sources omit the field and gate A3 arbitrates required record shapes: relocation, never synthesis. Mixing item-mode and table-mode keys refuses; item fields are closed to item-local selectors at the schema AND the parser (sibling reads refuse; the parse layer names T3 as defense-in-depth). THE MEASURED SHAPES, honestly scoped. select-item carries its label as its own text — the pure homogeneous shape, collected completely and A3-green. radio-group-item carries ONLY its id: its label is a sibling `label` component joined by htmlFor->id — a DECLARED KEY JOIN, which is T3's mechanism by the ratified boundary, and position-based pairing is banned. T2 collects what each item itself carries ({value: self.id}); the sibling labels are dropped WITH RECORD (mode-aware messages naming T3 — the v1 table-mode strings stay byte-frozen), pinned as the measured T3 frontier. Also recorded, not papered over: neither item declares a `value` prop in the contract (the examples encode value into ids by convention) — a contract-modeling gap for the taxonomy trail alongside dspack#38. Eval overlay: the full receiving plans (a collecting plan IS a catalog component, unlike T1's transparent plans) land as committed evidence. Production-v3 derived coverage: 80 -> 72 unresolved (radio-group-item collected; select-item collected, four select subs dropped with reasons, content/group absorbed). The 72 stay deliberately open as the T3-T5 measure. 267 tests green (+8 T2). All byte gates untouched. --- docs/PROFILES.md | 14 +- eval/build-eval-profile.mjs | 8 +- eval/shadcn-v3.eval.profile.json | 185 +++++++++------------ src/eval-fixture.test.ts | 26 +-- src/t2.test.ts | 222 ++++++++++++++++++++++++++ src/targets/a2ui/surface.ts | 60 ++++++- src/transform/model.ts | 15 ++ src/transform/parse-v2.ts | 75 +++++++-- src/transform/profile-schema-v2.ts | 129 +++++++++------ src/transform/profile.v2.schema.json | 129 +++++++++------ src/transform/shadcn-v2-respelling.ts | 54 +++++++ 11 files changed, 692 insertions(+), 225 deletions(-) create mode 100644 src/t2.test.ts diff --git a/docs/PROFILES.md b/docs/PROFILES.md index e08e2a6..32a1c11 100644 --- a/docs/PROFILES.md +++ b/docs/PROFILES.md @@ -199,7 +199,19 @@ write phase, and two same-phase writers on one destination refuse; layered writers in distinct phases are legal (a passthrough prop beats a harvested one, deterministically). -**Repetition** collects repeated sub-structures into an array-valued prop: +**Repetition** collects repeated sub-structures into an array-valued prop. +T2's **item-mode** makes each matching descendant one record whose fields +resolve on the item's own node (`self.text`, `self.props.`, `self.id`) +— no joins, no sibling reads (a label paired by `htmlFor` is T3's declared +join): + +```jsonc +"collects": [ + { "of": ["select-item"], "into": "prop:options", "item": { "label": "self.text" } } +] +``` + +Table-mode expresses the three-level text shape: ```jsonc "collects": [ diff --git a/eval/build-eval-profile.mjs b/eval/build-eval-profile.mjs index c2fabf5..55fe655 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 } = await import(join(root, "dist/transform/shadcn-v2-respelling.js")); +const { SHADCN_V2_SURFACES, SHADCN_V3_T1_SURFACES, SHADCN_V3_T2_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)"; @@ -77,6 +77,10 @@ export function buildEvalProfile() { const { propMap: _p, ...rest } = plan; return { ...rest, structural: {}, required: [], surface: structuredClone(t1) }; } + // T2 resolutions (proven in src/t2.test.ts): a collecting plan IS a + // catalog component — the whole receiving plan is committed evidence. + const t2 = SHADCN_V3_T2_PLANS[plan.dspackId]; + if (t2) return structuredClone(t2); return plan; }); @@ -87,7 +91,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 proven in src/t1.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); 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/eval/shadcn-v3.eval.profile.json b/eval/shadcn-v3.eval.profile.json index c844d04..3e5cc46 100644 --- a/eval/shadcn-v3.eval.profile.json +++ b/eval/shadcn-v3.eval.profile.json @@ -1394,74 +1394,52 @@ "ComponentCommon" ], "structural": { - "children": { + "options": { "schema": { - "$ref": "#/$defs/ChildList" + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "value" + ], + "additionalProperties": false + } }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.expense-report-form'." + "description": "The selectable options, one record per item.", + "synthNote": "A2UI models repeated options as data on the group; dspack models them as repeated sub-components (T2 item-mode collection)." } }, - "required": [ - "children" - ], "propMap": { - "value": { - "a2ui": "value", - "kind": "string", - "description": "Controlled selected value. Must match the value of exactly one RadioGroupItem in the group." - }, - "defaultValue": { - "a2ui": "defaultValue", - "kind": "string", - "description": "Initially selected value for an uncontrolled group. Omit it when there is no safe default — an unselected group forces a deliberate choice." - }, - "orientation": { - "a2ui": "orientation", - "kind": "enum", - "targetEnum": [ - "vertical", - "horizontal" - ], - "description": "Sets aria-orientation and constrains arrow-key navigation to one axis. Unset by default, which lets both axes navigate; set it to match the visual arrangement whenever the layout is committed to a direction." - }, - "disabled": { - "a2ui": "disabled", - "kind": "boolean", - "description": "Disables every item in the group. To disable one option, set disabled on that RadioGroupItem instead." - }, - "required": { - "a2ui": "required", - "kind": "boolean", - "description": "Whether a selection is required before the form can be submitted. Sets aria-required on the group." - }, "name": { "a2ui": "name", - "kind": "string", - "description": "Form field name applied to the items' hidden native inputs, under which the selected value is submitted." - }, - "loop": { - "a2ui": "loop", - "kind": "boolean", - "description": "Whether arrow-key navigation wraps from the last item back to the first." + "kind": "string" }, - "dir": { - "a2ui": "dir", - "kind": "enum", - "targetEnum": [ - "ltr", - "rtl" - ], - "description": "Reading direction for arrow-key navigation. Inherited from an ambient direction provider when omitted." + "defaultValue": { + "a2ui": "defaultValue", + "kind": "string" } }, + "required": [ + "options" + ], "surface": { - "routes": [ + "collects": [ { - "from": [ - "children" + "of": [ + "radio-group-item" ], - "to": "slots:children" + "into": "prop:options", + "item": { + "value": "self.id" + } } ] } @@ -1473,72 +1451,61 @@ "ComponentCommon" ], "structural": { - "children": { + "options": { "schema": { - "$ref": "#/$defs/ChildList" + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": [ + "label" + ], + "additionalProperties": false + } }, - "description": "Child component IDs, in order.", - "synthNote": "Scaffolded: observed with child nodes in worked example 'ex.workspace-members-directory'." + "description": "The selectable options.", + "synthNote": "A2UI models repeated options as data on the control (T2 item-mode collection)." } }, - "required": [ - "children" - ], "propMap": { - "value": { - "a2ui": "value", - "kind": "string", - "description": "Controlled selected value; must match the value of one SelectItem. The empty string means nothing is selected and shows the SelectValue placeholder." - }, "defaultValue": { "a2ui": "defaultValue", - "kind": "string", - "description": "Initially selected value for an uncontrolled Select. Omit it to start on the placeholder." - }, - "open": { - "a2ui": "open", - "kind": "boolean", - "description": "Controlled open state of the listbox. Only needed when something outside the Select has to open or close it." - }, - "defaultOpen": { - "a2ui": "defaultOpen", - "kind": "boolean", - "description": "Whether the listbox starts open for an uncontrolled Select." - }, - "disabled": { - "a2ui": "disabled", - "kind": "boolean", - "description": "Whether the whole control is non-interactive. To disable one option, set disabled on that SelectItem instead." - }, - "required": { - "a2ui": "required", - "kind": "boolean", - "description": "Whether a value must be chosen before the form can be submitted. Sets aria-required on the trigger." - }, - "name": { - "a2ui": "name", - "kind": "string", - "description": "Form field name applied to the hidden native select, under which the value is submitted." - }, - "dir": { - "a2ui": "dir", - "kind": "enum", - "targetEnum": [ - "ltr", - "rtl" - ], - "description": "Reading direction. Inherited from an ambient direction provider when omitted." + "kind": "string" } }, + "required": [ + "options" + ], "surface": { - "routes": [ + "collects": [ { - "from": [ - "children" + "of": [ + "select-item" ], - "to": "slots:children" + "into": "prop:options", + "item": { + "label": "self.text" + } } - ] + ], + "subs": { + "select-trigger": { + "drop": "the trigger renders from the bound value, not authored content" + }, + "select-value": { + "drop": "the visible value is runtime state derived from the selection" + }, + "select-label": { + "drop": "group headings are not carried by the flat options shape" + }, + "select-separator": { + "drop": "visual grouping chrome with no data meaning" + } + } } }, { @@ -1974,7 +1941,7 @@ "commit": "48643ff (aestheticfunction/dspack, merged as b573637 / PR #35)", "sha256": "ea87346f85965937cb2b18e3998f19da7ba3cee41b41572638d657f20b3f5565" }, - "derivation": "scaffoldProfile(v3) + the six shipped plans transplanted with their byte-proven v2 re-spelling + the T1 transparent-identity resolutions proven in src/t1.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); 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 91dce0f..74fcc2a 100644 --- a/src/eval-fixture.test.ts +++ b/src/eval-fixture.test.ts @@ -6,14 +6,16 @@ * compounds, 1 unresolved"): that profile's nine extra compound plans carried * judgment that did not survive the reboot. This fixture transplants only * COMMITTED mapping evidence — the six shipped plans in their byte-proven v2 - * re-spelling, and now the T1 transparent-identity resolutions proven in - * src/t1.test.ts — onto a mechanical scaffold of the pinned v3 contract, - * declares zero casualties, and lets every open decision show as open: + * re-spelling, the T1 transparent-identity resolutions (src/t1.test.ts), and + * the T2 item-mode collection plans (src/t2.test.ts) — onto a mechanical + * scaffold of the pinned v3 contract, declares zero casualties, and lets + * every open decision show as open: * * 106 sub-components under 19 mapped compounds - * 26 resolved — the transplanted families (card 5, table 7, - * alert-dialog 8) plus T1's form family (6) - * 80 unresolved — real, deliberate, and the measure of the T2–T5 work + * 34 resolved — the transplanted families (card 5, table 7, + * alert-dialog 8), T1's form family (6), T2's + * radio-group (1) and select (7) + * 72 unresolved — real, deliberate, and the measure of the T3–T5 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 @@ -50,19 +52,19 @@ describe("the production-v3 evaluation fixture", () => { expect(profile.casualtyComponents).toEqual([]); }); - it("the fatal coverage gate refuses it against the v3 contract: 80 unresolved decisions, each pathed", () => { + it("the fatal coverage gate refuses it against the v3 contract: 72 unresolved decisions, each pathed", () => { try { transformFromJson(contract, { profile: loadProfile(structuredClone(fixtureJson)) }); - expect.unreachable("80 open representation decisions must refuse, not emit"); + expect.unreachable("72 open representation decisions must refuse, not emit"); } catch (e) { expect(e).toBeInstanceOf(ProfileContractError); const issues = (e as ProfileContractError).issues; - expect(issues).toHaveLength(80); + expect(issues).toHaveLength(72); expect(issues.every((i) => i.message.includes("unresolved"))).toBe(true); } }); - it("derived coverage: 106 subs, 26 resolved (transplants + T1 form), 80 open", () => { + it("derived coverage: 106 subs, 34 resolved (transplants + T1 + T2), 72 open", () => { const profile = loadProfile(structuredClone(fixtureJson)); const byId = new Map(profile.components.map((p: ComponentPlan) => [p.dspackId, p])); let resolved = 0; @@ -84,7 +86,7 @@ describe("the production-v3 evaluation fixture", () => { } } expect(resolved + unresolved).toBe(106); - expect(unresolved).toBe(80); - expect(Object.fromEntries(resolvedByCompound)).toEqual({ card: 5, table: 7, "alert-dialog": 8, form: 6 }); + expect(unresolved).toBe(72); + expect(Object.fromEntries(resolvedByCompound)).toEqual({ card: 5, table: 7, "alert-dialog": 8, form: 6, "radio-group": 1, select: 7 }); }); }); diff --git a/src/t2.test.ts b/src/t2.test.ts new file mode 100644 index 0000000..e350921 --- /dev/null +++ b/src/t2.test.ts @@ -0,0 +1,222 @@ +/** + * T2: homogeneous repeated sub-structure → data array (item-mode collection). + * + * Fail-first (captured before any code, /tmp/t2-failfirst.txt): the eval + * ledger carries radio-group-item and select's seven subs among its 80 + * unresolved; the item-mode spelling was refused by the v2 grammar; and + * ex.notification-preferences refused end-to-end. + * + * The measured shapes that scoped this milestone: + * - `select-item` carries its label as its OWN text — the pure homogeneous + * shape, collected completely by T2; + * - `radio-group-item` carries ONLY its id; its label is a SIBLING `label` + * joined by htmlFor→id — a DECLARED KEY JOIN, which is T3's mechanism. + * T2 collects what each item itself carries ({value: self.id}) and the + * sibling labels are dropped WITH RECORD — the measured T3 frontier, not + * a T2 defect. Position-based pairing is banned by the ratified model. + * - Neither item declares a `value` prop in the contract (the example + * encodes value into ids by convention) — a contract-modeling gap noted + * for the taxonomy trail, not papered over here. + */ +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, ProfileLoadError } from "./transform/profile-load.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")); + +const RG_T2_SURFACE = { + collects: [{ of: ["radio-group-item"], into: "prop:options", item: { value: "self.id" } }], +}; +const SELECT_T2_SURFACE = { + collects: [{ of: ["select-item"], into: "prop:options", item: { label: "self.text" } }], + subs: { + "select-trigger": { drop: "the trigger renders from the bound value, not authored content" }, + "select-value": { drop: "the visible value is runtime state derived from the selection" }, + "select-label": { drop: "group headings are not carried by the flat options shape" }, + "select-separator": { drop: "visual grouping chrome with no data meaning" }, + }, +}; + +/** Hermetic T2 profile: radio-group + select plans over the eval fixture base. */ +const t2Profile = (mutate?: (doc: Record) => void) => { + const base = structuredClone(fixture) as Record & { components: Array> }; + const pick = (id: string) => structuredClone(base.components.find((c) => c.dspackId === id)!); + const radioGroup = { + a2ui: "RadioGroup", + dspackId: "radio-group", + commons: ["ComponentCommon"], + structural: { + options: { + schema: { type: "array", items: { type: "object", properties: { value: { type: "string" }, label: { type: "string" } }, required: ["value"], additionalProperties: false } }, + description: "The selectable options, one record per item.", + synthNote: "A2UI models repeated options as data on the group; dspack models them as repeated sub-components.", + }, + }, + propMap: { name: { a2ui: "name", kind: "string" }, defaultValue: { a2ui: "defaultValue", kind: "string" } }, + required: ["options"], + surface: structuredClone(RG_T2_SURFACE), + }; + const select = { + a2ui: "Select", + dspackId: "select", + commons: ["ComponentCommon"], + structural: { + options: { + schema: { type: "array", items: { type: "object", properties: { label: { type: "string" } }, required: ["label"], additionalProperties: false } }, + description: "The selectable options.", + synthNote: "A2UI models repeated options as data on the control.", + }, + }, + propMap: { defaultValue: { a2ui: "defaultValue", kind: "string" } }, + required: ["options"], + surface: structuredClone(SELECT_T2_SURFACE), + }; + const doc: Record & { components: Array> } = { + ...base, + components: [pick("card"), radioGroup, select], + }; + delete (doc as Record)["x-scaffold"]; + mutate?.(doc); + return loadProfile(doc); +}; + +const surfaceOf = (root: unknown): DspackSurface => + ({ dspackSurface: "0.1", system: contract.name as string, intent: "preference-settings", root }) as DspackSurface; + +/** The production notification-preferences radio-group, verbatim shape. */ +const RG_SURFACE = { + component: "radio-group", + props: { name: "digest", defaultValue: "monday" }, + children: [ + { component: "radio-group-item", id: "digest-monday" }, + { component: "label", props: { htmlFor: "digest-monday" }, text: "Monday morning" }, + { component: "radio-group-item", id: "digest-friday" }, + { component: "label", props: { htmlFor: "digest-friday" }, text: "Friday afternoon" }, + ], +}; + +describe("T2 grammar and gates", () => { + it("the item-mode spelling loads (the fail-first refusal, inverted)", () => { + expect(t2Profile().language).toBe("v2"); + }); + + it("mixing item-mode and table-mode keys refuses", () => { + expect(() => + t2Profile((doc) => { + const rg = (doc.components as Array>).find((c) => c.dspackId === "radio-group")!; + rg.surface = { collects: [{ of: ["radio-group-item"], into: "prop:options", item: { value: "self.id" }, shape: "flat", row: ["x"], cells: ["y"] }] }; + }), + ).toThrowError(ProfileLoadError); + }); + + it("item fields are item-local: a sub() read refuses at the item field's path", () => { + // The schema's closed item-selector pattern fires first (self.text | + // self.id | self.props.); the parse layer carries the same rule + // with a T3-naming message as defense-in-depth behind it. + try { + t2Profile((doc) => { + const rg = (doc.components as Array>).find((c) => c.dspackId === "radio-group")!; + rg.surface = { collects: [{ of: ["radio-group-item"], into: "prop:options", item: { label: "sub(label).text" } }] }; + }); + expect.unreachable("a join read must refuse"); + } catch (e) { + const issues = (e as ProfileLoadError).issues; + expect(issues.some((i) => i.path.includes("/item/label") || i.path.includes("collects"))).toBe(true); + expect(issues.map((i) => i.message).join("\n")).toContain("pattern"); + } + }); +}); + +describe("T2 collects the measured shapes", () => { + it("select: the pure homogeneous shape collects completely", () => { + const s = surfaceOf({ + component: "select", + props: { defaultValue: "all" }, + children: [ + { + component: "select-content", + children: [ + { component: "select-item", text: "All roles" }, + { component: "select-item", text: "Admins" }, + { component: "select-item", text: "Editors" }, + ], + }, + ], + }); + const { messages } = emitSurface(s, contract, { profile: t2Profile() }); + const [sel] = (messages[1] as { updateComponents: { components: Array> } }) + .updateComponents.components; + expect(sel.component).toBe("Select"); + expect(sel.options).toEqual([{ label: "All roles" }, { label: "Admins" }, { label: "Editors" }]); + const check = transformFromJson(contract, { profile: t2Profile(), surface: { messages } }); + expect(check.validation.gates.find((g) => g.name === "instance")?.pass).toBe(true); + }); + + it("radio-group: items collect what they carry; sibling labels are the recorded T3 frontier", () => { + const { messages, warnings, fidelity } = emitSurface(surfaceOf(RG_SURFACE), contract, { profile: t2Profile() }); + const [rg] = (messages[1] as { updateComponents: { components: Array> } }) + .updateComponents.components; + expect(rg.component).toBe("RadioGroup"); + expect(rg.name).toBe("digest"); + expect(rg.options).toEqual([{ value: "digest-monday" }, { value: "digest-friday" }]); + + // The htmlFor-joined sibling labels are dropped WITH RECORD, naming T3. + const labelDrops = warnings.filter((w) => w.code === "surface-sub-dropped" && w.message.includes("'label'")); + expect(labelDrops).toHaveLength(2); + expect(labelDrops[0].message).toContain("T3"); + expect(fidelity.filter((f) => f.kind === "dropped" && f.source.includes("label"))).toHaveLength(2); + + // Each collected record is ledgered with its source item. + const collected = fidelity.filter((f) => f.kind === "moved" && f.note?.includes("item record")); + expect(collected).toHaveLength(2); + expect(collected[0].source).toContain("radio-group-item"); + + const check = transformFromJson(contract, { profile: t2Profile(), surface: { messages } }); + expect(check.validation.gates.find((g) => g.name === "instance")?.pass).toBe(true); + }); + + it("emission is deterministic across runs", () => { + const a = emitSurface(surfaceOf(RG_SURFACE), contract, { profile: t2Profile() }); + const b = emitSurface(surfaceOf(RG_SURFACE), contract, { profile: t2Profile() }); + expect(JSON.stringify([a.messages, a.warnings, a.fidelity])).toBe(JSON.stringify([b.messages, b.warnings, b.fidelity])); + }); + + it("an item missing its field omits it, and A3 arbitrates — never synthesis", () => { + const s = surfaceOf({ + component: "radio-group", + children: [{ component: "radio-group-item" }], // no id at all + }); + const { messages } = emitSurface(s, contract, { profile: t2Profile() }); + const [rg] = (messages[1] as { updateComponents: { components: Array> } }) + .updateComponents.components; + expect(rg.options).toEqual([{}]); + // The record schema requires `value`, so A3 refuses the emission. + const check = transformFromJson(contract, { profile: t2Profile(), surface: { messages } }); + expect(check.validation.gates.find((g) => g.name === "instance")?.pass).toBe(false); + }); + + it("derived coverage resolves both families through the collects", () => { + const out = (() => { + try { + return transformFromJson(contract, { profile: t2Profile() }); + } catch { + return undefined; + } + })(); + // The hermetic profile passes the contract gate (card + both T2 plans). + expect(out).toBeDefined(); + const unresolved = out!.mapping.coverage.filter( + (c) => c.disposition === "unclassified" && (c.id.startsWith("radio-group.") || c.id.startsWith("select.")), + ); + expect(unresolved).toEqual([]); + expect(out!.mapping.coverage.find((c) => c.id === "radio-group.radio-group-item")?.detail).toContain("collected"); + }); +}); diff --git a/src/targets/a2ui/surface.ts b/src/targets/a2ui/surface.ts index 5b94b38..7911b87 100644 --- a/src/targets/a2ui/surface.ts +++ b/src/targets/a2ui/surface.ts @@ -753,6 +753,56 @@ class SurfaceEmitter { ): void { void model; if (collect.as.kind === "inline") return; + + // T2 item-mode: each descendant matching `of` (document order) becomes + // one record whose fields resolve against THAT item's own node. Absent + // sources omit the field — gate A3 arbitrates required record shapes, + // never synthesis. Sibling content the collect does not claim follows + // the ordinary consumed-subtree accounting (closeCollects). + if (collect.fields) { + const records: Json[] = []; + const visitItems = (n: SurfaceNode, nPath: string): void => { + if (collect.of.includes(n.component)) { + const record: Json = {}; + for (const [fieldName, sel] of Object.entries(collect.fields!)) { + const value = + sel.kind === "self-text" ? n.text : sel.kind === "self-id" ? n.id : sel.kind === "self-prop" ? (n.props?.[sel.prop] as Json[keyof Json] | undefined) : undefined; + if (value !== undefined) record[fieldName] = value as Json[keyof Json]; + } + records.push(record); + this.diagnostics.pushFidelity( + { + source: `${nPath} (${n.component})`, + destination: collect.as.kind === "inline" ? "(inline)" : collect.as.name, + origin: collect.origin, + kind: "moved", + class: "maps-cleanly", + note: `item record { ${Object.keys(record).join(", ")} } collected`, + }, + treePath, + Band.BeforeChildren, + Phase.TableBody, + rule, + ); + return; // an item's subtree is the item's; nested items do not re-match + } + collectChildren(n).forEach((c, i) => visitItems(c.node, `${nPath}${c.suffix}[${i}]`)); + }; + collectChildren(node).forEach((c, i) => visitItems(c.node, `${path}${c.suffix}[${i}]`)); + if (records.length > 0) { + const landed = this.write(instance, collect.as.name, records as Json[keyof Json]); + if (!landed) { + this.diagnostics.pushFidelity( + { source: `${path} (${collect.of.join("|")} items)`, destination: "(discarded)", origin: collect.origin, kind: "dropped", class: "lossy", note: `'${collect.as.name}' was already written by an earlier phase; the collected records did not land` }, + treePath, + Band.BeforeChildren, + Phase.TableBody, + rule, + ); + } + } + return; + } const out: Json[] = []; const flat: string[] = []; const scalarField = Object.values(collect.item ?? {}).find((f) => isCollect(f) && f.scalar) as Collect | undefined; @@ -836,12 +886,15 @@ class SurfaceEmitter { if (claimedByCollect.has(c) || this.claimedByRoute(model, c)) continue; const childPath = `${path}${child.suffix}`; const reason = model.drops[c]; + const itemMode = model.collects.some((col) => col.fields); this.diagnostics.push( { code: "surface-sub-dropped", message: reason ? `${childPath}: '${c}' dropped: ${reason}.` - : `${childPath}: '${c}' has no slot in the synthesized table shape; dropped.`, + : itemMode + ? `${childPath}: '${c}' is not a collected item and has no disposition; dropped (sibling pairing is T3's declared join).` + : `${childPath}: '${c}' has no slot in the synthesized table shape; dropped.`, }, treePath, Band.BeforeChildren, @@ -862,10 +915,13 @@ class SurfaceEmitter { ); } + const itemsOnly = model.collects.every((col) => col.fields); this.diagnostics.push( { code: "surface-composition-flattened", - message: `${path}: compound '${node.component}' subtree consumed into the synthesized table shape (documented casualty; cell content beyond text is not carried).`, + message: itemsOnly + ? `${path}: compound '${node.component}' subtree consumed into collected item records (uncollected content is not carried).` + : `${path}: compound '${node.component}' subtree consumed into the synthesized table shape (documented casualty; cell content beyond text is not carried).`, }, treePath, Band.BeforeChildren, diff --git a/src/transform/model.ts b/src/transform/model.ts index 1d3174c..a86ff92 100644 --- a/src/transform/model.ts +++ b/src/transform/model.ts @@ -108,6 +108,12 @@ export type Selector = | { kind: "subtree-text"; subs: string[] } /** This node's ordered children, emitted as instances. */ | { kind: "children" } + /** + * T2: the node's own surface `id`. Item-local — only valid inside a + * collect's item fields, where each repeated item contributes the one + * datum it carries (the production radio-group-item carries nothing else). + */ + | { kind: "self-id" } /** No source: a declarative A2UI action synthesized from a deterministic slug. */ | { kind: "synthesized-action" }; @@ -182,6 +188,13 @@ export interface Route { export interface Collect { /** When this collect writes, relative to every other route and collect. */ order: WriteOrder; + /** + * T2 item-mode: each descendant matching `of` becomes ONE RECORD whose + * fields resolve against that item's own node (self.text, self.props.X, + * self.id — never joins, never siblings). Mutually exclusive with the + * table-mode `item`/`scalar` nesting. + */ + fields?: Record; /** * True when every repetition's cells concatenate into ONE flat list (v1's * header semantics: two header rows append into a single column list); @@ -266,6 +279,8 @@ export function describeSelector(s: Selector): string { return `sub(${s.subs.join("|")}).subtreeText`; case "children": return "children"; + case "self-id": + return "self.id"; case "synthesized-action": return "synthesized.action"; } diff --git a/src/transform/parse-v2.ts b/src/transform/parse-v2.ts index 2464b7e..abe55f1 100644 --- a/src/transform/parse-v2.ts +++ b/src/transform/parse-v2.ts @@ -65,18 +65,26 @@ export interface SurfaceV2Route { } export interface SurfaceV2Collect { - /** Sections: direct children that open the collection. */ + /** Sections (table-mode) or the repeated items themselves (item-mode). */ of: string[]; /** Array-valued destination (`prop:`). */ into: string; - /** `flat`: all rows concatenate into one list. `records`: one record per row. */ - shape: "flat" | "records"; - /** Record field carrying each row's cells; required when shape is `records`. */ + /** table-mode: `flat` concatenates all rows into one list; `records` keeps one record per row. */ + shape?: "flat" | "records"; + /** table-mode: record field carrying each row's cells; required when shape is `records`. */ field?: string; - /** Repetitions inside a section. */ - row: string[]; - /** Cells inside a repetition, flattened to their subtree text. */ - cells: string[]; + /** table-mode: repetitions inside a section. */ + row?: string[]; + /** table-mode: cells inside a repetition, flattened to their subtree text. */ + cells?: string[]; + /** + * T2 item-mode: each matching descendant becomes one record; every field + * resolves against THAT item's own node. Selectors: `self.text`, + * `self.props.`, `self.id`. No joins, no sibling reads — the + * htmlFor-paired sibling label the production radio-group carries is T3's + * declared join, measured and out of scope here. + */ + item?: Record; } export interface SurfaceV2Donate { @@ -112,7 +120,7 @@ export interface SurfaceV2 { const ID = "[a-z][a-z0-9-]*"; const PROP = "[A-Za-z][A-Za-z0-9]*"; const SELECTOR = new RegExp( - `^(self\\.text|self\\.props\\.(${PROP})|sub\\((${ID}(?:\\|${ID})*)\\)\\.(text|label|firstText|subtreeText)|children|synthesized\\.action)$`, + `^(self\\.text|self\\.id|self\\.props\\.(${PROP})|sub\\((${ID}(?:\\|${ID})*)\\)\\.(text|label|firstText|subtreeText)|children|synthesized\\.action)$`, ); const DESTINATION = new RegExp(`^(prop|textChild|slot|slots|action):(${PROP})$`); @@ -120,6 +128,7 @@ export function parseSelector(raw: string): Selector | null { const m = SELECTOR.exec(raw); if (!m) return null; if (m[1] === "self.text") return { kind: "self-text" }; + if (m[1] === "self.id") return { kind: "self-id" }; if (m[1] === "children") return { kind: "children" }; if (m[1] === "synthesized.action") return { kind: "synthesized-action" }; if (m[2]) return { kind: "self-prop", prop: m[2] }; @@ -171,6 +180,8 @@ function orderOfSelector(s: Selector, to: Destination): WriteOrder { return WriteOrder.CollectLead; case "self-text": return to.kind === "text-child" ? WriteOrder.SelfTextChild : WriteOrder.SelfText; + case "self-id": + return WriteOrder.SelfText; // unreachable in routes: `compatible` refuses it case "synthesized-action": return WriteOrder.Action; case "children": @@ -187,6 +198,8 @@ function compatible(s: Selector, to: Destination): boolean { return to.kind === "action"; case "self-text": return to.kind === "prop" || to.kind === "text-child"; + case "self-id": + return false; // item-field-only: a route may not read self.id default: return to.kind === "prop"; } @@ -385,6 +398,50 @@ export function parseSurfaceV2(surface: SurfaceV2, plan: ComponentPlan, path: st if (!declared.has(into.name)) { issues.push({ path: `${at}/into`, message: `destination '${into.name}' is not declared by this plan` }); } + + // T2 item-mode and table-mode are distinct shapes; mixing them is a + // contradiction, not a preference. + const itemMode = c.item !== undefined; + const tableKeys = [c.shape, c.field, c.row, c.cells].some((v) => v !== undefined); + if (itemMode && tableKeys) { + issues.push({ path: at, message: "a collect is either item-mode (item) or table-mode (shape/row/cells/field), never both" }); + continue; + } + if (itemMode) { + const fields: Record = {}; + let ok = true; + for (const [fieldName, raw] of Object.entries(c.item!)) { + const sel = parseSelector(raw); + const itemLocal = sel && (sel.kind === "self-text" || sel.kind === "self-prop" || sel.kind === "self-id"); + if (!itemLocal) { + issues.push({ + path: `${at}/item/${fieldName}`, + message: `'${raw}' is not an item field source (item fields resolve on the item's own node: self.text, self.props., self.id — sibling reads and joins are T3)`, + }); + ok = false; + continue; + } + fields[fieldName] = sel; + } + if (!ok || Object.keys(fields).length === 0) { + if (Object.keys(c.item!).length === 0) issues.push({ path: `${at}/item`, message: "an item-mode collect names at least one field" }); + continue; + } + claim(into.name, WriteOrder.Collect, `collects/${i}`, `${at}/into`); + model.collects.push({ + order: WriteOrder.Collect, + flatten: false, + of: c.of, + as: into, + fields, + origin: `v2:collects/${i}`, + }); + continue; + } + if (c.shape === undefined || c.row === undefined || c.cells === undefined) { + issues.push({ path: at, message: "a table-mode collect declares shape, row, and cells (or use item-mode with `item`)" }); + continue; + } if (c.shape === "records" && !c.field) { issues.push({ path: `${at}/field`, message: "a records collect names the field its rows' cells land in" }); continue; diff --git a/src/transform/profile-schema-v2.ts b/src/transform/profile-schema-v2.ts index 70688bf..c092dc4 100644 --- a/src/transform/profile-schema-v2.ts +++ b/src/transform/profile-schema-v2.ts @@ -370,55 +370,94 @@ export const profileSchemaV2: Record = { "collects": { "type": "array", "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "of", - "into", - "shape", - "row", - "cells" - ], - "properties": { - "of": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" - } - }, - "into": { - "type": "string", - "pattern": "^prop:[A-Za-z][A-Za-z0-9]*$" - }, - "shape": { - "enum": [ - "flat", - "records" - ] - }, - "field": { - "type": "string", - "pattern": "^[A-Za-z][A-Za-z0-9]*$" - }, - "row": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "of", + "into", + "item" + ], + "properties": { + "of": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + }, + "into": { + "type": "string", + "pattern": "^prop:[A-Za-z][A-Za-z0-9]*$" + }, + "item": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[A-Za-z][A-Za-z0-9]*$" + }, + "additionalProperties": { + "type": "string", + "pattern": "^(self\\.text|self\\.id|self\\.props\\.[A-Za-z][A-Za-z0-9]*)$" + }, + "description": "T2 item-mode: one record per matching descendant; fields resolve on the item's own node (self.text, self.props., self.id). Joins and sibling reads are T3." + } } }, - "cells": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" + { + "type": "object", + "additionalProperties": false, + "required": [ + "of", + "into", + "shape", + "row", + "cells" + ], + "properties": { + "of": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + }, + "into": { + "type": "string", + "pattern": "^prop:[A-Za-z][A-Za-z0-9]*$" + }, + "shape": { + "enum": [ + "flat", + "records" + ] + }, + "field": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9]*$" + }, + "row": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + }, + "cells": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + } } } - } + ] } }, "subs": { diff --git a/src/transform/profile.v2.schema.json b/src/transform/profile.v2.schema.json index f6b0a65..45fadaa 100644 --- a/src/transform/profile.v2.schema.json +++ b/src/transform/profile.v2.schema.json @@ -362,55 +362,94 @@ "collects": { "type": "array", "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "of", - "into", - "shape", - "row", - "cells" - ], - "properties": { - "of": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" - } - }, - "into": { - "type": "string", - "pattern": "^prop:[A-Za-z][A-Za-z0-9]*$" - }, - "shape": { - "enum": [ - "flat", - "records" - ] - }, - "field": { - "type": "string", - "pattern": "^[A-Za-z][A-Za-z0-9]*$" - }, - "row": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "of", + "into", + "item" + ], + "properties": { + "of": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + }, + "into": { + "type": "string", + "pattern": "^prop:[A-Za-z][A-Za-z0-9]*$" + }, + "item": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[A-Za-z][A-Za-z0-9]*$" + }, + "additionalProperties": { + "type": "string", + "pattern": "^(self\\.text|self\\.id|self\\.props\\.[A-Za-z][A-Za-z0-9]*)$" + }, + "description": "T2 item-mode: one record per matching descendant; fields resolve on the item's own node (self.text, self.props., self.id). Joins and sibling reads are T3." + } } }, - "cells": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" + { + "type": "object", + "additionalProperties": false, + "required": [ + "of", + "into", + "shape", + "row", + "cells" + ], + "properties": { + "of": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + }, + "into": { + "type": "string", + "pattern": "^prop:[A-Za-z][A-Za-z0-9]*$" + }, + "shape": { + "enum": [ + "flat", + "records" + ] + }, + "field": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9]*$" + }, + "row": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + }, + "cells": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + } + } } } - } + ] } }, "subs": { diff --git a/src/transform/shadcn-v2-respelling.ts b/src/transform/shadcn-v2-respelling.ts index 83e30f2..2acb6d2 100644 --- a/src/transform/shadcn-v2-respelling.ts +++ b/src/transform/shadcn-v2-respelling.ts @@ -79,3 +79,57 @@ export const SHADCN_V3_T1_SURFACES: Record = { }, }, }; + +/** + * T2 resolutions for the production v3 contract — item-mode collection + * spellings proven by src/t2.test.ts. `radio-group` collects what each item + * itself carries ({value: self.id}); its htmlFor-joined sibling labels are + * the measured T3 frontier, dropped with record until declared joins land. + * `select` is the pure homogeneous shape ({label: self.text}). + * + * Each entry carries the full receiving plan (structural options schema) — + * unlike T1's transparent plans, a collecting plan IS a catalog component. + */ +export const SHADCN_V3_T2_PLANS: Record> = { + "radio-group": { + a2ui: "RadioGroup", + dspackId: "radio-group", + commons: ["ComponentCommon"], + structural: { + options: { + schema: { type: "array", items: { type: "object", properties: { value: { type: "string" }, label: { type: "string" } }, required: ["value"], additionalProperties: false } }, + description: "The selectable options, one record per item.", + synthNote: "A2UI models repeated options as data on the group; dspack models them as repeated sub-components (T2 item-mode collection).", + }, + }, + propMap: { + name: { a2ui: "name", kind: "string" }, + defaultValue: { a2ui: "defaultValue", kind: "string" }, + }, + required: ["options"], + surface: { collects: [{ of: ["radio-group-item"], into: "prop:options", item: { value: "self.id" } }] }, + }, + select: { + a2ui: "Select", + dspackId: "select", + commons: ["ComponentCommon"], + structural: { + options: { + schema: { type: "array", items: { type: "object", properties: { label: { type: "string" } }, required: ["label"], additionalProperties: false } }, + description: "The selectable options.", + synthNote: "A2UI models repeated options as data on the control (T2 item-mode collection).", + }, + }, + propMap: { defaultValue: { a2ui: "defaultValue", kind: "string" } }, + required: ["options"], + surface: { + collects: [{ of: ["select-item"], into: "prop:options", item: { label: "self.text" } }], + subs: { + "select-trigger": { drop: "the trigger renders from the bound value, not authored content" }, + "select-value": { drop: "the visible value is runtime state derived from the selection" }, + "select-label": { drop: "group headings are not carried by the flat options shape" }, + "select-separator": { drop: "visual grouping chrome with no data meaning" }, + }, + }, + }, +}; From 28cfb765415f2d81b380cdec5732e13ea14c690e Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 13:29:14 -0400 Subject: [PATCH 2/2] =?UTF-8?q?eval(t1):=20the=20authoritative=20post-T1?= =?UTF-8?q?=20Build=20baseline=20=E2=80=94=20the=20governance=20hole=20is?= =?UTF-8?q?=20closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same six prompts, same model and config as the T1 matrix; contract 3.1.0 (the form-control rule live), gen 0.2.2, emit 0.5.0+T1. The T1-exposed defect now fails at the layer that owns it: zero-control forms are caught at S3 with repairable findings, and the 'donation boundary found 0' emit blocker is gone from the entire post-T1 column. The old 0/6 number stands only as historical evidence of the hole. Authoritative post-T1: pre-T1 1/6; post-T1 0/6 with every failure at an honest frontier — unmapped components (alert, field-set, dialog), stray subs outside their compound (correctly refused), and lint-exhausted repairs where the model cannot satisfy the tightened governance in two attempts. Two adapter timeouts retried once, originals preserved. T1's conversion on this model is limited by model quality, not representation: the named chain emits deterministically in the corpus tests, and under generation the model's empty forms now die at governance instead of sailing through — the system working. --- eval/t1-rebaseline-matrix.json | 220 +++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 eval/t1-rebaseline-matrix.json diff --git a/eval/t1-rebaseline-matrix.json b/eval/t1-rebaseline-matrix.json new file mode 100644 index 0000000..b1f37fa --- /dev/null +++ b/eval/t1-rebaseline-matrix.json @@ -0,0 +1,220 @@ +{ + "model": "qwen3.6:35b", + "contract": "3.1.0 @ dspack 862ca95", + "gen": "0.2.2", + "emit": "0.5.0+T1 (main)", + "intents": [ + "destructive-action", + "record-collection", + "structured-input", + "preference-settings", + "record-detail", + "overlay-task" + ], + "runs": [ + { + "variant": "pre-T1", + "intent": "destructive-action", + "outcome": "failed-lint-exhausted", + "attempts": 3, + "firstAttemptGates": "", + "perAttempt": [ + "", + "", + "" + ], + "firstBlocker": "failed-lint-exhausted", + "zeroControlFinding": true, + "seconds": 482 + }, + { + "variant": "pre-T1", + "intent": "record-collection", + "outcome": "failed-lint-exhausted", + "attempts": 3, + "firstAttemptGates": "", + "perAttempt": [ + "", + "", + "" + ], + "firstBlocker": "failed-lint-exhausted", + "zeroControlFinding": false, + "seconds": 325 + }, + { + "variant": "pre-T1", + "intent": "structured-input", + "outcome": "passed", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": null, + "zeroControlFinding": false, + "seconds": 77 + }, + { + "variant": "pre-T1", + "intent": "preference-settings", + "outcome": "failed-gate", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": "emit refusal: unknown component 'form': not a mapped component of the 'shadcn/ui v3 \u2014 coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-compone", + "zeroControlFinding": false, + "seconds": 144 + }, + { + "variant": "pre-T1", + "intent": "record-detail", + "outcome": "failed-gate", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": "emit refusal: unknown component 'form': not a mapped component of the 'shadcn/ui v3 \u2014 coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-compone", + "zeroControlFinding": false, + "seconds": 107 + }, + { + "variant": "pre-T1", + "intent": "overlay-task", + "outcome": "failed-gate", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": "emit refusal: unknown component 'dialog': not a mapped component of the 'shadcn/ui v3 \u2014 coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-compo", + "zeroControlFinding": false, + "seconds": 105 + }, + { + "variant": "post-T1", + "intent": "destructive-action", + "outcome": "failed-gate", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": "emit refusal: unknown component 'alert': not a mapped component of the 'shadcn/ui v3 \u2014 coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-compon", + "zeroControlFinding": false, + "seconds": 68 + }, + { + "variant": "post-T1", + "intent": "record-collection", + "outcome": "failed-lint-exhausted", + "attempts": 3, + "firstAttemptGates": "", + "perAttempt": [ + "", + "", + "" + ], + "firstBlocker": "failed-lint-exhausted", + "zeroControlFinding": true, + "seconds": 342 + }, + { + "variant": "post-T1", + "intent": "structured-input", + "outcome": "failed-gate", + "attempts": 2, + "firstAttemptGates": "", + "perAttempt": [ + "", + "" + ], + "firstBlocker": "emit refusal: unknown component 'form-field': not a mapped component of the 'shadcn/ui v3 \u2014 coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-c", + "zeroControlFinding": true, + "seconds": 156 + }, + { + "variant": "post-T1", + "intent": "preference-settings", + "outcome": "failed-adapter", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": "failed-adapter", + "zeroControlFinding": false, + "seconds": 598 + }, + { + "variant": "post-T1", + "intent": "record-detail", + "outcome": "failed-adapter", + "attempts": 2, + "firstAttemptGates": "", + "perAttempt": [ + "", + "" + ], + "firstBlocker": "failed-adapter", + "zeroControlFinding": true, + "seconds": 650 + }, + { + "variant": "post-T1", + "intent": "overlay-task", + "outcome": "failed-lint-exhausted", + "attempts": 3, + "firstAttemptGates": "", + "perAttempt": [ + "", + "", + "" + ], + "firstBlocker": "failed-lint-exhausted", + "zeroControlFinding": false, + "seconds": 234 + } + ], + "retries": [ + { + "variant": "post-T1", + "intent": "preference-settings", + "outcome": "failed-gate", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": "emit refusal: unknown component 'field-set': not a mapped component of the 'shadcn/ui v3 \u2014 coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-co", + "zeroControlFinding": false, + "seconds": 83, + "note": "single retry of an infrastructure adapter timeout (originals preserved below)" + }, + { + "variant": "post-T1", + "intent": "record-detail", + "outcome": "failed-gate", + "attempts": 1, + "firstAttemptGates": "", + "perAttempt": [ + "" + ], + "firstBlocker": "emit refusal: unknown component 'form-label': not a mapped component of the 'shadcn/ui v3 \u2014 coverage-evaluation catalog (NOT production, NOT Studio's profile)' profile (sub-c", + "zeroControlFinding": false, + "seconds": 33, + "note": "single retry of an infrastructure adapter timeout (originals preserved below)" + } + ], + "method": { + "contract": "3.1.0 @ dspack 862ca95 (governance rule live)", + "gen": "0.2.2 (requiredCategories S3)", + "emit": "0.5.0+T1 @ 868721b", + "model": "qwen3.6:35b, maxRepairs 2, same six prompts as the T1 matrix", + "profiles": "same PRE/POST derivation as the T1 matrix" + }, + "finding": "THE GOVERNANCE HOLE IS CLOSED: zero-control forms are caught at S3 with repairable findings (zeroControlFinding true in 4 runs) and the 'donation boundary found 0' emit blocker is GONE from the entire post-T1 column \u2014 the T1-exposed defect now fails at the layer that owns it. Authoritative post-T1 baseline: pre-T1 1/6 passed (structured-input); post-T1 0/6 with all failures at honest frontiers: unmapped components (alert, field-set, dialog), stray subs outside their compound (form-field, form-label \u2014 correctly refused), and lint-exhausted repairs where the model cannot satisfy the tightened governance within 2 repairs. Two adapter timeouts were retried once (originals preserved). T1's conversion effect on this model is limited by model quality, not representation: the named chain emits deterministically in the corpus tests; under generation, qwen3.6:35b's form output now dies at governance instead of sailing through as semantically empty \u2014 which is the system working." +}