From 33b8979dc733841b9b168e5d03da11e23f705e66 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 17:02:00 -0400 Subject: [PATCH] =?UTF-8?q?spec(v0.4):=20S2=20sub-component=20containment?= =?UTF-8?q?=20=E2=80=94=20=C2=A75.1,=20ratified=20on=20post-T3=20Build=20e?= =?UTF-8?q?vidence=20(0.4.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sub-declared component id may appear only within the subtree of an instance of a declaring compound, unless the contract also declares the id as a top-level component (the explicit independently-usable escape hatch; unused by shipped contracts today). Measured need: 19% of generated nodes in the post-T3 six-scenario Build probe placed subs outside their compound (root-level form-label, select-trigger beside its select, bare alert-dialog-content duplicated beside a nested one); all passed S2-as-membership, none could be expressed as an S3 rule (all four types anchor on containers; nothing anchors at root), and the emitter refused terminally AFTER the repair loop. Two of six scenarios died unrepaired on this class. Semantics: owners come only from composition.subComponents (never names, prefixes, adjacency, or examples); pass iff at least one owner is an ANCESTOR (any depth — intermediate structure allowed); root orphans and sibling placement are the same refusal; both-declared ids are components everywhere; findings are pathed and name the orphan and its owner(s) so repair can relocate. Ownership, not order — v0.3 §8's exclusions stand. No schema change: the ownership relationship is already contract data. Enforcement lands in dspack-gen S2 (fail-first pins ride that change). Co-Authored-By: Claude Fable 5 --- package.json | 2 +- spec/dspack-v0.4.md | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index de28594..6d4f8f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aestheticfunction/dspack-spec", - "version": "0.4.3", + "version": "0.4.4", "description": "The dspack specification: spec documents, JSON Schemas, reference example contracts, and the validation harness (bin: dspack-validate).", "type": "module", "license": "Apache-2.0", diff --git a/spec/dspack-v0.4.md b/spec/dspack-v0.4.md index 51a2497..5085dc7 100644 --- a/spec/dspack-v0.4.md +++ b/spec/dspack-v0.4.md @@ -259,6 +259,61 @@ territory, resolved through the contract at lint time. Generation schemas contin encode vocabulary and shape only (v0.3 §3) — neither categories nor any rule content belongs in them. +### 5.1 S2 — sub-component containment + +> **Amendment (2026-08-07), on measured evidence.** Post-T3 Build evaluation +> against the production contract (dspack-emit eval/, six scenarios) showed +> 19% of generated nodes placing sub-components outside their declaring +> compound — root-level `form-label`, `select-trigger` beside rather than +> beneath `select`, bare `alert-dialog-content` duplicated next to a correctly +> nested one. Every such surface passed S2 (membership only), passed S3 +> (no rule can express inverse containment: all four rule types anchor on a +> container and constrain descendants, and nothing anchors at the root), and +> then failed **terminally** at the emitter — after the repair loop, which +> never saw a finding. Two of six scenarios died unrepaired on this class, +> more than on any representation gap. + +A component id declared as a sub-component of a compound may appear in a +surface **only within the subtree of an instance of a declaring compound**, +unless the contract also declares that id as a top-level component (which is +the contract's way of saying *independently usable*; no shipped contract +does so today). + +This is a structural vocabulary invariant, not design-system judgment: the +ownership relationship is already declared by `composition.subComponents`, +the check is mechanically derivable from the contract, and the emitter +already enforces the same relationship later and terminally. Placing it in +S2 makes the defect visible to the repair loop. It is **not** an S3 rule and +MUST NOT be authored as per-contract containment rules when the contract +already declares the ownership. + +Normative semantics, per surface node whose component id is sub-declared: + +- **Owners** are exactly the compounds declaring the id in their + `composition.subComponents` — never inferred from names, prefixes, + adjacency, or examples. (Document-wide sub-id uniqueness — the existing + ambiguous-vocabulary refusal — means exactly one owner today; if a future + revision permits the same id under several compounds, that declaration set + *is* the owner set.) +- The node passes iff **at least one owner appears in its ancestor chain**. + The owner may be any ancestor, not only the parent: arbitrary intermediate + descendants are permitted where the contract's semantics allow them. +- A root-level sub-component and a sub placed as a *sibling* of its owner + are the same defect — no owner in the ancestor chain — and both refuse. +- An id declared **both** as a top-level component and as a sub-component is + a component everywhere: containment does not apply to it. +- The finding is pathed and MUST name the orphaned sub-component and its + declared owner(s), so a repair round can relocate it. + +S2 remains a check on **any** produced surface. Generation schemas continue +to encode vocabulary and shape only — tightening a grammar to per-parent +child branches is an optional engineering decision downstream of this spec, +never a substitute for the gate. + +What v0.3 §8 excluded stays excluded: `acceptsChildren` semantics, non-enum +prop value types, and sub-component nesting **order** remain outside S2. +Containment is about *ownership*, not order. + ## 6. Deliberate Ceiling Still not expressible in v0.4, recorded so the ceiling stays explicit: