@adia-ai/adia-ui-forge 0.8.56 → 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 (31) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +16 -0
  4. package/README.md +1 -1
  5. package/commands/demo-audit.md +1 -1
  6. package/commands/gen-ui-review.md +1 -1
  7. package/commands/package-release.md +1 -1
  8. package/commands/site-deployment.md +1 -1
  9. package/package.json +1 -1
  10. package/plugin.yaml +1 -1
  11. package/prompts/demo-audit.md +1 -1
  12. package/prompts/gen-ui-review.md +1 -1
  13. package/prompts/package-release.md +1 -1
  14. package/prompts/site-deployment.md +1 -1
  15. package/scripts/forge-lint.mjs +168 -0
  16. package/scripts/lint-rules.generated.mjs +1700 -0
  17. package/scripts/site-postwrite-derivation-gate +23 -127
  18. package/skills/a2ui-maintenance/references/pipeline-overview.md +12 -10
  19. package/skills/demo-audit/references/visual-probe-triage.md +4 -1
  20. package/skills/gen-ui-review/SKILL.md +4 -1
  21. package/skills/gen-ui-review/references/loop-protocol.md +6 -5
  22. package/skills/package-release/references/changelog-discipline.md +6 -3
  23. package/skills/package-release/references/cut-procedure.md +15 -13
  24. package/skills/package-release/references/gates-catalog.md +5 -2
  25. package/skills/package-release/references/recovery-paths.md +6 -3
  26. package/skills/package-release/scripts/gate-roster.mjs +10 -7
  27. package/skills/primitive-authoring/references/common-gotchas.md +6 -6
  28. package/skills/primitive-authoring/references/yaml-contract.md +27 -14
  29. package/skills/site-docs-authoring/SKILL.md +7 -9
  30. package/hooks/hooks.json +0 -44
  31. 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
@@ -51,16 +51,18 @@ ruling was load-bearing on `wireFormat` defaulting to `'dialect'`, with a
51
51
  **Named expiry**: the flag-flip ADR that makes `'v1'` the shipping default
52
52
  had to re-rule site-a2ui's fitness (re-point vs retirement-by-attrition).
53
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.
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, hookis 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.
64
66
 
65
67
  All paths repo-relative. Specs worth reading before structural changes:
66
68
  `.claude/docs/specs/a2ui-v0.9-catalog-guide.md` (protocol + catalog format),
@@ -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
  | --- | --- |
@@ -1,8 +1,9 @@
1
1
  # Loop Protocol — one full review cycle
2
2
 
3
3
  Five phases per prompt; human QA gate at cycle close; Phase 5 runs for FAILING
4
- prompts only. Scripts ship in this skill at
5
- `${CLAUDE_PLUGIN_ROOT}/skills/gen-ui-review/scripts/` and are run from the
4
+ prompts only. `<plugin-root>` below is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's
5
+ installed directory in Codex. Scripts ship in this skill at
6
+ `<plugin-root>/skills/gen-ui-review/scripts/` and are run from the
6
7
  monorepo root (they read `apps/genui/…/gallery-latest.json` and write the
7
8
  `review/` tree there). Corpus-pattern doctrine consumed by Phase 5:
8
9
  [corpus-html-patterns.md](corpus-html-patterns.md).
@@ -79,7 +80,7 @@ primitive lookup (`TAG_TO_COMPONENT`, the authoritative table), attr
79
80
  sanitization, overflow gate:
80
81
 
81
82
  ```text
82
- node ${CLAUDE_PLUGIN_ROOT}/skills/gen-ui-review/scripts/gen-review-decompose.mjs
83
+ node <plugin-root>/skills/gen-ui-review/scripts/gen-review-decompose.mjs
83
84
  --cycle N [--group <slug>] [--prompt <slug>] [--port 5300] [--settle 2500] [--dry-run]
84
85
  ```
85
86
 
@@ -190,7 +191,7 @@ never get fix plans. Reads ONLY the decomposed file.
190
191
  5. **Schema gate** (must exit 0 before touching the ledger):
191
192
 
192
193
  ```bash
193
- node ${CLAUDE_PLUGIN_ROOT}/skills/gen-ui-review/scripts/validate-cycle-scores.mjs --cycle N --strict
194
+ node <plugin-root>/skills/gen-ui-review/scripts/validate-cycle-scores.mjs --cycle N --strict
194
195
  ```
195
196
 
196
197
  6. **Update ledger** (`review/cycle-ledger.json`): `cycleNumber`,
@@ -203,7 +204,7 @@ never get fix plans. Reads ONLY the decomposed file.
203
204
  7. **Exit condition**:
204
205
 
