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

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.
Files changed (44) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +40 -0
  4. package/README.md +1 -1
  5. package/__init__.py +5 -0
  6. package/commands/demo-audit.md +1 -1
  7. package/commands/gen-ui-review.md +1 -1
  8. package/commands/package-release.md +1 -1
  9. package/commands/site-deployment.md +1 -1
  10. package/package.json +1 -1
  11. package/plugin.yaml +1 -1
  12. package/prompts/demo-audit.md +1 -1
  13. package/prompts/gen-ui-review.md +1 -1
  14. package/prompts/package-release.md +1 -1
  15. package/prompts/site-deployment.md +1 -1
  16. package/scripts/forge-lint.mjs +168 -0
  17. package/scripts/lint-rules.generated.mjs +1700 -0
  18. package/scripts/site-postwrite-derivation-gate +23 -127
  19. package/skills/a2ui-maintenance/SKILL.md +1 -1
  20. package/skills/a2ui-maintenance/references/data-model-reactivity.md +49 -29
  21. package/skills/a2ui-maintenance/references/pipeline-overview.md +58 -22
  22. package/skills/a2ui-maintenance/references/surface-lifecycle.md +14 -7
  23. package/skills/component-md-authoring/SKILL.md +116 -0
  24. package/skills/component-md-authoring/agents/openai.yaml +3 -0
  25. package/skills/demo-audit/references/visual-probe-triage.md +4 -1
  26. package/skills/gen-ui-review/SKILL.md +4 -1
  27. package/skills/gen-ui-review/references/loop-protocol.md +6 -5
  28. package/skills/package-release/references/changelog-discipline.md +6 -3
  29. package/skills/package-release/references/cut-procedure.md +61 -14
  30. package/skills/package-release/references/gates-catalog.md +6 -2
  31. package/skills/package-release/references/recovery-paths.md +6 -3
  32. package/skills/package-release/scripts/gate-roster.mjs +10 -7
  33. package/skills/package-release/scripts/release-pack.mjs +289 -17
  34. package/skills/primitive-authoring/references/anti-patterns.md +2 -2
  35. package/skills/primitive-authoring/references/api-contract.md +17 -4
  36. package/skills/primitive-authoring/references/authoring-cycle.md +1 -1
  37. package/skills/primitive-authoring/references/code-style.md +1 -1
  38. package/skills/primitive-authoring/references/common-gotchas.md +6 -6
  39. package/skills/primitive-authoring/references/form-control-sizing.md +22 -7
  40. package/skills/primitive-authoring/references/token-contract.md +5 -0
  41. package/skills/primitive-authoring/references/yaml-contract.md +132 -14
  42. package/skills/site-docs-authoring/SKILL.md +7 -9
  43. package/hooks/hooks.json +0 -44
  44. package/scripts/forge-lint +0 -315
@@ -1,88 +1,40 @@
1
1
  #!/usr/bin/env python3
2
2
  """site-postwrite-derivation-gate — PostToolUse check: site source edits name their stale derivatives.
3
3
 
4
- The docs site renders converted routes from GENERATED artifacts: site/site.js
5
- probes site-a2ui/pages/<slug>.a2ui.json per route and renders it via
6
- <a2ui-root>.doc the HTML fragment under site/pages/** is only the 404
7
- fallback. An editor who changes the fragment and stops there has changed
8
- NOTHING users see (shipped exactly that way 2026-07, caught only by manual
9
- review). site/sitemap.json likewise feeds site/llms.txt and the patterns
10
- index. This hook feeds the regeneration obligation back at write-time; the
11
- hard gate is the repo-wide site-a2ui verify wired in CI.
12
-
13
- Quiet on: fragments with no sitemap route, routes the ledger has not
14
- converted (the fragment IS what renders), anything under site-a2ui/ and the
15
- generated pages sidecar-prewrite-guard owns, and any internal error
16
- (fail-open — this hook must never block editing with a crash).
4
+ site/sitemap.json feeds GENERATED surfaces (site/llms.txt, the patterns/
5
+ templates index) that go stale the moment a route or content path changes
6
+ without a regen. This hook feeds that regeneration obligation back at
7
+ write-time.
8
+
9
+ (Before ADR-0072 Decision 2 / gh#2410, this hook also flagged
10
+ site/pages/**/*.html fragment edits whose route the site-a2ui ledger
11
+ marked "converted" the docs site rendered such routes from a GENERATED
12
+ site-a2ui/pages/<slug>.a2ui.json artifact, so a fragment-only edit changed
13
+ nothing users saw. That render path retired; every route now renders
14
+ straight from its fragment, so a fragment edit is never stale on its own
15
+ and needs no nudge.)
17
16
 
18
17
  Usage:
19
18
  site-postwrite-derivation-gate --hook # PostToolUse mode: event JSON on stdin
20
- site-postwrite-derivation-gate selftest # prove matcher + resolution on fixtures
19
+ site-postwrite-derivation-gate selftest # prove matcher on fixtures
21
20
  """
