@adia-ai/adia-ui-forge 0.8.55 → 0.8.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.55",
3
+ "version": "0.8.56",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.55",
3
+ "version": "0.8.56",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog — adia-ui-kit-forge
2
2
 
3
+ ## [0.8.56] — 2026-08-31
4
+
5
+ ### Added
6
+ - **New `component-md-authoring` skill (gh#2615).** Authors the two judgment sections of a component's `component.md` — Screen-reader spec and Behavioral spec — typed directly into the yaml's new `screenReader:`/`behavioral:` fields (component.md itself is a generated shell, never hand-edited). Sits next to `primitive-authoring` in `packages/plugins/adia-ui-forge/skills/`; Codex (`agents/openai.yaml`) and Hermes/Pi (`__init__.py`) manifests regenerated to register it.
7
+ - **`primitive-authoring`'s `yaml-contract.md` gains a semantic-id grammar for `examples[].a2ui` component ids (gh#2492 Phase 1).** The `.claude/docs/specs/a2ui-editor.md:170` "free-form" id convention was written for editor-generated ids only, not authored yaml examples — reading it as license for both produced a corpus-wide drift toward cryptic, positional ids (`q`, `k1`, `k1v`) reported against `blockquote.yaml`, `badge.yaml`, `aside.yaml`, `alert.yaml`, and `anchor-bar.yaml`. New `## examples: field` section documents the grammar (kebab-case, role-descriptive, unique per example, a short-real-word root/wrapper exception) with before/after examples; `a2ui-editor.md:170` amended to scope its own claim to editor-generated ids and cross-reference the new grammar. Phase 2 (a separate dispatch) sweeps the corpus to match.
8
+ - **Phase 2 (gh#2492): the corpus-wide sweep landed and `check-example-ids.mjs` flips from advisory to `npm run check:example-ids:strict` in the `npm run check` chain.** 526 violations across 48 yamls (`packages/web-components`, `packages/web-modules`) renamed to the Phase 1 grammar — see `packages/web-components/CHANGELOG.md` and `packages/web-modules/CHANGELOG.md` for the file list. `yaml-contract.md`'s `## examples: field` section updated to describe the gate as enforced, plus a noted known gap (the checker's positional regex doesn't catch a `letter-digit-letter-digit` chain like `g1i1`, out of scope for this sweep since nothing flagged it). `check-example-ids.mjs` itself gained an entrypoint guard (`if (import.meta.url === ...)`) — the CLI's `main()` was running as a side effect of importing `checkExampleIds` as a library function, which the sweep tooling does.
9
+
10
+ ### Fixed
11
+ - **`package-release`'s Step 5 staging allowlist was missing the factory README's own prose echo of the generation-MCP pin — bump.mjs's `A2UI_MCP_PIN` rewrites both `adia-ui-factory/.mcp.json` and its README, but Step 5 only ever staged `.mcp.json`, so the README edit rode uncommitted through every cut (gh#2473).** `release-pack.mjs`'s Step 5 now also stages `packages/plugins/adia-ui-factory/README.md`. Added a new Step 5.6 unstaged-tracked-files guard right after staging (numbered 5.6, not 5.5 — `cut-procedure.md`'s own §Step 5.5 already names a different, pre-existing manual freshness trip-wire): it fails the cut loudly if any tracked file bump.mjs or a cut-hygiene step touched is still modified-in-the-worktree after Step 5's allowlist ran, so the next allowlist gap is caught at cut time instead of discovered by hand later. `selftest` extended with a direct unit test of the new `unstagedTrackedLeftovers()` parser (plain modifies/deletes, staged-only and untracked exclusions, C-quoted paths, rename/copy new-side reporting) plus a marker asserting the guard's `--dry` code path runs.
12
+
13
+ ### Changed
14
+ - **`package-release`: gate 4 retries once on host-contention timeout (Mode A, gh#2195, #2362)**; `cut-procedure.md` fails fast on a pnpm-shaped `node_modules` in Step 1 and adds Step 12 (restore the pnpm dev layout after a cut) (#2197, #2202); `gates-catalog.md` follows.
15
+ - **`a2ui-maintenance` references follow the A2UI runtime.** `data-model-reactivity.md`, `surface-lifecycle.md`, and `pipeline-overview.md` describe the vendored v1 data model after ADR-0078 P3/P4 (walker retirement, #2271), the `v0_9`→`v1_0` dialect rename (gh#2212, #2241), and the five opt-out-scoped catalogs (gh#2211, #2233, #2216).
16
+ - **`primitive-authoring` references absorb the ADR-drift harvest (#2441) and ADR-0063 wave 2 (#2443)**: `anti-patterns.md`, `api-contract.md`, `authoring-cycle.md`, `code-style.md`, `form-control-sizing.md`, `token-contract.md`, and `yaml-contract.md` now state the ratified spellings (`no-*` negation prefix, `closable`, `outlined`, `status=completed`) and the amended ADR-0047/0056/0059/0061 rulings inline.
17
+ - **`HARNESS-NOTES.md`** pointer for non-Claude harnesses (gh#2449).
18
+
19
+ ### Fixed
20
+ - **`generate-release-notes.mjs` (gh#2474): two bugs found and hand-fixed live during the v0.8.56 cut, now fixed at the source.** (1) When a CHANGELOG `[X.Y.Z]` section carries two same-named `### Changed`/`### Fixed` blocks (one at cut time, another added by a later hand-fix), the generator now merges them into one heading in first-occurrence order before demoting to `####` — the duplicate `####` heading that tripped CodeRabbit's MD024 (db13eaebb) can't recur. (2) The Slack post's per-package headline now prefers a bullet starting with `- **BREAKING` over document order, falling back to the first bullet only when none is BREAKING (b378b5fd9). Added `mergeDuplicateHeadings()`, unit coverage in `scripts/release/generate-release-notes.test.js`, and `selftest` regression cases for both. `scripts/release/generate-release-notes.mjs`.
21
+
22
+ ### Maintenance
23
+ - **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
24
+ - **`.codex-plugin/` touched in this release window** (1 file(s), e.g. `.codex-plugin/plugin.json`) — carried by the entries above.
25
+ - **`skills/` touched in this release window** (14 file(s), e.g. `a2ui-maintenance/SKILL.md`) — carried by the entries above.
26
+
3
27
  ## [0.8.55] — 2026-08-28
4
28
 
5
29
  ### Changed
package/__init__.py CHANGED
@@ -11,6 +11,11 @@ def register(ctx):
11
11
  path=Path(os.path.join(_HERE, "skills", "a2ui-maintenance", "SKILL.md")),
12
12
  description="Maintains the A2UI pipeline (packages/gen-ui/a2ui/): the chunk corpus, compose strategies (zettel, chunk-zettel, free-form, monolithic), retrieval, validator, calibration, evals, the a2ui MCP server. Use to author/harvest/fix chunks, tune STRONG_MATCH or zettel thresholds, validate an A2UI document, diagnose an eval gap/regression or lift a semantic fail, change MCP tools (generate_ui, compose_from_chunks, check_anti_patterns, refine_composition), scan anti-patterns, run pipeline ops, or when a contract can't express a shape. NOT for app screens (screen-composition), runtime gen-UI features (gen-ui-wiring), primitive authoring (primitive-authoring), or gallery scoring (gen-ui-review).",
13
13
  )
14
+ ctx.register_skill(
15
+ name="component-md-authoring",
16
+ path=Path(os.path.join(_HERE, "skills", "component-md-authoring", "SKILL.md")),
17
+ description="Author the two judgment sections of a component's `component.md` — Screen-reader spec and Behavioral spec — and keep it PR-fresh. Use when a component's states, composed children, aria behavior, or error/empty/ loading handling changes and it already has (or should grow) a `component.md`, or when asked to \"add component.md for X\" / \"write the screen-reader spec for X\" / \"why did check:component-md-fresh fail\". NOT the yaml prop/slot/event/token contract itself (primitive-authoring owns that — this skill only owns the two authored yaml fields, `screenReader`/`behavioral`, plus the optional `intent` field); NOT gen-ui corpus/retrieval wiring (a2ui-maintenance); NOT a component's CSS token audit (component-token-audit).",
18
+ )
14
19
  ctx.register_skill(
15
20
  name="cross-harness-authoring-standards",
16
21
  path=Path(os.path.join(_HERE, "skills", "cross-harness-authoring-standards", "SKILL.md")),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.55",
3
+ "version": "0.8.56",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
5
5
  "keywords": [
6
6
  "adia-ui",
package/plugin.yaml CHANGED
@@ -1,4 +1,4 @@
1
1
  name: "adia-ui-kit-forge"
2
- version: "0.8.55"
2
+ version: "0.8.56"
3
3
  description: "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin)."
4
4
  manifest_version: 1
@@ -47,7 +47,7 @@ Unmatched work defaults to pipeline-overview and re-classifies from there.
47
47
  | Tune the anti-pattern catalogue | [anti-patterns](references/anti-patterns.md) |
48
48
  | A contract can't express a content shape — decide how to extend it | [format-extension-decisions](references/format-extension-decisions.md) |
49
49
  | Surface regeneration, pending/stale rendering, the `doc`-setter bracket | [surface-lifecycle](references/surface-lifecycle.md) (ADR-0061) |
50
- | Data-model internals — `Cell`/`Derived`, RFC-6901 pointers, `path-pointer.js` call-site migration, watch semantics | [data-model-reactivity](references/data-model-reactivity.md) (ADR-0078) |
50
+ | Data-model internals — `Cell`/`Derived`, RFC-6901 pointers, watch semantics (shipped) | [data-model-reactivity](references/data-model-reactivity.md) (ADR-0078) |
51
51
 
52
52
  ## Contracts that gate every change
53
53
 
@@ -1,14 +1,16 @@
1
1
  # v1 data-model reactivity — Cell/Derived, RFC-6901 pointers, value-identity cutoff
2
2
 
3
3
  Source of truth: [ADR-0078](../../../../../../docs/ops/adr/adr-0078-a2ui-runtime-adopts-v1-data-model.md)
4
- (ratified 2026-08-20, gh#1762; phased plan tracked in gh#1784). Read the ADR
5
- before touching `packages/gen-ui/a2ui`'s data-model internals, `path-pointer.js`
6
- call sites, or `surface.js`/`renderer.js` watch semantics — this file is the
7
- routing pointer + the shape of the adopted contract, not a restatement of the
8
- ruling, and NOT a claim that any phase has shipped (check gh#1784's own state
9
- before citing this as already-live behavior).
4
+ (ratified 2026-08-20, gh#1762; phased plan tracked in gh#1784, **closed
5
+ 2026-08-28 all four phases P1-P4 shipped**). Read the ADR before touching
6
+ `packages/gen-ui/a2ui`'s data-model internals or `surface.js`/`renderer.js`
7
+ watch semantics this file is the routing pointer + the shape of the
8
+ adopted contract, not a restatement of the ruling. `path-pointer.js` (the
9
+ transitional three-walker shim gh#1763 staged) is deleted as of P4; every
10
+ call site now uses the vendored `resolvePointer`/`setPointer`/`deletePointer`/
11
+ `createDataModel` primitives directly.
10
12
 
11
- ## What's decided (direction, not yet fully built)
13
+ ## What's decided (shipped ADR-0078 P1-P4 complete, gh#1784 closed)
12
14
 
13
15
  The in-repo A2UI runtime (`packages/gen-ui/a2ui`, the 0.9 dialect) **adopts**
14
16
  the vendored `packages/genui` v1.0 data model internally — `Cell`/`Derived`
@@ -19,8 +21,11 @@ data model. This is an internal implementation swap, not a consumer
19
21
  migration or a wire-grammar change: the dialect's eight message kinds, the
20
22
  `{path}` binding-prop shape, `updateDataModel`, `HandlerContext.updateModel/
21
23
  setModel`, `registerController/Handler/Resolver`, and the `<a2ui-root>`
22
- element API are all unchanged (ADR-0078 Decision item 2 — falsified if any
23
- consumer needs a code change, or if `dialect-schema.source.mjs` diffs).
24
+ element API are all unchanged. **[amended 2026-08-28, gh#2268/lld-0005,
25
+ gh#2212]** Decision item 2's falsifier is narrowed to wire-GRAMMAR diffs
26
+ only: it is falsified if any consumer needs a code change, or if
27
+ `dialect-schema.source.mjs`'s wire grammar diffs — a bare `$id`/filename
28
+ identifier rename in `dialect-schema.source.mjs` no longer falsifies it.
24
29
  Phased across future cuts, plan/LLD to follow — nothing rides in the cut
25
30
  this ADR itself was ratified for (item 6).
26
31
 
@@ -49,19 +54,25 @@ this ADR itself was ratified for (item 6).
49
54
  same identity-cutoff rule as R-R10). This supersedes `surface.js`'s
50
55
  prefix-descend rule and `renderer.js`'s re-apply-all-bindings behavior.
51
56
 
52
- ## What's converging, and what it changes
57
+ ## What converged (P1-P4, complete)
53
58
 
54
- `path-pointer.js` (gh#1763) currently preserves three divergent legacy
55
- walkers (`getByPath`/`setByPath`, `getPath`/`setPath`, `getModelValue`) on
56
- purpose, as a staging step each call site migrates to the vendored
57
- semantics above and retires when its last call site migrates. Every
58
- migration phase must name its own behavior deltas rather than changing
59
- silently (ADR-0078 item 4 is falsified by an unnamed semantics change) —
60
- known deltas already called out: `/a//b/` stops resolving forgivingly,
61
- `/name/length` on a string stops leaking the primitive's own property,
62
- and a2ui's own read/write asymmetry (today: `getByPath`/`getPath` treat
63
- absent-path/`""`/`"/"` alike, but `setByPath`/`setPath` no-op on root
64
- instead of replacing it) converges on the read/write split named above.
59
+ `path-pointer.js` (gh#1763) preserved three divergent legacy walkers
60
+ (`getByPath`/`setByPath`, `getPath`/`setPath`, `getModelValue`) as a
61
+ deliberate staging step; each call site migrated to the vendored semantics
62
+ above in turn (P2: `renderer.js`; P3: `surface.js`/`wiring-registry.js`),
63
+ and P4 deleted the module once its last call site migrated no divergent
64
+ walker survives (Decision 4). Every migration phase named its own behavior
65
+ deltas rather than changing silently (ADR-0078 item 4's falsifier): `/a//b/`
66
+ stopped resolving forgivingly, `/name/length` on a string stopped leaking
67
+ the primitive's own property, `~0`/`~1` escaping is now honored, and a2ui's
68
+ own read/write asymmetry (previously: `getByPath`/`getPath` treated
69
+ absent-path/`""`/`"/"` alike, but `setByPath`/`setPath` no-op'd on root
70
+ instead of replacing it) converged on the read/write split named above. The
71
+ full Δ1-Δ10 delta table lives in
72
+ [lld-0001-a2ui-data-model-consumption §Data](../../../../../../docs/ops/lld/lld-0001-a2ui-data-model-consumption.md#data);
73
+ the surviving regression proof for the pointer-only deltas is
74
+ `packages/gen-ui/a2ui/data-model-pointer-semantics.test.js` (migrated from
75
+ `path-pointer.test.js` in P4).
65
76
 
66
77
  ## What stays fixed (don't "fix" these under this ADR)
67
78
 
@@ -76,15 +87,24 @@ instead of replacing it) converges on the read/write split named above.
76
87
  - `record.js`'s bidirectional-overlap store — app-layer, outside this
77
88
  package, rides a separate review track (R2), not this ADR.
78
89
 
79
- ## Consumption mechanism — open, LLD decides
90
+ ## Consumption mechanism — decided (build-time copy)
80
91
 
81
- `@adia-ai/a2ui` is a zero-runtime-deps package (ADR-0048 posture);
82
- `packages/genui` is a vendored, never-edited-in-place artifact (ADR-0059).
83
- Whether the adopted primitives land via a build-time vendor-copy (with a
84
- provenance stamp) or a workspace-internal import is NOT decided by
85
- ADR-0078whichever the LLD picks must either preserve the zero-deps
86
- posture or explicitly re-rule it. Check the LLD (once authored, per gh#1784)
87
- before assuming either mechanism.
92
+ `@adia-ai/a2ui` is a zero-runtime-deps package (ADR-0048 posture).
93
+ **[amended 2026-08-29, ADR-0096, gh#2373 — recorded in ADR-0078's own
94
+ 2026-08-29 amendment]**
95
+ `packages/genui` is absorbed first-party in-repo source, not a vendored
96
+ artifact`packages/genui/VENDOR.json` and the vendor-and-sync mechanism
97
+ are gone (gh#2372 closed; `VENDOR.json` confirmed absent from origin/main).
98
+ [lld-0001](../../../../../../docs/ops/lld/lld-0001-a2ui-data-model-consumption.md)
99
+ decided a build-time byte-identical copy of
100
+ `packages/genui/renderer/dist/data-model.js` (+`.d.ts`) into
101
+ `packages/gen-ui/a2ui/`, with a `data-model.provenance.json` sidecar and a
102
+ freshness gate in `npm run check` — preserving the zero-deps posture (no new
103
+ package dependency, no covert workspace-import). The provenance sidecar now
104
+ points at the in-repo source path instead of a `VENDOR.json` sha: it stamps
105
+ `source.js`/`source.dts` + `syncedAt` + `contentHash` only —
106
+ `packages/genui/renderer/dist/data-model.js` — with no vendor-sha field at
107
+ all.
88
108
 
89
109
  ## Eval-floor risk
90
110
 
@@ -4,34 +4,63 @@
4
4
 
5
5
  Two protocol layers coexist (ADR-0059, `docs/ops/spec/spec-a2ui-v1-conformance.md`):
6
6
  the shipping dialect this pipeline emits (Layer A, `packages/gen-ui/a2ui/`) and
7
- the vendored A2UI v1.0 Candidate stack (Layer B, `packages/genui/`) reached
8
- through `packages/genui/wire-bridge/`. Candidate terminology is
7
+ the A2UI v1.0 Candidate stack (Layer B, `packages/genui/`) reached
8
+ through `packages/genui/wire-bridge/`. **[amended 2026-08-29, ADR-0096]**
9
+ `adiahealth/gen-ui-system` is absorbed first-party under `packages/genui/`
10
+ and the standalone repo is archived — Layer B is in-repo source, not a
11
+ vendored dependency; `VENDOR.json` and the sync mechanism are gone (see
12
+ this same note on siblings `data-model-reactivity.md` and
13
+ `surface-lifecycle.md`). Candidate terminology is
9
14
  **renderer/agent** — never client/server: `callableFrom` values are
10
15
  `rendererOnly`/`agentOnly`/`rendererOrAgent`; the wire function kinds are
11
16
  `callRendererFunction`/`callAgentFunction` +
12
17
  `rendererFunctionResponse`/`agentFunctionResponse`; the MIME type is
13
18
  `application/a2ui+json`; catalog resolution is strict (component `catalogId` →
14
19
  surface `catalogId` → error, no registry default). The producer's
15
- `wireFormat: 'v1'` flag exists and defaults to `'dialect'`
16
- (`packages/genui/adia-producer/exit-gate.js`). Documents authored here stay
17
- dialect-shaped; the bridge owns the translation — never hand-write Candidate
18
- envelopes from this skill's surfaces.
19
-
20
- site-a2ui (the build-time HTML→A2UI docs-site transpile) is RULED fit as the
21
- **dialect side's regression corpus**, not a v1.0 conformance bed (ADR-0068):
22
- it exercises the dialect renderer, the ADR-0061 lifecycle path, and the
23
- engine transpiler at real-content scale in production, but never touches the
24
- producer, the bridge, or the `wireFormat` flag so it neither blocks the
25
- v1.0 migration nor gets re-pointed at the v1 wire. **Named expiry:** that
26
- ruling is load-bearing on `wireFormat` defaulting to `'dialect'`; the flag-flip
27
- ADR that makes `'v1'` the shipping default MUST re-rule site-a2ui's fitness
28
- (re-point vs retirement-by-attrition) the fitness verdict expires with the
29
- `'dialect'` default. Expanding site-a2ui new message kinds, new consumers,
30
- or a promotion PROGRAM toward all routes also invalidates the ruling's
31
- basis and needs a new decision; burn-down of existing
32
- `visual-drift`/`blocked-format-gap` rows (including a route thereby becoming
33
- parity-promoted) is ordinary maintenance and stays permitted (ADR-0068
34
- Decision 5).
20
+ `wireFormat` flag (`packages/genui/adia-producer/exit-gate.js`) **[flipped
21
+ 2026-08-30, PR #2412]** now defaults to `'v1'`, not `'dialect'` ADR-0072
22
+ ratified the flip (its precondition, genui-system#51's re-verification,
23
+ re-scoped and satisfied via ADR-0096/PR #2401) and the flip itself has since
24
+ executed. Documents authored here stay dialect-shaped; the bridge owns the
25
+ translation never hand-write Candidate envelopes from this skill's
26
+ surfaces.
27
+
28
+ **Catalogs are opt-out scopes, not a taxonomy.** A2UI v1.0 lets a renderer
29
+ mix catalogs within one surface: `createSurface.catalogId` is the default,
30
+ and any component may carry its own `catalogId` to override it
31
+ (a2ui.org/specification/v1.0-a2ui/, "Catalog Reference"). So the useful way
32
+ to split a catalog is by what an app OMITS, never by component kind — Buttons
33
+ and Inputs are never omitted together, whole feature areas are. This is the
34
+ ratified basis of the five-catalog partition (gh#2211, Kim 2026-08-28):
35
+ `adia.core` / `adia.navigation` / `adia.data` / `adia.agent` / `adia.shells`,
36
+ every one a derived view over the yaml `category` axis and package path,
37
+ never a hand list. Two facts that trip a re-derivation: the `category` axis
38
+ had no `data` bucket for tables/charts (they declared `agent`; the partition's
39
+ pre-step re-categorizes five components), and `packages/web-modules/**` is a
40
+ path seam, not a category (its sidecars mostly declare `layout`/`container`).
41
+ The partition lives on the Layer B side only; the dialect catalog stays one
42
+ file per ADR-0059 §1. The 44 L1 harvested widgets are a pattern library, not
43
+ catalog members.
44
+
45
+ site-a2ui (the build-time HTML→A2UI docs-site transpile) was RULED fit as
46
+ the **dialect side's regression corpus**, not a v1.0 conformance bed
47
+ (ADR-0068): it exercised the dialect renderer, the ADR-0061 lifecycle path,
48
+ and the engine transpiler at real-content scale in production, but never
49
+ touched the producer, the bridge, or the `wireFormat` flag. That fitness
50
+ ruling was load-bearing on `wireFormat` defaulting to `'dialect'`, with a
51
+ **Named expiry**: the flag-flip ADR that makes `'v1'` the shipping default
52
+ had to re-rule site-a2ui's fitness (re-point vs retirement-by-attrition).
53
+
54
+ **[resolved, ADR-0072]** That trigger has fired and been ruled:
55
+ retirement-by-attrition, not re-point (Decision 2) — promotion program
56
+ closed, site-a2ui moves to breakage-only maintenance, an attrition-exit
57
+ ticket tracks the wind-down (gh#2410), and the mechanism is removed once the
58
+ promoted set reaches zero. The flip itself has since executed (PR #2412,
59
+ merged 2026-08-30: `exit-gate.js` now defaults to `'v1'`) — site-a2ui is in
60
+ its attrition window now, not a still-open future trigger. Burn-down of
61
+ existing `visual-drift`/`blocked-format-gap` rows is no longer "ordinary
62
+ maintenance toward parity promotion" (ADR-0068 Decision 5) — it is wind-down
63
+ toward the mechanism's removal per gh#2410.
35
64
 
36
65
  All paths repo-relative. Specs worth reading before structural changes:
37
66
  `.claude/docs/specs/a2ui-v0.9-catalog-guide.md` (protocol + catalog format),
@@ -151,6 +180,11 @@ for any constant or decision lives in git and PR descriptions
151
180
  currently fails, mostly a schema-generation gap around `data-*`/`span`
152
181
  attributes, not corpus-content defects — see the ticket before
153
182
  assuming a chunk is actually broken).
183
+ 13. **Every generated catalog schema carries three synthesized universal
184
+ props today** (`slot`/`hidden`/`ariaLive`, none declared in any yaml
185
+ SoT); ADR-0097 rules a fourth, `traits`, but that part is decided-not-
186
+ yet-shipped (gh#2513) — see `primitive-authoring/references/
187
+ yaml-contract.md` §Synthesized universal props for the full contract.
154
188
 
155
189
  ## Test + run commands (all verified in root package.json)
156
190
 
@@ -57,13 +57,20 @@ Staleness is exposed as one attribute plus three events, never styling:
57
57
 
58
58
  The lifecycle is renderer-runtime work on gen-ui-kit's side of the ADR-0059
59
59
  line. The three wire envelope kinds (`beginSurfaceUpdate` /
60
- `commitSurfaceUpdate` / `abortSurfaceUpdate` as v1.0 server kinds) are a
61
- genui-system standard PROPOSAL, not dialect schema — `a2ui.schema.json`
62
- gains nothing, the dialect wire format is byte-identical, and stream-driven
63
- regeneration waits on the upstream standard (the dialect escape hatch was
64
- explicitly denied at ratification). Never hand-write lifecycle envelopes
65
- from this skill's surfaces; the Bridge owns the mapping when the standard
66
- lands.
60
+ `commitSurfaceUpdate` / `abortSurfaceUpdate` as v1.0 server kinds) are
61
+ defined by the v1.0 Candidate reference implementation — `a2ui.schema.json`
62
+ gains nothing, the dialect wire format is byte-identical. Never hand-write
63
+ lifecycle envelopes from this skill's surfaces; the Bridge owns the mapping
64
+ when the kinds land.
65
+
66
+ **[amended 2026-08-29, ADR-0096]** `adiahealth/gen-ui-system` is absorbed
67
+ first-party under `packages/genui/` and the standalone repo is archived —
68
+ there is no external, separately-governed "upstream standard" to wait on
69
+ any more. ADR-0096 Decision 5: gen-ui-kit "owns the whole stack now... and
70
+ the v1.0 Candidate reference implementation itself, not only the consumer
71
+ side of a vendor boundary." Stream-driven regeneration now proceeds on
72
+ gen-ui-kit's own schedule against the in-repo `packages/genui/` source,
73
+ not an upstream release.
67
74
 
68
75
  ## Interlock worth knowing
69
76
 
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: component-md-authoring
3
+ description: >-
4
+ Author the two judgment sections of a component's `component.md` —
5
+ Screen-reader spec and Behavioral spec — and keep it PR-fresh. Use when a
6
+ component's states, composed children, aria behavior, or error/empty/
7
+ loading handling changes and it already has (or should grow) a
8
+ `component.md`, or when asked to "add component.md for X" / "write the
9
+ screen-reader spec for X" / "why did check:component-md-fresh fail". NOT
10
+ the yaml prop/slot/event/token contract itself (primitive-authoring owns
11
+ that — this skill only owns the two authored yaml fields,
12
+ `screenReader`/`behavioral`, plus the optional `intent` field); NOT gen-ui
13
+ corpus/retrieval wiring (a2ui-maintenance); NOT a component's CSS token
14
+ audit (component-token-audit).
15
+ disable-model-invocation: false
16
+ user-invocable: true
17
+ ---
18
+
19
+ # component-md-authoring
20
+
21
+ `component.md` (gh#2615) is a generated shell, per-component, sitting next
22
+ to its `.yaml` SoT (`packages/web-components/components/<name>/component.md`,
23
+ or `packages/web-modules/<cluster>/<name>/component.md` for a composite/
24
+ shell). Every section except two is mechanically transcluded from the yaml
25
+ by `scripts/build/gen-component-md.mjs` — Intent, API (props/events/slots),
26
+ Structural (Light DOM anatomy + states + composes), Tokens, Rules,
27
+ Anti-patterns, Related. This skill's whole charter is the two sections that
28
+ aren't: **Screen-reader spec** and **Behavioral spec**.
29
+
30
+ ## The load-bearing decision: where the authoring happens
31
+
32
+ You do not hand-edit `component.md`. You edit the yaml's `screenReader:`
33
+ and `behavioral:` fields (and, ideally, `intent:`) — `component.md` is
34
+ regenerated from them. This is deliberate, not incidental:
35
+
36
+ - **No second source of truth.** plan-2615's evidence pass on gh#2615 found
37
+ most of component.md's "intent layer" already lives in the yaml
38
+ (`a2ui.rules`, `anti_patterns`, `related`, examples). The two genuine
39
+ gaps — screen-reader and behavioral judgment — get the SAME treatment:
40
+ authored once, in yaml, transcluded everywhere else (component.md today;
41
+ gen-ui corpus derivation once a2ui-maintenance wires it in).
42
+ - **The freshness gate is PR-blocking, not staleness-only.** Because the
43
+ authored content lives in a yaml field, `component.md` is 100%
44
+ mechanically regenerable — `check:component-md-fresh` can do a real byte-
45
+ diff, the same shape as `check:reference-docs-fresh`, except PR-blocking
46
+ (operator ruling 2026-08-31) rather than advisory-only. A hand-edit
47
+ directly in `component.md` will be silently clobbered by the next
48
+ `npm run docs:component-md` and will fail the gate as "not fresh" — this
49
+ is the guard rail, not a bug.
50
+
51
+ ## Authoring a component's two sections
52
+
53
+ 1. Confirm the component doesn't already have adequate coverage — read its
54
+ existing `states:`, `a2ui.rules`, and `.class.js` source. Per
55
+ primitive-authoring's own first principle, **source wins**: verify every
56
+ claim you're about to write (focus order, aria attribute names, event
57
+ names) against the actual `.class.js`/`.js` file, not just the yaml
58
+ prose.
59
+ 2. **Screen-reader spec** — focus order across composed children (order
60
+ `showModal()`/connect moves focus, what wraps at the tab boundary),
61
+ live-region announcement sequence (what fires `role="alert"` or an
62
+ `aria-live` region, and when), and any keyboard map beyond the trait
63
+ default (`pressable`/`focusable` already cover Enter/Space/click — only
64
+ document what's ADDITIONAL, e.g. arrow-key grid nav, Escape-dismiss).
65
+ Do not restate a static `aria-*` attribute the yaml's `props`/`states`
66
+ already document plainly — that's derived content, not new judgment.
67
+ 3. **Behavioral spec** — dismiss/error/empty/loading states and
68
+ transitions NOT already modeled by `states:`. Distinguish "fetching" vs
69
+ "confirmed empty" where both exist (see `table.yaml`'s `screenReader`/
70
+ `behavioral` for a worked example: three distinct states, not one).
71
+ Name what is explicitly NOT handled (no built-in error state, no
72
+ built-in loading state) as clearly as what is — an absence is often the
73
+ more actionable fact for a consumer.
74
+ 4. Both fields require `minLength: 20` (schema-enforced) — a placeholder
75
+ one-liner will fail `check:components-valid`. Write real prose, grounded
76
+ in source, not a restatement of the component's `description`.
77
+ 5. Regenerate and verify:
78
+
79
+ ```bash
80
+ node scripts/build/components.mjs --validate # schema-valid yaml
81
+ npm run docs:component-md # regenerate component.md
82
+ npm run check:component-md-fresh # PR-blocking gate
83
+ ```
84
+
85
+ 6. If this is the component's FIRST component.md (yaml previously had
86
+ neither field), run `npm run build:components` too — the corpus/catalog
87
+ rebuild picks up the new yaml content, and `npm run eval:diff --
88
+ --engine zettel` should show no regression (preserve-not-regress floor,
89
+ owned by a2ui-maintenance).
90
+
91
+ ## Scope (gh#2615 pilot)
92
+
93
+ `scripts/build/gen-component-md.mjs`'s `SCAN_ROOTS` covers
94
+ `packages/web-components/components/` and `packages/web-modules/chat/`
95
+ today — the pilot 5 (`button`, `modal`, `table`, `field`, `chat-shell`).
96
+ Extending to every web-modules cluster, or sweeping the remaining ~145
97
+ primitives, is deliberately out of scope for this pass (file a follow-up
98
+ task rather than silently expanding `SCAN_ROOTS` for one-off need — a
99
+ cluster added there without a plan for authoring every component inside it
100
+ just produces components with a `.yaml` but no eligible `component.md`,
101
+ which the generator already handles gracefully by skipping them, but which
102
+ defeats the point of a rollout plan).
103
+
104
+ ## Cross-references
105
+
106
+ - `primitive-authoring`'s authoring-cycle: add "new/changed states or aria
107
+ behavior → component-md-authoring's authored sections may need a pass"
108
+ to your own SoT-change checklist when editing a yaml that already has a
109
+ `component.md` sibling.
110
+ - `scripts/schemas/component.yaml.schema.json` — `intent`/`screenReader`/
111
+ `behavioral` field definitions (all optional; a component with a `.yaml`
112
+ but neither authored field simply has no `component.md` yet).
113
+ - `scripts/verify/check-component-md-fresh.mjs` — the PR-blocking gate:
114
+ byte-freshness (component.md matches a fresh render) AND same-PR
115
+ coverage (a component.md-bearing component's source/yaml change must
116
+ touch component.md in the same diff).
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "Component Md Authoring"
3
+ short_description: "Author the two judgment sections of a component's `component.md` — Screen-reader spec and Behavioral spec — and keep it PR-fresh."
@@ -31,6 +31,7 @@ Two entry variants, converging at Step 5:
31
31
  | 9 | Dispatch publish workflows; wait; verify registry | Publish |
32
32
  | 10 | GH releases + site deploy dispatch | Deploy |
33
33
  | 11 | Author release notes (default, not optional) | No |
34
+ | 12 | Restore the pnpm dev layout (`npm ci` left it npm-shaped) | node_modules only |
34
35
 
35
36
  ---
36
37
 
@@ -44,8 +45,18 @@ git -C "$REPO" status --short
44
45
  git -C "$REPO" log --oneline -8
45
46
  git -C "$REPO" fetch && git -C "$REPO" log HEAD..origin/main --oneline # must be empty
46
47
  git -C "$REPO" tag --list 'vX.Y.Z' '*-vX.Y.Z' # must NOT exist yet
48
+ [ -d "$REPO/node_modules/.pnpm" ] && echo "STOP: pnpm-shaped node_modules — run npm ci first" # gate 9 precondition, see §3.1 layout note
47
49
  ```
48
50
 
51
+ **Check the `node_modules` layout here, not at gate 9.** The §3.1 layout note (gh#1359)
52
+ already says gate 9 (`check:js-bundles-fresh`) needs the npm-ci layout, but it sits AFTER
53
+ the gate roster and is easy to read past — the v0.8.55 cut (2026-08-28) ran the full
54
+ 33-gate pre-flight under a pnpm-shaped tree, lost ~12 minutes to gates 1-8 and 10-33
55
+ passing, then failed gate 9 with phantom bundle drift (1.24 MB on disk vs 1.52 MB fresh
56
+ on every JS entry — the Phosphor icon glob resolving differently, not a real source
57
+ change). The one-line check above fails in under a second instead. `npm ci` to recover;
58
+ `npm install` alone does NOT reshape an existing pnpm tree.
59
+
49
60
  `branch --show-current` ≠ `main` → stop; cutting on a feature branch pushes a stale `main` ref while the tags point at the feature tip. Recovery: [`recovery-paths.md`](recovery-paths.md) §Scenario 8. If multiple unpushed `release(*):` commits exist → this is a batch push, [`recovery-paths.md`](recovery-paths.md) §Scenario 2.
50
61
 
51
62
  ## §Step 2 — Classify uncommitted files; stash strays
@@ -244,7 +255,9 @@ git -C "$REPO" checkout -b "release/vX.Y.Z"
244
255
  git -C "$REPO" reset HEAD >/dev/null 2>&1
245
256
  git -C "$REPO" add package-lock.json packages/*/package.json packages/*/CHANGELOG.md \
246
257
  packages/gen-ui/a2ui/*/package.json packages/gen-ui/a2ui/*/CHANGELOG.md \
247
- packages/genui/adia-catalog/base.json packages/genui/adia-catalog/adia-pack.json \
258
+ packages/genui/adia-catalog/adia.core.json packages/genui/adia-catalog/adia.navigation.json \
259
+ packages/genui/adia-catalog/adia.data.json packages/genui/adia-catalog/adia.agent.json \
260
+ packages/genui/adia-catalog/adia.shells.json \
248
261
  docs/ops/releases/vX.Y.Z.md # + in-scope source + Step-3.0 regen outputs
249
262
  git -C "$REPO" diff --cached --stat | tail -3 # count must match the allowlist
250
263
  ```
@@ -261,6 +274,19 @@ node scripts/build/components.mjs --verify && npm run check:chunks-fresh && npm
261
274
 
262
275
  Drift here means a regen output was left out of the allowlist — stage it and re-run. <2s now vs ~5min of tag-move recovery after CI catches it. Do not proceed to tag with drift.
263
276
 
277
+ ## §Step 5.6 — Unstaged-tracked-files guard (`release-pack.mjs`, gh#2473)
278
+
279
+ `release-pack.mjs`'s Step 5 mechanizes this: right after its own `git add`
280
+ (the allowlist above, as automated), it runs `git status --porcelain` and
281
+ fails the cut if any tracked file is still modified-in-the-worktree —
282
+ proof the allowlist covered everything bump.mjs / cut-hygiene touched this
283
+ cut, not just what §Step 5.5's three named freshness gates happen to check.
284
+ This is the fourth time the allowlist itself went stale (gh#1198, gh#1899,
285
+ gh#1954, gh#2473 — most recently the factory README's generation-MCP pin
286
+ prose), so the guard is generic rather than another named file. A manual
287
+ cut should run the equivalent check by hand: `git status --porcelain`
288
+ after staging must be empty of `M`/`D` lines.
289
+
264
290
  ## §Step 5.7 — Release PR: push the branch, merge, re-baseline
265
291
 
266
292
  The release commit reaches `main` through the standard PR flow. Mechanized
@@ -415,6 +441,25 @@ Any Step-2 stashes: `git stash pop`; flag conflicts to the operator.
415
441
 
416
442
  Always author notes at end-of-cycle without being asked — context is freshest now. Single version → [`notes-authoring.md`](notes-authoring.md); ≥2 versions since the last broadcast → its §Rollup section. Surface the draft inline for copy-paste; the operator owns posting. Skip only on an explicit "no notes".
417
443
 
444
+ ## §Step 12 — Restore the dev layout
445
+
446
+ The cut runs under `npm ci` (Step 1's layout check, gate 9), and that leaves
447
+ `node_modules` npm-shaped. The dev server refuses to start on that tree:
448
+ `npm run dev`'s `predev` hook (`scripts/dev/check-pnpm-layout.mjs`) hard-stops
449
+ because the Phosphor icon glob matches zero files under the hoisted layout and
450
+ every `<icon-ui>` would render empty (gh#340). Put the tree back before handing
451
+ the checkout to anyone, including yourself:
452
+
453
+ ```bash
454
+ pnpm install # single checkout
455
+ node scripts/dev/bootstrap-worktree.mjs # inside a linked worktree
456
+ ```
457
+
458
+ Both are idempotent and touch no committed file (`pnpm-lock.yaml` is
459
+ gitignored). Live cost of skipping this: the v0.8.55 cut (2026-08-28) left the
460
+ operator's primary checkout unable to run `npm run dev` until the layout was
461
+ restored by hand.
462
+
418
463
  ## §The 0.8.38 cut (the clean-world cut — delete this section after it lands)
419
464
 
420
465
  The first cut AFTER the estate-split bridge. Pre-conditions, all verifiable
@@ -159,6 +159,7 @@ The two `audit:*` probes are soft-fail pre-cut (exit-1 only on criticals); findi
159
159
  - **What:** the vitest suite (~1000+ tests).
160
160
  - **Stale-test failure:** the test asserts a behavior a peer deliberately changed (CHANGELOG-documented) without updating the assertion. Tell it apart by reading assertion vs CHANGELOG vs source: if the code matches the CHANGELOG's described behavior, the test is stale — update the assertion (see [`recovery-paths.md`](recovery-paths.md) §Scenario 5). Real regression → fix the regression.
161
161
  - **Parallel-contention flake:** heavy web-modules composite suites can fail under full parallelism on a loaded machine (`signals: drain loop exceeded 100 iterations`). Load-dependent, not a code defect. Sequential is the source of truth, and **the roster gate now runs it directly** (`npm run test:unit:serial` = `vitest run --no-file-parallelism`, gate 4 since 2026-07-19) — a parallel `test:unit` flake outside the roster still isn't a blocker, and don't raise the drain guard.
162
+ - **Same-host contention timeout (gh#2195):** gate 4 running solo (Phase 1, see the execution-model note above) isolates it from every OTHER pre-flight gate, but not from unrelated concurrent load on the same shared, multi-tenant host — another agent's build, a peer's own `npm run check`. gh#2195's diagnosis found this surfaces as a genuine 45s per-test timeout on a DIFFERENT single file each run, with no repeat culprit (the contention signature, not an order-dependent leak) — and found no separate "release pre-flight runner" with its own core count exists to recalibrate `worker-cap.mjs` against; it's the same operator/agent host either way. `release-pack.mjs`'s `runSoloGateWithRetry()` now retries gate 4 exactly once, scoped to only the file(s) it named as failed, when the failure output has a parseable `FAIL <file>` list — a file that fails once and passes clean on that immediate re-run was contention, not a regression; a file that fails twice is treated as a real one and the pre-flight still aborts. No other gate, and no OTHER invocation of `test:unit:serial` (CI, local dev, `npm run check`), gets this retry.
162
163
  - **Exit 143 (SIGTERM):** the vitest process was KILLED (resource pressure / a stray terminator), not a test failure — re-run the gate directly (`npx vitest run --no-file-parallelism`) before diagnosing anything; a clean re-run means transient, proceed (v0.8.34 handoff hit this).
163
164
  - **First-time-at-cut failures are REAL (v0.8.34):** this serial suite runs tests PR CI never does — `exit-gate.corpus` (dialect-catalog conformance over real harvested chunks) surfaces yaml-schema drift (e.g. an array prop missing `items.type`, the command/combobox `DynamicStringList` mis-map) only HERE, potentially weeks after the yaml edit merged green. Treat such a failure as a genuine latent defect to root-cause at the yaml SoT, never as release-blocking noise. (Structural fix — promoting the corpus batch into PR CI — tracked as a follow-up.)
164
165
 
@@ -516,6 +516,75 @@ const EVAL_HEALTH_CHAIN_NUMBERS = [16, 27, 28]; // eval:diff zettel → eval:dif
516
516
  // for a dedicated/idle host.
517
517
  const PREFLIGHT_CONCURRENCY = Number(process.env.PREFLIGHT_CONCURRENCY) || 4;
518
518
 
519
+ // gh#2195 — gate 4 flake-retry, scoped to the solo phase only.
520
+ //
521
+ // gh#2195's own diagnosis found two mechanisms behind "test:unit:serial
522
+ // flakes under host load": a real wall-clock timing race in one test (fixed
523
+ // separately, PR #2290) and genuine 45s timeouts under contention on the
524
+ // same shared, multi-tenant host an operator or agent session runs
525
+ // everything else on. There is no separate "release pre-flight runner" with
526
+ // its own core count to recalibrate worker-cap.mjs against — Phase 1 above
527
+ // already runs gate 4 solo, with no sibling PRE-FLIGHT gate competing, so
528
+ // what is left is contention from OTHER, unrelated concurrent processes on
529
+ // that same host (other agents' builds, other `npm run check` runs) that no
530
+ // single process's static worker-cap formula can see or size against.
531
+ //
532
+ // A file that fails once under that kind of contention and then passes
533
+ // clean on an immediate, unmodified re-run was never proving a code
534
+ // regression — gate 4 exists to prove one, and a transient scheduling stall
535
+ // isn't that. A file that fails twice in a row almost certainly is a real
536
+ // regression (a genuine bug reproduces deterministically; host contention
537
+ // does not), so this retries AT MOST ONCE, and only the files gate 4 itself
538
+ // named as failed — never the whole suite, never a second retry. Scoped to
539
+ // gate 4's solo phase alone: every other gate, and every OTHER invocation of
540
+ // `npm run test:unit:serial` (CI, local dev, `npm run check`), stays
541
+ // single-shot and strict — this never masks a real regression anywhere else.
542
+ const FAILED_TEST_FILE_RE = /^ FAIL\s+(\S+)/gm;
543
+
544
+ // Pure — extracts the deduped list of failed test file paths from vitest's
545
+ // default-reporter output. Its "Failed Tests" section prints a leading
546
+ // ` FAIL <file> > ...` line per failing test — the same line is emitted for
547
+ // an assertion failure and for a per-test timeout alike.
548
+ function extractFailedTestFiles(text) {
549
+ const files = new Set();
550
+ for (const m of (text || '').matchAll(FAILED_TEST_FILE_RE)) files.add(m[1]);
551
+ return [...files];
552
+ }
553
+
554
+ // Runs the solo gate; on failure, extracts which file(s) it named as failed
555
+ // and — only if at least one was parseable — retries gate 4 scoped to
556
+ // exactly those files, once. An unparseable failure (e.g. vitest itself
557
+ // crashed before printing a summary) fails closed: no retry, same behavior
558
+ // as before this change.
559
+ async function runSoloGateWithRetry(soloGate, args) {
560
+ const first = await runGateCaptured(soloGate, args);
561
+ printGateResult(first);
562
+ if (first.ok) return first;
563
+
564
+ const failedFiles = extractFailedTestFiles(`${first.stdout || ''}\n${first.stderr || ''}`);
565
+ if (failedFiles.length === 0) {
566
+ console.error('\n Gate 4 failed with no parseable failed-file list — not retrying (fail closed, gh#2195).');
567
+ return first;
568
+ }
569
+
570
+ console.log(`\n Gate 4 failed on ${failedFiles.length} file(s) — retrying once, scoped to just those files (host-contention tolerance, gh#2195):`);
571
+ for (const f of failedFiles) console.log(` - ${f}`);
572
+ const retryGate = {
573
+ n: soloGate.n,
574
+ cmd: `${soloGate.cmd} -- ${failedFiles.map((f) => `"${f}"`).join(' ')}`,
575
+ what: `${soloGate.what} — retry of ${failedFiles.length} flaky file(s)`,
576
+ };
577
+ const retry = await runGateCaptured(retryGate, args);
578
+ printGateResult(retry);
579
+
580
+ if (retry.ok) {
581
+ console.log(`\n ✓ gate 4 passed on retry — treating the first failure as host contention, not a regression (gh#2195).`);
582
+ return { ...retry, note: `passed on retry after 1 failure (gh#2195 host-contention tolerance); first attempt failed: ${failedFiles.join(', ')}` };
583
+ }
584
+ console.error('\n Gate 4 failed again on retry — a real regression, not a flake. Aborting.');
585
+ return { ...retry, note: `failed again on retry (gh#2195); first attempt failed: ${failedFiles.join(', ')}` };
586
+ }
587
+
519
588
  // Resolves one gate's actual command string — the same versionArg/
520
589
  // targetVersionArg/resume branching step3PreFlight has always applied.
521
590
  // Returns null for a versionArg gate deferred by an in-flight resume (Step
@@ -656,8 +725,7 @@ async function step3PreFlight(args) {
656
725
  const poolGates = GATE_ROSTER.filter((g) => g.n !== SOLO_GATE_NUMBER && !chainNumbers.has(g.n));
657
726
 
658
727
  console.log(`\n-- Phase 1/2: gate ${SOLO_GATE_NUMBER} solo (test:unit:serial) --`);
659
- const soloResult = await runGateCaptured(soloGate, args);
660
- printGateResult(soloResult);
728
+ const soloResult = await runSoloGateWithRetry(soloGate, args);
661
729
  if (!soloResult.ok) {
662
730
  console.error('\n Pre-flight aborted at Phase 1 — fix the gate failure before proceeding.');
663
731
  console.error(' See references/gates-catalog.md for failure-mode → recovery.');
@@ -799,10 +867,11 @@ function step4PromoteAndBump(args) {
799
867
  sh('npm install --package-lock-only --no-audit --no-fund', args);
800
868
  sh('npm run check:lockstep', args);
801
869
 
802
- // Step 4d.5 — regenerate the derived genui catalog (gh#617): its catalogId
803
- // embeds the lockstep version (`adia.base@X.Y.Z`), so the bump above just
804
- // invalidated base.json/adia-pack.json. The v0.8.26 cut skipped this and
805
- // check:genui-catalog failed in CI one push later. Step 5 stages both files.
870
+ // Step 4d.5 — regenerate the derived genui catalog (gh#617; gh#2211/ADR-0093
871
+ // repartitioned): each catalog's catalogId embeds the lockstep version
872
+ // (`adia.<scope>@X.Y.Z`), so the bump above just invalidated all five.
873
+ // The v0.8.26 cut skipped this and check:genui-catalog failed in CI one
874
+ // push later. Step 5 stages all five files.
806
875
  console.log('\n Step 4d.5 — regenerate derived genui catalog (catalogId carries the lockstep version):');
807
876
  sh('node scripts/build/derive-genui-catalog.mjs', args);
808
877
 
@@ -907,6 +976,39 @@ function step4PromoteAndBump(args) {
907
976
  }
908
977
  }
909
978
 
979
+ // Pure so the selftest can lock the parse without a real git tree (gh#2473,
980
+ // same "pure so selftest can lock the transform" shape as bump.mjs's
981
+ // bumpPinnedRef). Takes `git status --porcelain` output, returns the tracked
982
+ // paths Step 5.6's guard should block on — worktree status (porcelain
983
+ // column Y, index 1) 'M' or 'D'; 'A'/'?' (new/untracked) are out of scope,
984
+ // see the guard's own comment. Handles the two porcelain path shapes a
985
+ // plain `line.slice(3)` mishandles: C-style-quoted paths (git quotes any
986
+ // path containing a space, or a rename's `->` separator surrounded by
987
+ // spaces) and rename/copy entries (`R old -> new`, `C old -> new`),
988
+ // where the file that actually needs staging is the NEW path.
989
+ function unstagedTrackedLeftovers(porcelainOutput) {
990
+ const leftover = [];
991
+ for (const line of porcelainOutput.split('\n')) {
992
+ if (line.length <= 3 || !/[MD]/.test(line[1])) continue;
993
+ let rest = line.slice(3).trim();
994
+ // Rename/copy: "old -> new" (both sides may independently be quoted) —
995
+ // the worktree-dirty path to report is the NEW (post-rename) side.
996
+ const arrow = rest.indexOf(' -> ');
997
+ if (arrow !== -1) rest = rest.slice(arrow + 4).trim();
998
+ // git C-quotes a path containing a space, a quote, or a control char as
999
+ // a double-quoted, backslash-escaped string — strip the wrapper and
1000
+ // undo the escaping rather than reporting the literal quoted form.
1001
+ if (rest.length >= 2 && rest[0] === '"' && rest[rest.length - 1] === '"') {
1002
+ rest = rest
1003
+ .slice(1, -1)
1004
+ .replace(/\\"/g, '"')
1005
+ .replace(/\\\\/g, '\\');
1006
+ }
1007
+ leftover.push(rest);
1008
+ }
1009
+ return leftover;
1010
+ }
1011
+
910
1012
  // ── Step 5 — Commit ──────────────────────────────────────────────
911
1013
  function step5Commit(args) {
912
1014
  if (args.mode === 'handoff') {
@@ -956,14 +1058,18 @@ function step5Commit(args) {
956
1058
  `docs/ops/releases/v${args.version}.md`,
957
1059
  // gh#617: Step 4d.5 regenerates these post-bump (catalogId embeds the
958
1060
  // lockstep version); unstaged, check:genui-catalog fails in CI (v0.8.26).
959
- // catalog-data.js is the same derivation's third output (the browser-safe
960
- // ES-module projection of base.json/adia-pack.json) check-genui-catalog.mjs
961
- // verifies its freshness too, but this list never carried it (found live
962
- // during the v0.8.33 cut: base.json/adia-pack.json staged and committed
963
- // fine, catalog-data.js sat modified-uncommitted, CI caught the drift a
964
- // fresh checkout exposes that a local working tree doesn't).
965
- 'packages/genui/adia-catalog/base.json',
966
- 'packages/genui/adia-catalog/adia-pack.json',
1061
+ // gh#2211 (ADR-0093): the old base/pack pair retired in favor of five
1062
+ // opt-out-scoped catalogs. catalog-data.js is the same derivation's sixth
1063
+ // output (the browser-safe ES-module projection of all five)
1064
+ // check-genui-catalog.mjs verifies its freshness too, but this list never
1065
+ // carried it (found live during the v0.8.33 cut: the JSON catalogs staged
1066
+ // and committed fine, catalog-data.js sat modified-uncommitted, CI caught
1067
+ // the drift a fresh checkout exposes that a local working tree doesn't).
1068
+ 'packages/genui/adia-catalog/adia.core.json',
1069
+ 'packages/genui/adia-catalog/adia.navigation.json',
1070
+ 'packages/genui/adia-catalog/adia.data.json',
1071
+ 'packages/genui/adia-catalog/adia.agent.json',
1072
+ 'packages/genui/adia-catalog/adia.shells.json',
967
1073
  'packages/genui/adia-catalog/catalog-data.js',
968
1074
  // gh#1361: Step 4d.5b regenerates the catalog tier index post-bump (it
969
1075
  // derives from the catalog derive-genui-catalog.mjs just refreshed) and,
@@ -1015,12 +1121,54 @@ function step5Commit(args) {
1015
1121
  // Codex `agents/openai.yaml` glob above is.
1016
1122
  files.push(`${dir}/plugin.yaml`, `${dir}/__init__.py`, `${dir}/prompts/*.md`);
1017
1123
  }
1018
- if (name === 'adia-ui-factory') files.push(`${dir}/.mcp.json`, `${dir}/hermes-mcp.yaml`);
1124
+ // gh#2473: bump.mjs's PINNED_REFS also rewrites the factory README's
1125
+ // OWN prose echo of the generation-MCP pin (`server pinned:
1126
+ // @adia-ai/mcp@X` — see bump.mjs's A2UI_MCP_PIN comment,
1127
+ // check:plugin-count-claims' "factory README generation-MCP pin"
1128
+ // equality check) — same invariant-8 scope as .mcp.json, but this
1129
+ // list only ever staged .mcp.json + hermes-mcp.yaml, so the README
1130
+ // edit rode along unstaged/uncommitted every cut until Step 5.6
1131
+ // (below) started catching it.
1132
+ if (name === 'adia-ui-factory') files.push(`${dir}/.mcp.json`, `${dir}/hermes-mcp.yaml`, `${dir}/README.md`);
1019
1133
  return files;
1020
1134
  }),
1021
1135
  ];
1022
1136
  sh(`git add ${releaseFiles.join(' ')}`, args);
1023
1137
  sh('git diff --cached --stat | tail -3', args);
1138
+
1139
+ // Step 5.6 — unstaged-tracked-files guard (gh#2473). Numbered 5.6, not
1140
+ // 5.5: cut-procedure.md's own §Step 5.5 already names a DIFFERENT,
1141
+ // pre-existing manual freshness trip-wire (components.mjs --verify +
1142
+ // check:chunks-fresh + check:embeddings-fresh) that this script doesn't
1143
+ // automate — reusing "5.5" here would collide with that doc section for
1144
+ // an operator reading both. This guard slots before §Step 5.7 (the
1145
+ // release PR), the next free number in the doc's own sequence.
1146
+ //
1147
+ // The release commit's file allowlist above has drifted from what
1148
+ // bump.mjs / cut-hygiene actually edit more than once (gh#1198, gh#1899,
1149
+ // gh#1954, gh#2473 itself) — a step upstream starts writing a new file
1150
+ // and nobody remembers to add it here, so it rides through Step 5
1151
+ // uncommitted and either ships a half-bumped release commit or gets
1152
+ // discovered only when a later gate (or a human diff) trips over it.
1153
+ // Fail loud, right here, rather than trusting the allowlist is
1154
+ // exhaustive: after staging everything this list knows about, anything
1155
+ // STILL modified-in-the-worktree on a tracked path means the allowlist
1156
+ // missed a file this cut's bump/hygiene pass touched.
1157
+ if (args.dry) {
1158
+ console.log(" [dry] git status --porcelain (Step 5.6 guard: no unstaged tracked changes remain — Step 5's allowlist covered everything this cut touched)");
1159
+ } else {
1160
+ const leftover = unstagedTrackedLeftovers(shQuiet('git status --porcelain', args));
1161
+ if (leftover.length > 0) {
1162
+ console.error(`\nERROR: ${leftover.length} tracked file(s) modified by this cut are still unstaged after Step 5's allowlist ran:`);
1163
+ for (const f of leftover) console.error(` ✗ ${f}`);
1164
+ console.error('\n bump.mjs (or a cut-hygiene step) edited these but Step 5\'s `releaseFiles`');
1165
+ console.error(' allowlist does not know about them — add the missing path(s) and re-run,');
1166
+ console.error(' or `git add` + `git commit --amend` them onto the release commit by hand.');
1167
+ process.exit(1);
1168
+ }
1169
+ console.log(" ✓ no unstaged tracked changes remain — Step 5's allowlist covered everything this cut touched");
1170
+ }
1171
+
1024
1172
  if (!args.commitMessageFile) {
1025
1173
  console.error('ERROR: --commit-message-file required for cut/from-scratch mode');
1026
1174
  process.exit(2);
@@ -1403,9 +1551,120 @@ async function main() {
1403
1551
  // must never proceed to tag/push/publish on its own). A fixture temp dir
1404
1552
  // supplies the operator-authored content files (--commit-message-file /
1405
1553
  // --gh-notes-file) so nothing here depends on real repo state.
1406
- function selftest() {
1554
+ async function selftest() {
1407
1555
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'release-pack-selftest-'));
1408
1556
  try {
1557
+ // gh#2195 — gate-4 retry: extractFailedTestFiles() must parse vitest's
1558
+ // default-reporter ` FAIL <file> > ...` lines, and
1559
+ // runSoloGateWithRetry() must retry exactly once, scoped to exactly
1560
+ // those files, accepting a clean retry as a host-contention flake but
1561
+ // never masking a failure that reproduces, and never retrying at all
1562
+ // when the failure has no parseable failed-file list.
1563
+ {
1564
+ const sample = [
1565
+ ' FAIL packages/a/b.test.js > describe > one',
1566
+ ' FAIL packages/a/b.test.js > describe > two',
1567
+ ' FAIL packages/c/d.test.js > describe > three',
1568
+ ].join('\n');
1569
+ const files = extractFailedTestFiles(sample);
1570
+ if (files.length !== 2 || !files.includes('packages/a/b.test.js') || !files.includes('packages/c/d.test.js')) {
1571
+ console.error(`selftest FAIL: extractFailedTestFiles() expected 2 deduped files, got: ${JSON.stringify(files)}`);
1572
+ process.exit(1);
1573
+ }
1574
+
1575
+ const flakyMarker = path.join(tmp, 'flaky-invocation-count');
1576
+ const flakyScript = path.join(tmp, 'flaky-once.mjs');
1577
+ fs.writeFileSync(flakyScript, `
1578
+ import { writeFileSync } from 'node:fs';
1579
+ const isRetry = process.argv.slice(2).includes('--');
1580
+ if (!isRetry) {
1581
+ writeFileSync(${JSON.stringify(flakyMarker)}, '1');
1582
+ console.log(' FAIL packages/fake/flaky.test.js > describe > flaky test');
1583
+ console.error('Error: Test timed out in 45000ms.');
1584
+ process.exit(1);
1585
+ }
1586
+ console.log(' retry ran clean');
1587
+ process.exit(0);
1588
+ `);
1589
+ const flakyGate = { n: 4, cmd: `node "${flakyScript}"`, what: 'selftest flaky gate' };
1590
+ const flakyResult = await runSoloGateWithRetry(flakyGate, {});
1591
+ if (!flakyResult.ok || !flakyResult.note || !flakyResult.note.includes('packages/fake/flaky.test.js')) {
1592
+ console.error(`selftest FAIL: runSoloGateWithRetry() must accept a clean retry as a flake; got: ${JSON.stringify(flakyResult)}`);
1593
+ process.exit(1);
1594
+ }
1595
+
1596
+ const alwaysFailsScript = path.join(tmp, 'always-fails.mjs');
1597
+ fs.writeFileSync(alwaysFailsScript, `
1598
+ console.log(' FAIL packages/fake/broken.test.js > describe > broken test');
1599
+ process.exit(1);
1600
+ `);
1601
+ const alwaysFailsGate = { n: 4, cmd: `node "${alwaysFailsScript}"`, what: 'selftest always-fails gate' };
1602
+ const alwaysFailsResult = await runSoloGateWithRetry(alwaysFailsGate, {});
1603
+ if (alwaysFailsResult.ok) {
1604
+ console.error('selftest FAIL: runSoloGateWithRetry() must NOT mask a failure that reproduces on retry');
1605
+ process.exit(1);
1606
+ }
1607
+
1608
+ const sentinel = path.join(tmp, 'unparseable-retried');
1609
+ const unparseableScript = path.join(tmp, 'unparseable-fails.mjs');
1610
+ fs.writeFileSync(unparseableScript, `
1611
+ import { existsSync, writeFileSync } from 'node:fs';
1612
+ const sentinel = ${JSON.stringify(sentinel)};
1613
+ writeFileSync(sentinel, existsSync(sentinel) ? 'RETRIED' : 'FIRST-RUN');
1614
+ console.error('Segmentation fault (core dumped)');
1615
+ process.exit(139);
1616
+ `);
1617
+ const unparseableGate = { n: 4, cmd: `node "${unparseableScript}"`, what: 'selftest unparseable-crash gate' };
1618
+ const unparseableResult = await runSoloGateWithRetry(unparseableGate, {});
1619
+ if (unparseableResult.ok) {
1620
+ console.error('selftest FAIL: an unparseable gate-4 failure must never be treated as passing');
1621
+ process.exit(1);
1622
+ }
1623
+ if (fs.readFileSync(sentinel, 'utf8') !== 'FIRST-RUN') {
1624
+ console.error('selftest FAIL: an unparseable failure (no FAIL lines) must fail closed — no retry');
1625
+ process.exit(1);
1626
+ }
1627
+ }
1628
+
1629
+ // gh#2473 — Step 5.6's guard: unstagedTrackedLeftovers() must find a
1630
+ // plain modified/deleted tracked file, unquote a C-quoted path
1631
+ // (spaces), and report the NEW side of a rename/copy — all from a
1632
+ // fixture porcelain string, independent of `--dry` and independent of
1633
+ // any real git tree (the `--mode cut --dry` subprocess run below can
1634
+ // only ever prove the code path executes and prints a fixed success
1635
+ // string; it can't reach the `else` branch at all, since `sh()`'s dry
1636
+ // short-circuit never runs `git status`). This exercises the exact
1637
+ // logic Step 5.6 runs for real.
1638
+ {
1639
+ const porcelain = [
1640
+ ' M packages/plugins/adia-ui-factory/README.md',
1641
+ ' D packages/plugins/adia-ui-factory/hermes-mcp.yaml',
1642
+ 'M packages/already/staged.json', // staged-only (X=M, Y=space) — NOT a leftover
1643
+ '?? packages/new/untracked.json', // untracked — out of scope, NOT a leftover
1644
+ ' M "packages/has space/quoted file.md"',
1645
+ 'RM packages/old-name.md -> packages/new-name.md', // renamed AND further worktree-modified (Y=M) — a leftover
1646
+ 'R packages/clean-rename-old.md -> packages/clean-rename-new.md', // renamed, no further worktree change (Y=space) — NOT a leftover
1647
+ ].join('\n');
1648
+ const leftover = unstagedTrackedLeftovers(porcelain);
1649
+ const want = [
1650
+ 'packages/plugins/adia-ui-factory/README.md',
1651
+ 'packages/plugins/adia-ui-factory/hermes-mcp.yaml',
1652
+ 'packages/has space/quoted file.md',
1653
+ 'packages/new-name.md',
1654
+ ];
1655
+ const gotJson = JSON.stringify(leftover);
1656
+ const wantMissing = want.filter((w) => !leftover.includes(w));
1657
+ const wantExtra = leftover.filter((l) => !want.includes(l));
1658
+ if (wantMissing.length > 0 || wantExtra.length > 0 || leftover.length !== want.length) {
1659
+ console.error(`selftest FAIL: unstagedTrackedLeftovers() expected ${JSON.stringify(want)}, got ${gotJson}`);
1660
+ process.exit(1);
1661
+ }
1662
+ if (unstagedTrackedLeftovers('').length !== 0) {
1663
+ console.error('selftest FAIL: unstagedTrackedLeftovers() must return [] on empty porcelain output (the clean-tree case Step 5.6 hits on every normal cut)');
1664
+ process.exit(1);
1665
+ }
1666
+ }
1667
+
1409
1668
  const commitFile = path.join(tmp, 'commit.txt');
1410
1669
  const notesFile = path.join(tmp, 'notes.md');
1411
1670
  fs.writeFileSync(commitFile, 'selftest fixture commit message\n');
@@ -1462,6 +1721,16 @@ function selftest() {
1462
1721
  'packages/plugins/adia-ui-factory/__init__.py',
1463
1722
  'packages/plugins/adia-ui-factory/hermes-mcp.yaml',
1464
1723
  'packages/plugins/adia-ui-factory/prompts/*.md',
1724
+ // gh#2473 — Step 5's staging pathspec must carry the factory README's
1725
+ // own prose echo of the generation-MCP pin (bump.mjs's A2UI_MCP_PIN
1726
+ // README target), or it rides unstaged/uncommitted past the release
1727
+ // commit the same way .mcp.json itself did before gh#1198.
1728
+ 'packages/plugins/adia-ui-factory/README.md',
1729
+ // gh#2473 — Step 5.6's unstaged-tracked-files guard must actually run
1730
+ // (as a no-op) on a clean --dry plan. Its real leftover-detection
1731
+ // logic is unit-tested directly above via unstagedTrackedLeftovers();
1732
+ // this marker only proves the --dry code path reaches its console.log.
1733
+ "no unstaged tracked changes remain — Step 5's allowlist covered everything this cut touched",
1465
1734
  ];
1466
1735
  for (const marker of need) {
1467
1736
  if (!out.includes(marker)) {
@@ -1597,7 +1866,10 @@ function fileURLToPathCompat(url) {
1597
1866
  const topArgv = process.argv.slice(2);
1598
1867
  if (topArgv[0] === 'selftest') {
1599
1868
  rl.close();
1600
- selftest();
1869
+ selftest().catch((e) => {
1870
+ console.error(e);
1871
+ process.exit(1);
1872
+ });
1601
1873
  } else {
1602
1874
  main().catch((e) => {
1603
1875
  // sh()/shQuiet() failures already printed the failing command + its
@@ -23,8 +23,8 @@ closable: { type: Boolean, default: true, reflect: true }
23
23
  | --- | --- |
24
24
  | `modal-ui[closable]` (default `true`) | `modal-ui[permanent]` (default `false`) |
25
25
  | `skeleton-ui[animate]` | `skeleton-ui[static]` |
26
- | `stream-ui[cursor]` | `stream-ui[hideCursor]` |
27
- | `chart-ui[average]` | `chart-ui[hideAverage]` |
26
+ | `stream-ui[cursor]` | `stream-ui[noCursor]` |
27
+ | `chart-ui[average]` | `chart-ui[noAverage]` |
28
28
  | `toggle-group-ui[multiple]` | `toggle-group-ui[single]` |
29
29
  | `swiper-ui[pause-on-hover]` | `swiper-ui[noPauseOnHover]` |
30
30
 
@@ -66,8 +66,8 @@ AdiaUI conventions require `default: false` on Boolean props in the standard sha
66
66
  | --- | --- | --- |
67
67
  | Modal can be dismissed | `closable` | `permanent` |
68
68
  | Skeleton animates | `animate` | `static` |
69
- | Stream shows blinking cursor | `cursor` | `hideCursor` |
70
- | Chart shows average line | `average` | `hideAverage` |
69
+ | Stream shows blinking cursor | `cursor` | `noCursor` |
70
+ | Chart shows average line | `average` | `noAverage` |
71
71
  | Toggle group allows multi-select | `multiple` | `single` |
72
72
  | Swiper pauses on hover | `pause-on-hover` | `noPauseOnHover` |
73
73
 
@@ -253,9 +253,22 @@ Reserve `error` for validation state (`[error]` on form inputs matches ARIA patt
253
253
  <toggle-group-ui single>...</toggle-group-ui>
254
254
  ```
255
255
 
256
+ **[historical, 2026-08-31, ADR-0056 amendment]** `<toggle-group-ui>` /
257
+ `<toggle-option-ui>` were cut outright in `0.8.43` (gh#1617), before the
258
+ migration this ADR's Decision 2 anticipated ever ran — moot, not
259
+ falsified. `segmented-ui`/`segment-ui` absorbed the role
260
+ (`segmented.yaml`'s `multiple` prop description names the absorption
261
+ directly, gh#1369/#1363 C1). The example above stays as written because it
262
+ illustrates the Boolean-false naming rule against a real historical prop
263
+ name, not because `<toggle-group-ui>` still exists — `segmented-ui`
264
+ deliberately does NOT follow this same polarity (`[multiple]` is
265
+ positive-polarity by design, matching `select-ui[multiple]`; there is no
266
+ `segmented-ui[single]` opt-out), so it is not a drop-in replacement
267
+ example for this rule.
268
+
256
269
  ## Selection-item state — a declared, reflected `selected` prop, never a private `data-*` stamp
257
270
 
258
- A **selection-item primitive** (one selectable option inside a selection-group parent — `segment-ui` in `segmented-ui`, `toggle-option-ui` in `toggle-group-ui`) exposes its current state as a declared, reflected `selected: Boolean` prop, documented as parent-managed, and styled via `[selected]`:
271
+ A **selection-item primitive** (one selectable option inside a selection-group parent — `segment-ui` in `segmented-ui`; historically also `toggle-option-ui` in `toggle-group-ui`, cut in `0.8.43`/gh#1617 — see the dated note below) exposes its current state as a declared, reflected `selected: Boolean` prop, documented as parent-managed, and styled via `[selected]`:
259
272
 
260
273
  ```javascript
261
274
  // child — the declared API surface
@@ -286,7 +299,7 @@ Rules:
286
299
  - **The parent's `value` is the single source of truth**; the parent writes `selected` on its children (`segmented.class.js:146,149` sets/removes the attribute; `toggle-group.class.js:104-111` assigns `opt.selected`). Authors and generated markup drive the group's `value`, never `[selected]` on a child (`toggle-group.yaml`'s rules block says so in so many words).
287
300
  - **No private `data-*` stamp for API-conceptual state.** `data-selected` hides the state from the yaml SoT, the catalog, and generative authoring — a consumer or the A2UI grammar cannot express "this option is selected" against a stamp that no schema declares. `toggle-group-ui` used to stamp `data-selected` + style `:scope[data-selected]`; it converged onto `segment-ui`'s mechanism (`toggle-option.yaml:38-46`, `toggle-group.css:14`, `toggle-group.test.js:64` asserts the stamp is gone).
288
301
  - **Parent-stamped ARIA stays as-is — ARIA is wiring, not API.** `segment-ui` derives `aria-checked` from `selected` (`segment.class.js:42`); `toggle-option-ui` derives `aria-pressed` (`toggle-group.class.js:65`). The reflected prop is the API; the ARIA attribute follows it.
289
- - **Reference implementation:** `segment-ui` (`segment.yaml:32-36`, `segment.css:7`); `toggle-option-ui` is the converged second instance. A new selection-item primitive copies this shape extending one of the two by analogy is what produced the divergence in the first place (gh#1303).
302
+ - **Reference implementation:** `segment-ui` (`segment.yaml:32-36`, `segment.css:7`) is the current, live reference. **[historical, 2026-08-31, ADR-0056 amendment]** `toggle-option-ui`/`toggle-group-ui` were the converged second instance at ratification time, but both were cut outright in `0.8.43` (gh#1617), before any migration ran — `segmented-ui`/`segment-ui` absorbed the role. A new selection-item primitive copies `segment-ui`'s shape directly; there is no second live instance to extend by analogy anymore (the divergence gh#1303 describes was between `segment-ui` and the now-deleted `toggle-option-ui`).
290
303
 
291
304
  Migration note: removing `data-selected` was a breaking change for external CSS that targeted it, so it shipped in two halves — the additive `selected` prop in `0.8.39`, the stamp removal on the `0.8.40` breaking wave with a migration-guide entry (`packages/web-components/CHANGELOG.md`; the ADR text names the wave `0.9.0` — it shipped as `0.8.40`).
292
305
 
@@ -31,7 +31,7 @@ These rules are the distilled lessons from a 5-iteration audit. Each one corresp
31
31
 
32
32
  ### API / Attributes
33
33
 
34
- 1. **Boolean defaults are `false`.** If the expected default is "on," rename: `closable` → `permanent`, `animate` → `static`, `cursor` → `hideCursor`, `average` → `hideAverage`, `pause-on-hover` → `noPauseOnHover`.
34
+ 1. **Boolean defaults are `false`.** If the expected default is "on," rename: `closable` → `permanent`, `animate` → `static`, `cursor` → `noCursor`, `average` → `noAverage`, `pause-on-hover` → `noPauseOnHover`. `no-*` is the canonical negation prefix (ADR-0063 decision 1) — a new rename lands on `no-*` directly, never `hide-*`.
35
35
 
36
36
  2. **No magic-value sentinels in numeric props.** Indeterminate = `null`, not `-1`. Consumers branch on `value == null`, which is explicit.
37
37
 
@@ -239,7 +239,7 @@ Every Boolean prop on a component must default to `false`. If the expected defau
239
239
  | --------------------------------- | ------------------------------ |
240
240
  | `<drawer-ui permanent>` | `<drawer-ui closable>` |
241
241
  | `<chart-ui static>` | `<chart-ui animate>` |
242
- | `<cursor-ui hide-cursor>` | `<cursor-ui cursor>` |
242
+ | `<cursor-ui no-cursor>` | `<cursor-ui cursor>` |
243
243
  | `<carousel-ui no-pause-on-hover>` | `<carousel-ui pause-on-hover>` |
244
244
 
245
245
  Default behavior is the absent attribute. Attributes exist to opt OUT or carry a value — never to opt INTO the expected default.
@@ -20,10 +20,24 @@ posture flip.
20
20
 
21
21
  ## Where legibility floors live
22
22
 
23
- A `--*-min-width: 20ch`-class floor (the gh#781/gh#1633 pattern on
24
- `input-ui`/`select-ui`/`combobox-ui`) belongs on `:scope[inline]` only, never
25
- on the plain `:scope`. In the fill state the container owns the width, all
26
- the way downno exception, no re-added floor. Two floor classes are
23
+ **[retired 2026-08-28, ADR-0095]** The `--*-min-width: 20ch`-class floor
24
+ described below is no longer permitted anywhere, including on
25
+ `:scope[inline]`. A primitive never floors its own inline-size independent
26
+ of its own geometry the only permitted floor is
27
+ `min-inline-size: var(--<component>-height)` (a square minimum). ADR-0095
28
+ Decision Class 1 removes the 20ch-class field-width floor for
29
+ `input-ui`/`select-ui`/`combobox-ui` (plus `--input-label-min-width`,
30
+ `--date-range-picker-trigger-min-width`) and replaces it with the square
31
+ floor, generalizing the `button-ui` keep-square pattern already named below
32
+ to the field-width components.
33
+
34
+ *(Historical, pre-ADR-0095 rule — superseded by the retirement above, kept
35
+ only for context on what the old convention was.)* A
36
+ `--*-min-width: 20ch`-class floor (the gh#781/gh#1633 pattern on
37
+ `input-ui`/`select-ui`/`combobox-ui`) used to belong on `:scope[inline]`
38
+ only, never on the plain `:scope`. In the fill state the container owns the
39
+ width, all the way down — no exception, no re-added floor. Two floor
40
+ classes are
27
41
  explicitly NOT sizing-posture floors and stay in both states: inner
28
42
  `min-width: 0` flex unclampers (the ellipsis-reset machinery) and
29
43
  `min-width: var(--*-height)` squareness floors (`button-ui`'s keep-square
@@ -35,9 +49,10 @@ posture, and also stay in both states.
35
49
 
36
50
  - Plain `:scope` (no `[inline]`): block-level, `width: auto` in block flow,
37
51
  `width: 100%` in flex/grid parents. No `min-width` legibility floor here.
38
- - `:scope[inline]`: inline-level display, shrink-wrap sizing, and — only if
39
- the component is a floor-carrier (input/select/combobox-class) — the
40
- `--*-min-width: 20ch`-class floor moves here.
52
+ - `:scope[inline]`: inline-level display, shrink-wrap sizing. Per ADR-0095,
53
+ no `--*-min-width: 20ch`-class floor moves here any more — a floor-carrier
54
+ component uses the square minimum (`min-inline-size:
55
+ var(--<component>-height)`) instead, in both states.
41
56
  - A component that wants to own its line but still hug has no attribute for
42
57
  that state (ADR-0077 Decision item 6, accepted consequence) — tell the
43
58
  consumer to use explicit sizing (`width: fit-content` or an explicit
@@ -154,6 +154,11 @@ ADR-0053/0054 don't cover. Any new attribute follows these:
154
154
  opt-out. `chart-legend[static]` retires to `[interactive]` (inverted
155
155
  default). Applies to single-purpose toggles only — `noodles`'s
156
156
  `editable`/`readonly` pair is two orthogonal booleans and stays.
157
+ - **One spelling survives a documented alias; the alias retires
158
+ everywhere it appeared** — never a deprecation window. `alert[dismissible]`
159
+ retired (`[closable]` survives, the implemented prop); `card`'s style
160
+ enum's duplicate `outline` member retired (`outlined` survives, the
161
+ ADR-0044 style-axis token).
157
162
  - **Two sanctioned multi-value encodings, never a hand-rolled delimited
158
163
  string:** (a) `multiple` boolean + a structured option model (`select`'s
159
164
  `options`/`<option>` shape); (b) a native array-typed JS property,
@@ -114,13 +114,93 @@ stricter than the source is a defect, not documentation.
114
114
 
115
115
  Pipeline: `components.mjs` validates the shape per-yaml, cross-checks every
116
116
  referenced name against the full catalog on a full build, and forwards the
117
- lists onto `x-adiaui` → `catalog-a2ui_0_9.json`.
117
+ lists onto `x-adiaui` → `catalog-a2ui_1_0.json`.
118
118
  `scripts/build/derive-genui-catalog.mjs` then translates them into the
119
- canonical v1.0 key space (yaml `Segmented` → catalog `SegmentedControl`) on
120
- `base.json`/`adia-pack.json`, where the vendored `@genui/core` validator
121
- enforces them (`UNALLOWED_PARENT`/`UNALLOWED_CHILD`). Module-tier yamls
122
- (web-modules) carry constraints as dialect-catalog metadata only — modules
123
- have no v1.0 sidecar.
119
+ canonical v1.0 key space (yaml `Segmented` → catalog `Segmented`, canonical
120
+ per gh#2116) across the five opt-out-scoped catalogs (`adia.core.json`,
121
+ `adia.navigation.json`, `adia.data.json`, `adia.agent.json`,
122
+ `adia.shells.json`, gh#2211/ADR-0093), where the vendored `@genui/core`
123
+ validator enforces them (`UNALLOWED_PARENT`/`UNALLOWED_CHILD`). Module-tier
124
+ yamls (web-modules) now carry a v1.0 sidecar too and land in `adia.shells`.
125
+
126
+ ---
127
+
128
+ ## `examples:` field — a2ui example ids (semantic-id grammar, gh#2492)
129
+
130
+ Each `examples[].a2ui` block is a JSON array of component nodes (the same
131
+ `updateComponents.components[]` shape `.claude/docs/specs/a2ui-editor.md`
132
+ documents for the editor's live doc store). That spec's line "other
133
+ component ids are free-form (convention: `c-{n}` for generated ids)" governs
134
+ **editor-generated** ids only — ids the editor mints when a human drags a
135
+ component onto the canvas. It was never a license for **authored** ids
136
+ inside a component's own yaml examples, and treating it as one produced a
137
+ corpus-wide drift toward cryptic, positional ids (`q`, `k1`, `k1v`, `hdr`)
138
+ that don't describe what they are once an example has more than one or two
139
+ nodes — reported in gh#2492 against `blockquote.yaml`, `badge.yaml`,
140
+ `aside.yaml`, `alert.yaml`, and `anchor-bar.yaml` (the last four
141
+ corroborating it as a corpus-wide pattern, not a one-off).
142
+
143
+ **Grammar — authored `a2ui.examples[]` ids only:**
144
+
145
+ - **Kebab-case, role-descriptive.** The id names what the node IS or DOES in
146
+ the example, not its position in the array. `quote-body`, not `q` or
147
+ `node-2`.
148
+ - **Unique per example.** Scoped to one `examples[]` entry, not the whole
149
+ yaml — reusing `header` across two examples in the same file is fine;
150
+ reusing an id twice inside one example is not (the renderer's flat
151
+ `children: string[]` lookup would collide).
152
+ - **Compound ids read parent-then-role** for a node that belongs to a named
153
+ cluster: `kpi-revenue`, `kpi-revenue-value`, `kpi-revenue-label` — not
154
+ `k1`, `k1v`, `k1l`. This is what lets a reader studying the copy-paste-able
155
+ example understand the structure from the ids alone, without cross-
156
+ referencing the tree.
157
+ - **Exception — the root/wrapper id may stay short when the example has
158
+ exactly one top-level container and the short id is still a real word**,
159
+ e.g. `card`, `row`, `panel`, as long as every id it contains follows the
160
+ grammar. A single generic wrapper doesn't need `card-wrapper-root`; a
161
+ wrapper's *children* still do.
162
+ - Ids are internal wiring keys (`children: string[]` references, rendered
163
+ only as `data-a2ui-id`) — never user-visible copy — but they double as the
164
+ readable structure of the example a consumer studies, which is the whole
165
+ reason this grammar exists.
166
+
167
+ **Before / after** (`blockquote.yaml`'s `default` example, gh#2492's
168
+ reported repro):
169
+
170
+ ```jsonc
171
+ // before
172
+ [
173
+ { "id": "q", "component": "Blockquote", "cite": "…", "children": ["body"] },
174
+ { "id": "body", "component": "Text", "textContent": "Stay hungry. Stay foolish." }
175
+ ]
176
+
177
+ // after
178
+ [
179
+ { "id": "quote", "component": "Blockquote", "cite": "…", "children": ["quote-body"] },
180
+ { "id": "quote-body", "component": "Text", "textContent": "Stay hungry. Stay foolish." }
181
+ ]
182
+ ```
183
+
184
+ ```jsonc
185
+ // before (badge.yaml chart-dashboard, positional style mixed with semantic
186
+ // ids in the SAME example — k1/k1h/k1v alongside header-row/dash-title)
187
+ { "id": "k1", "component": "Card", "children": ["k1h", "k1v"] }
188
+
189
+ // after
190
+ { "id": "kpi-revenue", "component": "Card", "children": ["kpi-revenue-label", "kpi-revenue-value"] }
191
+ ```
192
+
193
+ **Enforcement**: `scripts/verify/check-example-ids.mjs` walks every yaml's
194
+ `examples[].a2ui` nodes and reports ids that fail the grammar (a bare
195
+ 1-2 char id, a `^[a-z]\d+` positional pattern like `k1`/`c2`, non-kebab-case,
196
+ or a within-example duplicate). gh#2492 Phase 2 swept the corpus-wide 526
197
+ violations across 48 files to zero and flipped the gate to `npm run
198
+ check:example-ids:strict` in the `npm run check` chain (non-zero exit on
199
+ any violation) — a new example that violates the grammar now fails the
200
+ build immediately, not just on a future sweep. Known gap: the checker's
201
+ positional-pattern regex catches `letter+digits` (`k1`, `c2`) but not a
202
+ `letter-digit-letter-digit` chain like `g1i1` — out of scope for this
203
+ sweep (nothing flagged it), left for a future refinement.
124
204
 
125
205
  ---
126
206
 
@@ -235,7 +315,7 @@ manifest format. ADR-0066 refines it three ways:
235
315
 
236
316
  - **`origin: primitive | module` on every L0 entry** — both YAML source
237
317
  roots (web-components primitives AND web-modules composites) compile into
238
- the same `catalog-a2ui_0_9.json` through one shared contract;
318
+ the same `catalog-a2ui_1_0.json` through one shared contract;
239
319
  `derive-catalog-tiers.mjs` stamps which root an entry came from onto
240
320
  `tier-index.json`. A module is legitimately a member of TWO rungs at two
241
321
  grains: its component API (props/events/slots) is L0, its assembled shell
@@ -283,6 +363,31 @@ props:
283
363
  - ghost
284
364
  ```
285
365
 
366
+ ### Synthesized universal props — `slot` / `hidden` / `ariaLive` / `traits`
367
+
368
+ `deriveProps()` (`scripts/build/derive-genui-catalog.mjs`) prepends three
369
+ props to every component's generated catalog schema before it ever reads a
370
+ yaml's own `props:` block: `slot: {type: string}`, `hidden: {type:
371
+ boolean}`, `ariaLive: {type: string}` (REQ-013 accessibility pair, gh#1353).
372
+ **No yaml SoT declares any of these** — they're synthesized, not authored,
373
+ and a sidecar that ever DOES declare one of these keys itself simply
374
+ overwrites the synthesized definition.
375
+
376
+ **Decided but not yet implemented:** ADR-0097
377
+ (`docs/ops/adr/adr-0097-traits-as-a2ui-synthesized-universal-prop.md`,
378
+ ratified via PR #2524) rules that `traits: {type: string}` becomes a fourth
379
+ member of this synthesized set, carrying the same space-separated grammar
380
+ as the existing HTML `[traits="…"]` declarative attribute
381
+ (`.claude/docs/specs/traits.md`, "Method 3" — `"ripple confetti-burst"`,
382
+ not a JSON array) — making `traits` legal on every component's A2UI wire
383
+ schema with no renderer change, and, once wired, checked by
384
+ `catalog-validator.js` against the live trait registry
385
+ (`packages/web-components/traits/_catalog.json`) with a **hard FAIL on an
386
+ unknown trait name** (the same rejection behavior the raw HTML mechanism
387
+ already has). As of this writing `deriveProps()` still synthesizes only the
388
+ three props above and `catalog-validator.js` has no trait-name check —
389
+ gh#2513 tracks the build.
390
+
286
391
  ### `required: true` field
287
392
 
288
393
  **When to use**: only for props where omitting them makes the component meaningless or inaccessible.
@@ -427,7 +532,7 @@ The build:
427
532
  **Downstream of the sidecars — two more derived artifacts (gh#970's release-PR stop):**
428
533
 
429
534
  ```bash
430
- node scripts/build/derive-genui-catalog.mjs # genui base.json + adia-pack.json derive FROM the sidecars
535
+ node scripts/build/derive-genui-catalog.mjs # genui's five opt-out-scoped catalogs derive FROM the sidecars
431
536
  npm run check:genui-catalog # the drift gate that fails PR CI if you skip the above
432
537
  ```
433
538