205
206
  ```bash
206
- node ${CLAUDE_PLUGIN_ROOT}/skills/gen-ui-review/scripts/gen-review-status.mjs --check-exit
207
+ node <plugin-root>/skills/gen-ui-review/scripts/gen-review-status.mjs --check-exit
207
208
  ```
208
209
 
209
210
  Exit 0 → `status: COMPLETE`; exit 1 → `status: OPEN` (the script lists the
@@ -1,5 +1,8 @@
1
1
  # `changelog-discipline.md` — Keep-a-Changelog mechanics + F-N1 enrichment
2
2
 
3
+ `<plugin-root>` below is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's installed directory
4
+ in Codex.
5
+
3
6
  > Load whenever a cut touches CHANGELOGs (always for author-from-scratch; for a
4
7
  > handoff only if F-N1 warns). The monorepo uses Keep-a-Changelog per package;
5
8
  > the cut **promotes** `## [Unreleased]` into `## [vX.Y.Z] — YYYY-MM-DD`.
@@ -18,7 +21,7 @@
18
21
  The heading swap is all that happens; content under it stays:
19
22
 
20
23
  ```bash
21
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/promote-unreleased.mjs" \
24
+ node "<plugin-root>/skills/package-release/scripts/promote-unreleased.mjs" \
22
25
  --version 0.X.Y --date YYYY-MM-DD --packages web-components,web-modules,a2ui/corpus
23
26
  ```
24
27
 
@@ -70,7 +73,7 @@ Why this recurs: cross-package sweeps leave 1–3 incidental touches (a docstrin
70
73
  ## §Stubs — ride-along lockstep
71
74
 
72
75
  ```bash
73
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/insert-stub.mjs" \
76
+ node "<plugin-root>/skills/package-release/scripts/insert-stub.mjs" \
74
77
  --version 0.X.Y --date YYYY-MM-DD \
75
78
  --substantive "<one-line> in @adia-ai/<pkg>" \
76
79
  --xref "packages/web-modules/CHANGELOG.md#0XY--YYYY-MM-DD" \
@@ -111,7 +114,7 @@ This is cut-procedure §Step 4f: the SAME matcher F-N1 uses at tag time (every r
111
114
  **Recovery — a warn AFTER tagging** (Step 4f skipped, or an interleaved merge added uncovered changes): the release commit is already merged via PR, so `--amend` is not possible —
112
115
 
113
116
  1. Run `--pending-version X.Y.Z --fix`; `git add` the CHANGELOGs; `git commit -m "fix(release): F-N1 enrichment — vX.Y.Z"`; push the branch → PR → CI → merge.
114
- 2. The SHA moved: `node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/tag-lockstep.mjs" --version X.Y.Z --delete`, then re-tag at `main`'s new post-merge HEAD.
117
+ 2. The SHA moved: `node "<plugin-root>/skills/package-release/scripts/tag-lockstep.mjs" --version X.Y.Z --delete`, then re-tag at `main`'s new post-merge HEAD.
115
118
  3. Re-run F-N1; expect per-package clean (umbrella error stays, ignored). ONE recovery round — if a second warn appears, the cause is upstream (find what keeps merging into the window), not another enrichment.
116
119
 
117
120
  ## §Dating and anchors
@@ -1,5 +1,8 @@
1
1
  # `cut-procedure.md` — the standard lockstep cut
2
2
 
3
+ `<plugin-root>` below is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's installed directory
4
+ in Codex.
5
+
3
6
  > Load for any class-A lockstep cut (cut & ship · author from scratch · deploy
4
7
  > handoff). Companions: [`gates-catalog.md`](gates-catalog.md) (gate roster +
5
8
  > failure routing), [`changelog-discipline.md`](changelog-discipline.md) (promotion
@@ -13,7 +16,7 @@ Two entry variants, converging at Step 5:
13
16
  - **Variant A — deploy handoff:** a peer pre-cut the release commit + CHANGELOG + bump + lockfile. Re-baseline, verify, run **Step 4e** (release docs + team notes — the peer's cut may not have generated them, and the pretag gate blocks Step 6 without them), then resume at Step 6 (tag).
14
17
  - **Variant B — author from scratch:** source landed under `## [Unreleased]` with no bump. Do Step 4 (promotion + bump + lockfile), then the full tail.
15
18
 
16
- `` `${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/release-pack.mjs` `` (bundled) mechanizes the sequence in two phases per invariant 3 — the cut modes stop at the release commit (PR → CI → merge happens between), `--mode handoff` runs tag→publish→deploy from post-merge main. **Run with `--go` for an operator-initiated release** (single authorization, §below); the steps below are the manual/diagnostic form and run straight through on the same one-go model.
19
+ `` `<plugin-root>/skills/package-release/scripts/release-pack.mjs` `` (bundled) mechanizes the sequence in two phases per invariant 3 — the cut modes stop at the release commit (PR → CI → merge happens between), `--mode handoff` runs tag→publish→deploy from post-merge main. **Run with `--go` for an operator-initiated release** (single authorization, §below); the steps below are the manual/diagnostic form and run straight through on the same one-go model.
17
20
 
18
21
  | Step | Action | Mutates? |
19
22
  | --- | --- | --- |
@@ -78,7 +81,6 @@ If `git diff <prev-tag>..HEAD --name-only` touches component yaml / `.a2ui.json`
78
81
  ```bash
79
82
  node scripts/build/components.mjs # catalog + per-component sidecars
80
83
  node scripts/build/generate-examples-md.mjs # .examples.md from .examples.html
81
- node scripts/build/site-a2ui.mjs --stale # /site/components/* converted rows
82
84
  npm run harvest:chunks # chunk corpus from site/apps/playgrounds/catalog
83
85
  npm run check:embeddings-fresh # ← run this FIRST; if it passes, SKIP the rebuild below
84
86
  npm run build:embeddings:chunks # ONLY if the line above failed (needs OPENAI_API_KEY)
@@ -89,8 +91,8 @@ npm run build:bundles # dist CSS+JS bundles
89
91
 
90
92
  **The `data-chunk-*` qualifier was the trap** (gh#421). Two separate cuts lost a CI round-trip to it:
91
93
 
92
- - **v0.8.14** edited two components' `*.examples.html` — not yaml, not `.contents.html`, no annotation — so it read as out of scope. It isn't: `.examples.html` feeds **three** generators (`.examples.md`, the `site-a2ui` converted rows, and the chunk harvest's source hashes). Every freshness gate this doc named came back clean, and CI still failed on `🔴 stale /site/components/{menu,popover}` plus `2 source file(s) changed since harvest`.
93
- - **The 0.8.16 cycle** then added a brand-new `site/pages/guides/theming.html` with *no* annotations, ran `site-a2ui`, `check:links`, `verify:llms`, and `verify:patterns-index` — all clean — and CI failed anyway: `1 source file(s) NEW since harvest`.
94
+ - **v0.8.14** edited two components' `*.examples.html` — not yaml, not `.contents.html`, no annotation — so it read as out of scope. It isn't: `.examples.html` feeds **two** generators (`.examples.md` and the chunk harvest's source hashes; a third, the retired site-a2ui converted rows, applied historically). Every freshness gate this doc named came back clean, and CI still failed on `🔴 stale /site/components/{menu,popover}` plus `2 source file(s) changed since harvest`.
95
+ - **The 0.8.16 cycle** then added a brand-new `site/pages/guides/theming.html` with *no* annotations, ran `check:links`, `verify:llms`, and `verify:patterns-index` — all clean — and CI failed anyway: `1 source file(s) NEW since harvest`.
94
96
 
95
97
  Root cause of both: `check-chunks-fresh` hashes **every** source file under the harvest globs and compares the set against the corpus record, so a new or changed file trips it whether or not the harvester extracts a chunk from it. The annotation governs what gets *harvested*, never what gets *hashed*. Hence the trigger list above names the directories, not the annotation.
96
98
 
@@ -121,7 +123,7 @@ npm run check:links # 15 intra-repo links
121
123
  npm run eval:diff -- --engine zettel # 16 eval floors
122
124
  npm run dogfood:status # 17 P0/P1 dogfood floor (static-only under npm-ci; run once more under bootstrap layout for full coverage, gh#1359)
123
125
  npm run check:examples-md-fresh # 18 .examples.md vs .examples.html
124
- npm run verify:site-a2ui # 19 site-a2ui rows vs source fragments
126
+ # gate 19 (verify:site-a2ui) retired with the site-a2ui mechanism ADR-0072 Decision 2 / gh#2410
125
127
  npm run verify:contrast # 20 WCAG AA — canvas-text AND text-on-fill
126
128
  npm run check:token-semantics-sync # 21 token-selection generated refs vs token sources
127
129
  npm run check:demo-routes # 22 demo surfaces routed + patterns indexed
@@ -154,9 +156,9 @@ Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source,
154
156
 
155
157
  **Run 4a whenever a hand-authored `## [Unreleased]` section is still sitting uncommitted, not only on a strict Variant B.** `release-pack.mjs --mode cut` (a peer's pre-staged content, not yet promoted) needs it exactly as much as `--mode from-scratch` does — the v0.8.4 near-miss was `--mode cut` skipping this step entirely because the doc (and the script) only associated promotion with "from scratch". Both modes now run it and both hard-fail before the bump if any roster package still carries non-empty `[Unreleased]` content afterward.
156
158
 
157
- **4a. Promote** `## [Unreleased]` → `## [vX.Y.Z] — YYYY-MM-DD` per package (`` `${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/promote-unreleased.mjs` ``); author fresh blocks for changed-but-unlogged packages; stub the pure ride-alongs (`` `${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/insert-stub.mjs` ``). Classification recipe + shapes: [`changelog-discipline.md`](changelog-discipline.md).
159
+ **4a. Promote** `## [Unreleased]` → `## [vX.Y.Z] — YYYY-MM-DD` per package (`` `<plugin-root>/skills/package-release/scripts/promote-unreleased.mjs` ``); author fresh blocks for changed-but-unlogged packages; stub the pure ride-alongs (`` `<plugin-root>/skills/package-release/scripts/insert-stub.mjs` ``). Classification recipe + shapes: [`changelog-discipline.md`](changelog-discipline.md).
158
160
 
159
- **4b. Bump.** PATCH vs MINOR: **MINOR is reserved for API-surface breaks only** (removed/renamed prop, attribute, slot, event, token, or tag). Visible behavior changes, re-scalings, and opt-in features stay PATCH; a CHANGELOG bullet saying "(MINOR behavior change)" is prose, not a semver directive. Unqualified "bump version" = PATCH; don't round-trip to ask. `node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/bump.mjs" --from X.Y.Z-1 --to X.Y.Z`. On a MINOR cut, also bump the internal `@adia-ai/*` `^ranges` separately (bump.mjs touches `"version"` fields only) — and a MINOR cut owes a MIGRATION GUIDE section ([`migration-guide-authoring.md`](migration-guide-authoring.md)).
161
+ **4b. Bump.** PATCH vs MINOR: **MINOR is reserved for API-surface breaks only** (removed/renamed prop, attribute, slot, event, token, or tag). Visible behavior changes, re-scalings, and opt-in features stay PATCH; a CHANGELOG bullet saying "(MINOR behavior change)" is prose, not a semver directive. Unqualified "bump version" = PATCH; don't round-trip to ask. `node "<plugin-root>/skills/package-release/scripts/bump.mjs" --from X.Y.Z-1 --to X.Y.Z`. On a MINOR cut, also bump the internal `@adia-ai/*` `^ranges` separately (bump.mjs touches `"version"` fields only) — and a MINOR cut owes a MIGRATION GUIDE section ([`migration-guide-authoring.md`](migration-guide-authoring.md)).
160
162
 
161
163
  **4c. Lockfile.** `npm install --package-lock-only --no-audit --no-fund` — must land in the release commit. The publish workflows open with `npm ci`, which hard-fails on a version/lockfile mismatch: a bump without the regenerated lockfile passes locally and breaks **every** publish at clean-install.
162
164
 
@@ -224,10 +226,10 @@ stub sections exist leaves it nothing to append to, and the gap resurfaces
224
226
  as F-N1 warns at the push boundary, costing a tag move:
225
227
 
226
228
  ```bash
227
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/insert-stub.mjs" \
229
+ node "<plugin-root>/skills/package-release/scripts/insert-stub.mjs" \
228
230
  --version X.Y.Z --date YYYY-MM-DD --previous-version X.Y.Z-1 \
229
231
  --substantive "<one-line>" --xref "<anchor>" --packages <missing-stubs> # 4a-stub — FIRST, only the missing ones (hard-errors on existing sections)
230
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/bump.mjs" --from X.Y.Z-1 --to X.Y.Z # 4b (skip if versions already moved)
232
+ node "<plugin-root>/skills/package-release/scripts/bump.mjs" --from X.Y.Z-1 --to X.Y.Z # 4b (skip if versions already moved)
231
233
  npm install --package-lock-only --no-audit --no-fund # 4c
232
234
  npm run check:lockstep # 4d
233
235
  node scripts/build/derive-genui-catalog.mjs # 4d.5 — catalogId carries the bumped version (gh#617)
@@ -296,7 +298,7 @@ unresolved AND no review requests changes; any other state stops with the
296
298
  evidence, never force-merges):
297
299
 
298
300
  ```bash
299
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/pr-bridge.mjs" \
301
+ node "<plugin-root>/skills/package-release/scripts/pr-bridge.mjs" \
300
302
  --branch "release/vX.Y.Z" --title "release: vX.Y.Z lockstep" --body-file <path>
301
303
  ```
302
304
 
@@ -344,7 +346,7 @@ Stage its CHANGELOG edits into the release commit (the Step-5 allowlist already
344
346
  Log the planned tag list (evidence table above), then tag **at `main`'s post-merge HEAD** (post-bump fixes belong in the tarball; the window's last merge is the tag point — exception: batch push tags each version at its own release-merge SHA):
345
347
 
346
348
  ```bash
347
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/tag-lockstep.mjs" \
349
+ node "<plugin-root>/skills/package-release/scripts/tag-lockstep.mjs" \
348
350
  --version X.Y.Z # umbrella vX.Y.Z + 10 <pkg>-vX.Y.Z (8 npm + 2 plugins)
349
351
  ```
350
352
 
@@ -370,7 +372,7 @@ a 14-package roster (`agent`, `persona` and `a2ui-protocol-mcp` missing), which
370
372
  is three packages that would simply never publish.
371
373
 
372
374
  ```bash
373
- PKGS=$(node -e "import('${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/package-paths.mjs')
375
+ PKGS=$(node -e "import('<plugin-root>/skills/package-release/scripts/package-paths.mjs')
374
376
  .then(m => console.log(m.PACKAGE_ROSTER.filter(p => p.lockstep !== false).map(p => p.name).join(' ')))")
375
377
  echo "$PKGS" # log it — this IS the tag list evidence
376
378
 
@@ -385,7 +387,7 @@ git -C "$REPO" push origin vX.Y.Z # umbrella last; triggers nothing
385
387
  Log the current registry snapshot (per-package versions + `dist-tags.latest`) before dispatching. For batch pushes, verify ordering against that snapshot: **oldest version publishes and settles first** — `npm dist-tag latest` is set by publish order. Then:
386
388
 
387
389
  ```bash
388
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/dispatch-publish.mjs" \
390
+ node "<plugin-root>/skills/package-release/scripts/dispatch-publish.mjs" \
389
391
  --version X.Y.Z --verify-triggered # re-dispatches missing/dead runs; registry-gated (gh#763)
390
392
  ```
391
393
 
@@ -1,5 +1,8 @@
1
1
  # `gates-catalog.md` — pre-flight gate roster + failure → recovery map
2
2
 
3
+ `<plugin-root>` below is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's installed directory
4
+ in Codex.
5
+
3
6
  > Load for a verify-only run or on any gate failure during a cut. Maps every
4
7
  > release-flow gate × what it checks × typical failure × recovery. Gates are
5
8
  > grouped by **failure category** — how the operator routes when one goes red —
@@ -16,7 +19,7 @@ Row layout per gate: **What** · **Typical failure** · **Recovery**.
16
19
 
17
20
  - **What:** all 10 lockstep `@adia-ai/*` packages declare the same `version` (the class-B `adia-plugins` package is excluded — `lockstep: false`, `scripts/package-paths.mjs`); internal `@adia-ai/*` dep ranges match policy (`^X.Y.0` during PATCH cycles, bumped at MINOR).
18
21
  - **Typical failure:** one package forgot to bump; a peer edited an internal range mid-PATCH; a `^0.0.x` range slipped in.
19
- - **Recovery:** version drift → `` `${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/bump.mjs` ``; range drift → `npm run check:lockstep:fix` auto-aligns, then re-run.
22
+ - **Recovery:** version drift → `` `<plugin-root>/skills/package-release/scripts/bump.mjs` ``; range drift → `npm run check:lockstep:fix` auto-aligns, then re-run.
20
23
  - **Why `^0.0.x` is forbidden:** npm pre-1.0 semver only widens the caret when major+minor aren't both zero — `^0.0.6` resolves to `>=0.0.6 <0.0.7`, locked to exactly 0.0.6. An internal dep pinned that way silently installs a *stale* sibling on every fresh `npm i` (this shipped a real ~4-day-latent bug before the lockstep policy). The `^X.Y.0` floor (Y≥1) widens correctly across patches; trust the gate, don't reason about caret semantics by hand. Moot at 1.0.0.
21
24
 
22
25
  ### `node scripts/release/check-release.mjs --all-pending` (F-N1, the release trip-wire)
@@ -212,7 +215,7 @@ npm run check:demo-shells
212
215
 
213
216
  Add `verify:corpus` + `check:embeddings-fresh` if chunks were touched; `check:lightningcss-build` if CSS was touched; F-N1 if unpushed release tags exist.
214
217
 
215
- **Full pre-cut sweep** — the 30-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3, sourced from `` `${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/gate-roster.mjs` `` (the ONE list; `release-pack.mjs` imports and runs it in full — a subset run is impossible without editing that file). ~90s wall time.
218
+ **Full pre-cut sweep** — the 30-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3, sourced from `` `<plugin-root>/skills/package-release/scripts/gate-roster.mjs` `` (the ONE list; `release-pack.mjs` imports and runs it in full — a subset run is impossible without editing that file). ~90s wall time.
216
219
 
217
220
  **Omnibus** — `npm run check` invokes everything. Heavy; use when re-baselining a stale checkout.
218
221
 
@@ -1,5 +1,8 @@
1
1
  # `recovery-paths.md` — the 8 recovery scenarios
2
2
 
3
+ `<plugin-root>` below is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's installed directory
4
+ in Codex.
5
+
3
6
  > Load on any F-N1 / pre-flight failure, for a batch push, or for post-release
4
7
  > recovery. Each scenario: the **shape** (what the repo state looks like), the
5
8
  > **resolution** (commands + judgment calls), and what to record. Every scenario
@@ -48,7 +51,7 @@ Ambiguous → surface it, don't guess.
48
51
 
49
52
  **Shape:** source + CHANGELOG entries landed under `## [Unreleased]`, no bump, no release commit.
50
53
 
51
- **Resolution:** [`cut-procedure.md`](cut-procedure.md) Variant B; `` `${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/promote-unreleased.mjs` `` mechanizes the heading swap; fresh blocks per [`changelog-discipline.md`](changelog-discipline.md) §Authoring.
54
+ **Resolution:** [`cut-procedure.md`](cut-procedure.md) Variant B; `` `<plugin-root>/skills/package-release/scripts/promote-unreleased.mjs` `` mechanizes the heading swap; fresh blocks per [`changelog-discipline.md`](changelog-discipline.md) §Authoring.
52
55
 
53
56
  ## §Scenario 4 — `[Unreleased]` extension (early cut + entangled fix)
54
57
 
@@ -80,11 +83,11 @@ Ambiguous → surface it, don't guess.
80
83
  **Resolution:**
81
84
 
82
85
  ```bash
83
- node "${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/dispatch-publish.mjs" \
86
+ node "<plugin-root>/skills/package-release/scripts/dispatch-publish.mjs" \
84
87
  --version X.Y.Z --verify-triggered # re-dispatches packages with no run OR a dead (cancelled/failed/timed-out) run; registry-gated; idempotent
85
88
  ```
86
89
 
87
- For a batch, preserve npm-latest ordering (`--after <prev>`). Verify against the registry, not the workflows. **Prevention:** push tags one-at-a-time ([`cut-procedure.md`](cut-procedure.md) §Step 8); `` `${CLAUDE_PLUGIN_ROOT}/skills/package-release/scripts/release-pack.mjs` `` does this automatically and follows with `--verify-triggered`.
90
+ For a batch, preserve npm-latest ordering (`--after <prev>`). Verify against the registry, not the workflows. **Prevention:** push tags one-at-a-time ([`cut-procedure.md`](cut-procedure.md) §Step 8); `` `<plugin-root>/skills/package-release/scripts/release-pack.mjs` `` does this automatically and follows with `--verify-triggered`.
88
91
 
89
92
  ## §Scenario 8 — Cut on the wrong branch
90
93
 
@@ -56,14 +56,17 @@ export const GATE_ROSTER = [
56
56
  // worktree.mjs && npm run dogfood:status` once under the pnpm layout too
57
57
  // before cutting (cut-procedure.md §3.1 states the per-gate layout need).
58
58
  { n: 17, cmd: 'npm run dogfood:status', what: 'P0/P1 dogfood floor (static-only under npm-ci; run once more under bootstrap layout for full coverage, gh#1359)' },
59
- // gh#421: these four live in the `npm run check` aggregate but were never
60
- // in the pre-cut roster, so a gate added to CI silently never reached a
61
- // cut. examples-md + site-a2ui are the two generators an `.examples.html`
62
- // edit invalidates (the v0.8.14 cut lost a CI round-trip to exactly that);
63
- // contrast is the AA gate that gh#427 widened from 42 to 90 pairs;
64
- // token-semantics-sync keeps the token-selection pack's generated half honest.
59
+ // gh#421: these live in the `npm run check` aggregate but were never in
60
+ // the pre-cut roster, so a gate added to CI silently never reached a cut.
61
+ // examples-md is the generator an `.examples.html` edit invalidates (the
62
+ // v0.8.14 cut lost a CI round-trip to exactly that); contrast is the AA
63
+ // gate that gh#427 widened from 42 to 90 pairs; token-semantics-sync keeps
64
+ // the token-selection pack's generated half honest.
65
+ // (Gate 19 was `verify:site-a2ui` — retired with the site-a2ui mechanism
66
+ // itself, ADR-0072 Decision 2 / gh#2410. Number left unassigned rather
67
+ // than renumbering 20+ — gate numbers are cited as identities in shipped
68
+ // release notes, same discipline gate 29's note states.)
65
69
  { n: 18, cmd: 'npm run check:examples-md-fresh', what: '.examples.md vs .examples.html' },
66
- { n: 19, cmd: 'npm run verify:site-a2ui', what: 'site-a2ui converted rows vs source fragments' },
67
70
  { n: 20, cmd: 'npm run verify:contrast', what: 'WCAG AA — canvas-text + text-on-fill pairs' },
68
71
  { n: 21, cmd: 'npm run check:token-semantics-sync', what: 'token-selection generated references vs token sources' },
69
72
  // Operator directive 2026-07-27: demo surfaces must be discoverable —
@@ -16,7 +16,7 @@ Composite authors: read §§1–5 BEFORE Phase 3 sketch. Anyone adding an async
16
16
  4. [minmax(min, 1fr) inside repeat() fighting container queries](#4-minmaxmin-1fr-inside-repeat-fighting-container-queries)
17
17
  5. [Nested `<!-- ... -->` inside design-plan canonical-sketch fenced blocks](#5-nested----inside-design-plan-canonical-sketch-fenced-blocks)
18
18
  6. [Async load/render function completing out of order](#6-async-loadrender-function-completing-out-of-order--a-guard-at-the-checkpoint-isnt-enough)
19
- 7. [Minting a wrapper-shaped component before its registry.js entry lands](#7-minting-a-wrapper-shaped-component-before-its-registryjs-entry-lands--site-a2ui-silently-deletes-the-node-not-just-mis-types-it)
19
+ 7. [Minting a wrapper-shaped component before its registry.js entry lands](#7-minting-a-wrapper-shaped-component-before-its-registryjs-entry-lands--the-transpiler-silently-deletes-the-node-not-just-mis-types-it)
20
20
 
21
21
  ---
22
22
 
@@ -123,15 +123,15 @@ async #loadContent(route) {
123
123
 
124
124
  ---
125
125
 
126
- ## 7. Minting a wrapper-shaped component before its registry.js entry lands — site-a2ui silently deletes the node, not just mis-types it
126
+ ## 7. Minting a wrapper-shaped component before its registry.js entry lands — the transpiler silently deletes the node, not just mis-types it
127
127
 
128
- **Pattern**: a tag is gated first and only, for `*-ui` tags, by `packages/gen-ui/a2ui/registry.js`'s hand-maintained `registry` map — inverted into `reverseRegistry` at `transpiler-maps.js`'s module init, consulted first thing in `compose/transpiler/transpiler.js:149-150`. `registry.js` is hand-edited, not generated by `node scripts/build/components.mjs` (that script writes sidecars/prop-catalog data, consumed only for prop-extraction fidelity on tags the transpiler ALREADY resolved — `transpiler-maps.js:22-26`); a runtime `registerType()` call doesn't rescue a stale row either — `reverseRegistry` is a one-time init snapshot, not live. Regenerate a site-a2ui row for a demo using a component minted in the SAME change, before its `registry.js` line lands, and the tag falls through to `transpiler.js`'s "Unknown → Column" branch (line 180-183) — same mechanism as gh#535's toolbar-group breakage, which at least rendered visibly-wrong. A NEW component is usually wrapper-shaped (one child, author-defined attributes like `anchor="bottom"` the transpiler doesn't map to any real A2UI prop). That shape trips a SECOND, separate rule right after — "single-child container chains flatten" (`transpiler.js:282-285`): a retyped Column with exactly one child and zero recognized props is discarded outright, and its child is spliced directly into the PARENT's children in its place. The wrapper's own id and node are never pushed to the tree at all — not visible-but-wrong, just gone. The row is then internally self-consistent (content hash matches source) so `check:chunks-fresh`/`verify:site-a2ui` both report clean.
128
+ **Pattern**: a tag is gated first and only, for `*-ui` tags, by `packages/gen-ui/a2ui/registry.js`'s hand-maintained `registry` map — inverted into `reverseRegistry` at `transpiler-maps.js`'s module init, consulted first thing in `compose/transpiler/transpiler.js:149-150`. `registry.js` is hand-edited, not generated by `node scripts/build/components.mjs` (that script writes sidecars/prop-catalog data, consumed only for prop-extraction fidelity on tags the transpiler ALREADY resolved — `transpiler-maps.js:22-26`); a runtime `registerType()` call doesn't rescue a stale row either — `reverseRegistry` is a one-time init snapshot, not live. Transpile a demo using a component minted in the SAME change, before its `registry.js` line lands (e.g. the chunk harvester, `node scripts/build/harvest-chunks.mjs`, or any other engine-transpiler consumer), and the tag falls through to `transpiler.js`'s "Unknown → Column" branch (line 180-183) — same mechanism as gh#535's toolbar-group breakage, which at least rendered visibly-wrong. A NEW component is usually wrapper-shaped (one child, author-defined attributes like `anchor="bottom"` the transpiler doesn't map to any real A2UI prop). That shape trips a SECOND, separate rule right after — "single-child container chains flatten" (`transpiler.js:282-285`): a retyped Column with exactly one child and zero recognized props is discarded outright, and its child is spliced directly into the PARENT's children in its place. The wrapper's own id and node are never pushed to the tree at all — not visible-but-wrong, just gone. The row is then internally self-consistent (content hash matches source) so `check:chunks-fresh` reports clean.
129
129
 
130
- **Example**: minting `anchor-bar-ui` (gh#495, PR #569) and regenerating the `bulk-action-toolbar` pattern's site-a2ui row before the worktree's `registry.js` entry for it existed. Git-verified on the pre-fix commit (`ebf71832d`): `site-a2ui/pages/site__patterns__bulk-action-toolbar.a2ui.json` contains zero occurrences of `pat-bulk-float-bar` (the anchor-bar-ui's own authored id) anywhere — not retyped-and-visible, genuinely absent — while its single child (the toolbar content) survived, reparented one level up. `verify:site-a2ui` reported 321/321 clean.
130
+ **Example (historical — the illustrating consumer has since retired):** minting `anchor-bar-ui` (gh#495, PR #569) and regenerating the `bulk-action-toolbar` pattern's site-a2ui row (site-a2ui itself retired 2026-08-31, ADR-0072 Decision 2 / gh#2410 — the underlying registry-gating hazard below is unchanged, only that particular consumer is gone) before the worktree's `registry.js` entry for it existed. Git-verified on the pre-fix commit (`ebf71832d`): the converted artifact contained zero occurrences of `pat-bulk-float-bar` (the anchor-bar-ui's own authored id) anywhere — not retyped-and-visible, genuinely absent — while its single child (the toolbar content) survived, reparented one level up. The site-a2ui freshness gate of the day reported clean regardless, for the exact reason the Detector below still explains.
131
131
 
132
- **Detector**: none generic — `verify:site-a2ui`'s freshness check can't catch this (the row IS fresh relative to its source, it transpiled correctly against a registry that was itself incomplete). The only catch is rendering the actual route in a browser and confirming the new tag's node count is nonzero, or re-running `node scripts/build/site-a2ui.mjs --page <route>` (or `--stale`) after `registry.js` is updated and diffing the row for the new component name. A non-wrapper-shaped new component (multiple children, or attributes that happen to map to real props) is lower-risk here — it survives as a visible-but-wrong Column, the gh#535 class, which at least has a visual tell.
132
+ **Detector**: none generic — a same-source freshness check can't catch this (the artifact IS fresh relative to its source, it transpiled correctly against a registry that was itself incomplete). The only catch is rendering the actual consuming surface and confirming the new tag's node count is nonzero, or re-running the transpile after `registry.js` is updated and diffing the output for the new component name. A non-wrapper-shaped new component (multiple children, or attributes that happen to map to real props) is lower-risk here — it survives as a visible-but-wrong Column, the gh#535 class, which at least has a visual tell.
133
133
 
134
- **Fix**: the `registry.js` entry is what gates resolution — land it (not just run `components.mjs`, which is necessary for prop fidelity but not sufficient to avoid the retype) before regenerating any site-a2ui row that uses the new tag. When gating a dispatched agent's PR that did this out of order, re-run `node scripts/build/site-a2ui.mjs --page <route>` on the merged tree and browser-probe the tag actually appears — never trust `verify:site-a2ui`'s green alone for a route touching a component minted in the same change.
134
+ **Fix**: the `registry.js` entry is what gates resolution — land it (not just run `components.mjs`, which is necessary for prop fidelity but not sufficient to avoid the retype) before transpiling anything that uses the new tag. When gating a dispatched agent's PR that did this out of order, re-run the transpile on the merged tree and confirm the tag actually appears in the output — never trust a freshness gate's green alone for a surface touching a component minted in the same change.
135
135
 
136
136
  **Generalizes to**: any hand-maintained resolution map (not build-generated) that a later regeneration step reads through — regenerating before the map is updated produces an internally-consistent-but-wrong artifact that passes a same-source freshness check; if the misresolved shape also happens to trip a downstream simplification/collapse rule, the failure escalates from "renders wrong" to "renders nothing," with no visual tell at all.
137
137