@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/__init__.py +5 -0
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/skills/a2ui-maintenance/SKILL.md +1 -1
- package/skills/a2ui-maintenance/references/data-model-reactivity.md +49 -29
- package/skills/a2ui-maintenance/references/pipeline-overview.md +56 -22
- package/skills/a2ui-maintenance/references/surface-lifecycle.md +14 -7
- package/skills/component-md-authoring/SKILL.md +116 -0
- package/skills/component-md-authoring/agents/openai.yaml +3 -0
- package/skills/package-release/references/cut-procedure.md +46 -1
- package/skills/package-release/references/gates-catalog.md +1 -0
- package/skills/package-release/scripts/release-pack.mjs +289 -17
- package/skills/primitive-authoring/references/anti-patterns.md +2 -2
- package/skills/primitive-authoring/references/api-contract.md +17 -4
- package/skills/primitive-authoring/references/authoring-cycle.md +1 -1
- package/skills/primitive-authoring/references/code-style.md +1 -1
- package/skills/primitive-authoring/references/form-control-sizing.md +22 -7
- package/skills/primitive-authoring/references/token-contract.md +5 -0
- package/skills/primitive-authoring/references/yaml-contract.md +113 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adia-ui-kit-forge",
|
|
3
|
-
"version": "0.8.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 (
|
|
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
|
|
23
|
-
|
|
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
|
|
57
|
+
## What converged (P1-P4, complete)
|
|
53
58
|
|
|
54
|
-
`path-pointer.js` (gh#1763)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`/name/length` on a string
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 —
|
|
90
|
+
## Consumption mechanism — decided (build-time copy)
|
|
80
91
|
|
|
81
|
-
`@adia-ai/a2ui` is a zero-runtime-deps package (ADR-0048 posture)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
8
|
-
through `packages/genui/wire-bridge/`.
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
gains nothing, the dialect wire format is byte-identical
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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).
|
|
@@ -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/
|
|
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
|
|
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
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
// check:genui-catalog failed in CI one
|
|
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
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
'
|
|
966
|
-
'packages/genui/adia-catalog/adia
|
|
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
|
-
|
|
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[
|
|
27
|
-
| `chart-ui[average]` | `chart-ui[
|
|
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` | `
|
|
70
|
-
| Chart shows average line | `average` | `
|
|
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
|
|
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`)
|
|
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` → `
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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-
|
|
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 `
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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-
|
|
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
|
|
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
|
|