22
21
  import json
23
22
  import sys
24
23
 
25
24
  SITEMAP_MARKER = "site/sitemap.json"
26
- FRAGMENT_MARKER = "site/pages/"
27
-
28
- # Generated files under site/pages/ are sidecar-prewrite-guard territory.
29
- GENERATED_EXCLUDE = ("site/pages/patterns/index.html",)
30
25
 
31
26
 
32
27
  def classify(path):
33
- """→ ("sitemap"|"fragment", repo-root prefix, "/"-rooted repo path) or (None, None, None)."""
34
- if not path or "site-a2ui/" in path:
35
- return None, None, None
36
- if path.endswith(SITEMAP_MARKER):
37
- return "sitemap", path[: -len(SITEMAP_MARKER)], "/" + SITEMAP_MARKER
38
- idx = path.find(FRAGMENT_MARKER)
39
- if idx == -1 or not path.endswith(".html") or path[idx:] in GENERATED_EXCLUDE:
40
- return None, None, None
41
- return "fragment", path[:idx], "/" + path[idx:]
42
-
43
-
44
- def find_route(sitemap, rel):
45
- """Walk sections/items for the entry whose `content` equals rel; → route path or None."""
46
- stack = [sitemap.get("sections") or []]
47
- while stack:
48
- node = stack.pop()
49
- if isinstance(node, list):
50
- stack.extend(node)
51
- elif isinstance(node, dict):
52
- if node.get("content") == rel and node.get("path"):
53
- return node["path"]
54
- stack.extend(v for k, v in node.items() if k in ("sections", "items", "children"))
28
+ """→ "sitemap" if `path` is site/sitemap.json, else None."""
29
+ if path and path.endswith(SITEMAP_MARKER):
30
+ return "sitemap"
55
31
  return None
56
32
 
57
33
 
58
- def resolve_fragment(root, rel):
59
- """→ feedback reason for a stale-artifact edit, or None to stay quiet.
60
-
61
- Raises on I/O / parse trouble — the caller fail-opens.
62
- """
63
- with open(root + "site/sitemap.json", encoding="utf-8") as f:
64
- route = find_route(json.load(f), rel)
65
- if not route:
66
- return None # not a routed page
67
- with open(root + "site-a2ui/ledger.json", encoding="utf-8") as f:
68
- rows = json.load(f).get("pages") or []
69
- row = next((r for r in rows if r.get("route") == route), None)
70
- if not row or row.get("status") != "converted":
71
- return None # legacy-rendered: the fragment IS what users see
72
- slug = row.get("slug") or route.lstrip("/").replace("/", "__")
73
- return (
74
- f"site-postwrite-derivation-gate · {rel} is the SOURCE for {route}, which renders "
75
- f"from GENERATED site-a2ui/pages/{slug}.a2ui.json — the artifact is now stale and "
76
- f"users still see the OLD content. Regenerate: `node scripts/build/site-a2ui.mjs "
77
- f"--page {route}` (repo-wide check: `npm run verify:site-a2ui`)."
78
- )
79
-
80
-
81
34
  SITEMAP_REASON = (
82
35
  "site-postwrite-derivation-gate · site/sitemap.json feeds GENERATED surfaces that are "
83
36
  "now stale: site/llms.txt (`npm run build:llms`) and the patterns/templates index "
84
- "(`npm run build:patterns-index`). If you changed routes or content paths, also verify "
85
- "site-a2ui route coverage: `node scripts/build/site-a2ui.mjs --verify`."
37
+ "(`npm run build:patterns-index`)."
86
38
  )
87
39
 
88
40
 
@@ -92,80 +44,24 @@ def hook_mode():
92
44
  except Exception:
93
45
  return 0
94
46
  tool_input = event.get("tool_input") or {}
95
- kind, root, rel = classify(tool_input.get("file_path") or "")
96
- if not kind:
97
- return 0
98
- if kind == "sitemap":
47
+ if classify(tool_input.get("file_path") or "") == "sitemap":
99
48
  print(json.dumps({"decision": "block", "reason": SITEMAP_REASON}))
100
- return 0
101
- try:
102
- reason = resolve_fragment(root, rel)
103
- except Exception:
104
- return 0 # fail-open: never block editing on our own error
105
- if reason:
106
- print(json.dumps({"decision": "block", "reason": reason}))
107
49
  return 0
108
50
 
109
51
 
110
52
  def selftest():
111
- import os
112
- import tempfile
113
-
114
53
  scope_cases = [
115
- ("/repo/site/pages/architecture/ontology.html", "fragment"),
116
- ("site/pages/guides/testing.html", "fragment"),
117
54
  ("/repo/site/sitemap.json", "sitemap"),
118
- ("/repo/site/pages/patterns/index.html", None), # generated: prewrite guard owns it
119
- ("/repo/site-a2ui/pages/site__x__y.a2ui.json", None),
120
- ("/repo/site-a2ui/ledger.json", None),
55
+ ("/repo/site/pages/architecture/ontology.html", None),
56
+ ("site/pages/guides/testing.html", None),
57
+ ("/repo/site/pages/patterns/index.html", None),
121
58
  ("/repo/site/site.js", None),
122
59
  ("/repo/site/pages/architecture/notes.txt", None),
123
60
  ("/repo/apps/tasks/app/index.html", None),
124
61
  ]
125
62
  for path, expected in scope_cases:
126
- if classify(path)[0] != expected:
127
- print(f"selftest: FAIL scope {path} → {classify(path)[0]} expected {expected}")
128
- return 1
129
- with tempfile.TemporaryDirectory() as tmp:
130
- root = tmp + os.sep
131
- os.makedirs(root + "site")
132
- os.makedirs(root + "site-a2ui")
133
- with open(root + "site/sitemap.json", "w", encoding="utf-8") as f:
134
- json.dump(
135
- {
136
- "sections": [
137
- {
138
- "items": [
139
- {"path": "/site/a/one", "content": "/site/pages/a/one.html"},
140
- {"path": "/site/a/two", "content": "/site/pages/a/two.html"},
141
- ]
142
- }
143
- ]
144
- },
145
- f,
146
- )
147
- with open(root + "site-a2ui/ledger.json", "w", encoding="utf-8") as f:
148
- json.dump(
149
- {
150
- "pages": [
151
- {"route": "/site/a/one", "slug": "site__a__one", "status": "converted"},
152
- {"route": "/site/a/two", "slug": "site__a__two", "status": "failed"},
153
- ]
154
- },
155
- f,
156
- )
157
- resolve_cases = [
158
- ("/site/pages/a/one.html", True), # converted → nudge
159
- ("/site/pages/a/two.html", False), # ledger row not converted → quiet
160
- ("/site/pages/a/three.html", False), # no sitemap entry → quiet
161
- ]
162
- for rel, should_fire in resolve_cases:
163
- fired = resolve_fragment(root, rel) is not None
164
- if fired != should_fire:
165
- print(f"selftest: FAIL resolve {rel} fired={fired} expected={should_fire}")
166
- return 1
167
- if "site__a__one.a2ui.json" not in resolve_fragment(root, "/site/pages/a/one.html"):
168
- print("selftest: FAIL reason does not name the stale artifact")
63
+ if classify(path) != expected:
64
+ print(f"selftest: FAIL scope {path} → {classify(path)} expected {expected}")
169
65
  return 1
170
66
  print("selftest: PASS")
171
67
  return 0
@@ -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,65 @@
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 — retired 2026-08-31]** That trigger fired and was
55
+ ruled: retirement, not re-point (Decision 2). Passive attrition never
56
+ netted the promoted set down (355 pages and growing at last measurement —
57
+ regen work kept adding rows faster than breakage retired them), so the
58
+ operator converted it to an active drain (gh#2410): the mechanism —
59
+ builder, ledger, artifact tree, gates, hook — is gone. The docs site now
60
+ renders every route through the legacy template path only; site-a2ui is
61
+ no longer part of this pipeline. The dialect side's regression-corpus
62
+ coverage site-a2ui once provided (real-content-scale exercise of the
63
+ dialect renderer, the ADR-0061 lifecycle path, and the engine transpiler)
64
+ has no standing replacement — see ADR-0072 Decision 2 / gh#2410 for the
65
+ closure record.
35
66
 
36
67
  All paths repo-relative. Specs worth reading before structural changes:
37
68
  `.claude/docs/specs/a2ui-v0.9-catalog-guide.md` (protocol + catalog format),
@@ -151,6 +182,11 @@ for any constant or decision lives in git and PR descriptions
151
182
  currently fails, mostly a schema-generation gap around `data-*`/`span`
152
183
  attributes, not corpus-content defects — see the ticket before
153
184
  assuming a chunk is actually broken).
185
+ 13. **Every generated catalog schema carries three synthesized universal
186
+ props today** (`slot`/`hidden`/`ariaLive`, none declared in any yaml
187
+ SoT); ADR-0097 rules a fourth, `traits`, but that part is decided-not-
188
+ yet-shipped (gh#2513) — see `primitive-authoring/references/
189
+ yaml-contract.md` §Synthesized universal props for the full contract.
154
190
 
155
191
  ## Test + run commands (all verified in root package.json)
156
192
 
@@ -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."
@@ -1,12 +1,15 @@
1
1
  # Mode 1 — Component visual probe: probe classes + triage
2
2
 
3
+ `<plugin-root>` below is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's installed directory
4
+ in Codex.
5
+
3
6
  Detection layers, deepest last:
4
7
 
5
8
  1. `npm run dogfood:visual-probe` — baseline per-page bar: no 4xx/5xx, no
6
9
  console JS errors, non-zero body rect, ≥1 upgraded custom-element host.
7
10
  Artifacts land in `qa/findings/visual-probe-<DATE>/`.
8
11
  2. Bundled deep analyzer —
9
- `node "${CLAUDE_PLUGIN_ROOT}/skills/demo-audit/scripts/analyze.mjs"`.
12
+ `node "<plugin-root>/skills/demo-audit/scripts/analyze.mjs"`.
10
13
  Resolves the target checkout from `$ADIA_REPO_ROOT`, else cwd — always run
11
14
  from (or point it at) the monorepo checkout, never the plugin install dir.
12
15
  Flags: `--filter <slug>` · `--port N` (default 5173) · `--out PATH`
@@ -77,7 +77,10 @@ only that file, never the raw DOM/canvas/gallery-latest.json.** The
77
77
  structural prompt-injection defense. Data model:
78
78
  [loop-protocol](references/loop-protocol.md).
79
79
 
80
- ## Scripts (`node ${CLAUDE_PLUGIN_ROOT}/skills/gen-ui-review/scripts/…`, from monorepo root)
80
+ ## Scripts (`node <plugin-root>/skills/gen-ui-review/scripts/…`, from monorepo root)
81
+
82
+ `<plugin-root>` is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's installed directory in
83
+ Codex.
81
84
 
82
85
  | Command | Purpose |
83
86
  | --- | --- |