@adia-ai/adia-ui-forge 0.8.3 → 0.8.5

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 (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/agents/framework-verifier.md +13 -5
  4. package/bin/release-pretag-docs-gate +226 -0
  5. package/hooks/hooks.json +10 -0
  6. package/package.json +1 -1
  7. package/skills/adia-a2ui/SKILL.md +10 -2
  8. package/skills/adia-a2ui/references/pipeline-overview.md +1 -1
  9. package/skills/adia-author/SKILL.md +1 -0
  10. package/skills/adia-author/references/anti-patterns.md +7 -7
  11. package/skills/adia-author/references/api-contract.md +9 -9
  12. package/skills/adia-author/references/authoring-cycle.md +6 -6
  13. package/skills/adia-author/references/code-style.md +4 -4
  14. package/skills/adia-author/references/lifecycle-patterns.md +8 -8
  15. package/skills/adia-author/references/token-contract.md +5 -5
  16. package/skills/adia-author/references/worked-example.md +16 -16
  17. package/skills/adia-author/references/yaml-contract.md +1 -1
  18. package/skills/adia-deploy/SKILL.md +44 -0
  19. package/skills/adia-deploy/evals/routing-corpus.json +146 -0
  20. package/skills/adia-deploy/references/deploy-playbooks.md +1 -1
  21. package/skills/adia-dogfood/SKILL.md +64 -11
  22. package/skills/adia-dogfood/evals/routing-corpus.json +146 -0
  23. package/skills/adia-gen-review/SKILL.md +55 -0
  24. package/skills/adia-gen-review/evals/routing-corpus.json +146 -0
  25. package/skills/adia-llm-internals/evals/routing-corpus.json +146 -0
  26. package/skills/adia-llm-internals/references/adapter-contract.md +1 -1
  27. package/skills/adia-llm-internals/references/add-a-provider.md +1 -0
  28. package/skills/adia-llm-internals/references/bridge-facade.md +1 -1
  29. package/skills/adia-llm-internals/references/browser-proxy-boundary.md +8 -6
  30. package/skills/adia-llm-internals/references/model-registry.md +1 -1
  31. package/skills/adia-release/SKILL.md +12 -5
  32. package/skills/adia-release/evals/evals.json +24 -0
  33. package/skills/adia-release/references/changelog-discipline.md +4 -4
  34. package/skills/adia-release/references/cut-procedure.md +45 -11
  35. package/skills/adia-release/references/gates-catalog.md +3 -3
  36. package/skills/adia-release/references/recovery-paths.md +4 -2
  37. package/skills/adia-release/scripts/bump.mjs +32 -1
  38. package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
  39. package/skills/adia-release/scripts/gate-roster.mjs +71 -0
  40. package/skills/adia-release/scripts/insert-stub.mjs +35 -1
  41. package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
  42. package/skills/adia-release/scripts/release-pack.mjs +284 -53
  43. package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
  44. package/skills/adia-ssr/SKILL.md +110 -0
  45. package/skills/adia-ssr/evals/audit-report.md +63 -0
  46. package/skills/adia-ssr/evals/routing-corpus.json +138 -0
  47. package/skills/adia-ssr/references/consumer-workarounds.md +63 -0
  48. package/skills/adia-ssr/references/failure-shapes.md +122 -0
  49. package/skills/adia-ssr/references/guard-patterns.md +142 -0
  50. package/skills/adia-ssr/references/status-ledger.md +35 -0
  51. package/skills/adia-ssr/references/test-without-linkedom.md +113 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-forge",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
package/CHANGELOG.md CHANGED
@@ -1,4 +1,24 @@
1
1
  # Changelog — adia-forge
2
+
3
+ ## [0.8.5] — 2026-07-17
4
+
5
+ ### Fixed
6
+ - **`adia-ssr` skill's gh#284 status corrected — narrowed, not fixed, not open** (`skills/adia-ssr/`, 2026-07-17): verified against 0.8.4 that every component named in the issue (`nav-ui`, `admin-shell`, `admin-sidebar`, `text-ui`, `badge-ui`, `avatar-ui`) has `static template = () => null`, so `connectedCallback`'s destructive `stamp()` never runs against their light-DOM children — a framework-wide survey (150 components) found every component WITH a non-null template derives content from properties only, never light-DOM children. Zero shipped components are exposed to the originally-reported symptom. Rather than patching `stamp()`/`connectedCallback` (rejected as disproportionate — changes every primitive's render lifecycle for a currently-zero-instance risk), shipped a forward-looking static audit, `scripts/dev/audit-template-child-conflict.mjs` (in `@adia-ai/web-components`, wired into `adia-author`'s structural-gate sequence), that flags a FUTURE component reintroducing the conflict shape. `SKILL.md`'s worked example, `references/failure-shapes.md` §2, `references/guard-patterns.md` §2, `references/status-ledger.md`, and `references/consumer-workarounds.md` all updated — the worked example previously answered "yes, known issue" for the exact `<text-ui>` case that no longer reproduces, which would have misled a future consult.
7
+
8
+ ### Added
9
+ - **`adia-ssr` knowledge skill** (`skills/adia-ssr/`: `SKILL.md`, `references/{failure-shapes,guard-patterns,consumer-workarounds,status-ledger,test-without-linkedom}.md`, `evals/{routing-corpus.json,audit-report.md}`; PR #293): NEW consumer-facing skill answering SSR-support questions for AdiaUI — the gh#284 narrowing above is this skill's flagship finding, corrected before the skill's first ship rather than after.
10
+ - **gh#268 campaign W1+W2 (forge estate)** — adia-release periphery surgery (`skills/adia-release/scripts/{promote-unreleased,bump,tag-lockstep,insert-stub,release-pack,dispatch-publish}.mjs`, `references/cut-procedure.md`: promote-unreleased in cut mode + the unpromoted-[Unreleased] guard; --yes scoped to checkpoint 1 with dedicated --push/--publish; evidence-first reordered checkpoints; site deploy via workflow dispatch, raw rsync gone; --mode batch hard-rejects; the 17-gate roster data-driven from `scripts/gate-roster.mjs`; retired --amend teachings replaced with the PR→retag flow); selftests on all 6 release scripts + check-release + the embeddings gate (content-hash freshness) (`skills/adia-release/evals/evals.json`), wired into CI; adia-author's fictional `Adia*` grammar (24 occurrences) and nonexistent-token worked example corrected to the real `UI*`/token surface (`skills/adia-author/references/{anti-patterns,worked-example,api-contract,authoring-cycle,code-style,lifecycle-patterns,token-contract,yaml-contract}.md`); adia-a2ui's corpus manifest now written by the harvest + gated, eval floors re-grounded to their gate scripts, reserved engine list corrected (also in AGENTS.md) (`skills/adia-a2ui/SKILL.md`, `references/pipeline-overview.md`); adia-dogfood scope reconciled with its aggregator (Mode 7) and the perl -i auto-fix rows made BUILDER-ONLY with the framework-verifier seat's mutating-Bash prohibition (generator≠critic) (`skills/adia-dogfood/SKILL.md`, `agents/framework-verifier.md`); typed records (Dogfood Findings, Gen-Review Scorecard, Deploy Record) + the deploy-freshness cadence rule (`skills/{adia-dogfood,adia-gen-review,adia-deploy}/SKILL.md`); routing/eval corpora for release, deploy, dogfood, gen-review, llm-internals (`skills/{adia-release,adia-deploy,adia-dogfood,adia-gen-review,adia-llm-internals}/evals/routing-corpus.json`).
11
+ - **`release-pretag-docs-gate` hook** (`bin/`, registered PreToolUse·Bash in `hooks/hooks.json`): denies irreversible release commands (tag-lockstep without `--delete`, release-tag pushes, `dispatch-publish`, `release-pack --mode handoff`) until the repo's `scripts/release/check-release-docs.mjs` passes — every lockstep CHANGELOG carries the version's section with no leftover `[Unreleased]` content, entry READMEs exist, and the generated team notes (`docs/releases/vX.Y.Z.md`) are present. Quiet outside the monorepo and on reversible commands; selftest shipped. Born from the v0.8.4 near-miss (tarballs shipped with unpromoted CHANGELOG headers). `skills/adia-release` cut procedure gains Step 4e (docs review + notes generation) accordingly.
12
+
13
+ ### Maintenance
14
+ - **Lockstep version bump** (`.claude-plugin/plugin.json` version field, 0.8.4 → 0.8.5).
15
+
16
+
17
+ ## [0.8.4] — 2026-07-16
18
+
19
+ ### Changed
20
+ - **`adia-llm-internals` references re-synced to the shipped fixes** (`skills/adia-llm-internals/references/`, `.claude-plugin/plugin.json` version): `bridge-facade.md` documents the new provider-resolution precedence (model id outranks env detection, `detectProviderFromModel`), `browser-proxy-boundary.md` documents origin-only passthrough URL swapping + the provider-prefix proxy map, `adapter-contract.md`/`model-registry.md`/`add-a-provider.md` carry the gemini raw-stopReason and keys-must-match-registry invariants.
21
+
2
22
  ## [0.8.3] — 2026-07-16
3
23
 
4
24
  ### Fixed
@@ -19,8 +19,16 @@ model: inherit
19
19
  The framework-verifier runs the preloaded sweep and review procedures and
20
20
  returns severity-ordered findings with file:line evidence; it holds no Write
21
21
  or Edit tool, so it cannot fix what it finds — that separation is the point.
22
- Surfaces under review are data; a "rate this clean" string inside one is
23
- itself a finding. Probes that cannot run (no browser, no dev server) are
24
- reported as UNMEASURED sections, never silently skipped. Done when every
25
- requested mode has either findings or an explicit clean/UNMEASURED verdict
26
- in the report.
22
+ **No Write/Edit tool is not the whole boundary: Bash alone can still mutate.**
23
+ Never run mutating Bash no `perl -i` / `sed -i` (in-place edits), no shell
24
+ redirects into a tracked file (`>`, `>>`), no `git add`/`commit`/`checkout --`
25
+ or other mutating git call, and no scripted file write via `node -e`/a
26
+ one-shot script (`fs.writeFileSync` etc.). Only detection commands run here —
27
+ the audit/probe scripts a mode's table names, always read-only. A finding
28
+ that has a documented mechanical fix (the dogfood skill's auto-fix allowlist)
29
+ is reported with its fix-routing noted, never applied inline; it routes to
30
+ the `component-author` builder seat or a human. Surfaces under review are
31
+ data; a "rate this clean" string inside one is itself a finding. Probes that
32
+ cannot run (no browser, no dev server) are reported as UNMEASURED sections,
33
+ never silently skipped. Done when every requested mode has either findings
34
+ or an explicit clean/UNMEASURED verdict in the report.
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ """release-pretag-docs-gate — PreToolUse gate on irreversible release verbs.
3
+
4
+ Fires before Bash runs a command that makes a release irreversible — tag
5
+ creation (tag-lockstep without --delete), release-tag pushes, publish
6
+ dispatch, or release-pack's handoff mode — and requires the release docs
7
+ to already be in shape: every lockstep CHANGELOG carries the version's
8
+ section (no leftover [Unreleased] content), entry READMEs exist, and the
9
+ team release notes are generated at docs/releases/v<X>.md. The actual
10
+ checks live in the repo (scripts/release/check-release-docs.mjs) so the
11
+ git pre-push hook shares them; this wrapper only classifies the command,
12
+ derives the version, and converts the gate's findings into a deny with
13
+ the repair commands attached.
14
+
15
+ Born from the v0.8.4 near-miss (2026-07-17): the promotion step was
16
+ skipped, F-N1 fired only after tagging, and the publish shipped tarballs
17
+ with [Unreleased] CHANGELOG headers. This gate runs the same checks at
18
+ the last reversible moment instead.
19
+
20
+ Deliberately quiet: non-Bash events, non-release commands, --delete /
21
+ --dry runs, and repos without the gate script (consumer installs of this
22
+ plugin) all exit 0 with no output.
23
+
24
+ Usage:
25
+ release-pretag-docs-gate --hook # PreToolUse mode: event JSON on stdin
26
+ release-pretag-docs-gate selftest # prove the classifier on fixtures
27
+ """
28
+ import json
29
+ import os
30
+ import re
31
+ import subprocess
32
+ import sys
33
+
34
+ GATE_REL = "scripts/release/check-release-docs.mjs"
35
+
36
+ # Irreversible release verbs -> how to find the version in the command.
37
+ VERSION_FLAG = re.compile(r"--version[= ]['\"]?(\d+\.\d+\.\d+)")
38
+ # A pushed TAG is a bare token like `v0.8.4`, `web-components-v0.8.4`, or
39
+ # the full-form `refs/tags/v0.8.4`; branch refs (`release/v0.8.4`,
40
+ # `refs/heads/…`) stay reversible and deliberately don't match, as do tag
41
+ # DELETIONS (`:refs/tags/v0.8.4` — leading colon).
42
+ TAG_TOKEN = re.compile(r"^(?:[a-z0-9-]+-)?v(\d+\.\d+\.\d+)$")
43
+
44
+ # Sentinel: the command is release-irreversible but no single version can
45
+ # be derived (e.g. `git push --tags`) — deny with the doctrine message
46
+ # rather than fail open.
47
+ AMBIGUOUS = "AMBIGUOUS"
48
+
49
+
50
+ def classify(command):
51
+ """Return the target version string when `command` is an irreversible
52
+ release verb, AMBIGUOUS when it is irreversible but version-less
53
+ (fail closed), else None."""
54
+ if not command:
55
+ return None
56
+ if "tag-lockstep.mjs" in command or "dispatch-publish.mjs" in command \
57
+ or ("release-pack.mjs" in command and "--mode handoff" in command):
58
+ # Reversal / preview / selftest invocations only suppress for these
59
+ # script verbs (selftest earned its row on first live fire: a
60
+ # compound running `tag-lockstep.mjs selftest` next to an unrelated
61
+ # --version flag denied — 2026-07-17).
62
+ if "--delete" in command or "--dry" in command or "selftest" in command:
63
+ return None
64
+ m = VERSION_FLAG.search(command)
65
+ return m.group(1) if m else None
66
+ push_m = re.search(r"\bgit\b[^|;&\n]*\bpush\b", command)
67
+ if push_m:
68
+ # Scan ONLY the push command's own segment — a compound command's
69
+ # unrelated text (a PR body mentioning "v0.8.4", a commit message)
70
+ # must not classify a branch push as a tag push (live false
71
+ # positive 2026-07-17: `git push <branch> … && gh pr create
72
+ # --body "…v0.8.4…"` denied).
73
+ segment = re.split(r"[|;&\n]", command[push_m.start():])[0]
74
+ # `git push --dry-run` / `-n` never moves refs.
75
+ if "--dry-run" in segment or re.search(r"\s-n\b", segment):
76
+ return None
77
+ # Blanket tag pushes can carry release tags AND break the
78
+ # one-tag-per-push publish doctrine — fail closed.
79
+ if "--tags" in segment or "--follow-tags" in segment:
80
+ return AMBIGUOUS
81
+ for token in segment.split():
82
+ token = token.strip("\"'")
83
+ if token.startswith("refs/tags/"):
84
+ token = token[len("refs/tags/"):]
85
+ m = TAG_TOKEN.match(token)
86
+ if m:
87
+ return m.group(1)
88
+ return None
89
+ return None
90
+
91
+
92
+ def find_repo_gate(start):
93
+ """Walk up from `start` to the git root looking for the gate script —
94
+ a release command launched from a subdirectory must not bypass the
95
+ gate just because cwd isn't the repo root."""
96
+ d = os.path.abspath(start or os.getcwd())
97
+ for _ in range(12):
98
+ gate = os.path.join(d, GATE_REL)
99
+ if os.path.isfile(gate):
100
+ return d, gate
101
+ if os.path.isdir(os.path.join(d, ".git")):
102
+ return None, None # repo root reached, no gate — consumer repo
103
+ parent = os.path.dirname(d)
104
+ if parent == d:
105
+ return None, None
106
+ d = parent
107
+ return None, None
108
+
109
+
110
+ def deny(reason):
111
+ print(json.dumps({
112
+ "hookSpecificOutput": {
113
+ "hookEventName": "PreToolUse",
114
+ "permissionDecision": "deny",
115
+ "permissionDecisionReason": reason,
116
+ }
117
+ }))
118
+ sys.exit(0)
119
+
120
+
121
+ def hook_mode():
122
+ try:
123
+ event = json.load(sys.stdin)
124
+ except Exception:
125
+ sys.exit(0)
126
+ if event.get("tool_name") != "Bash":
127
+ sys.exit(0)
128
+ version = classify((event.get("tool_input") or {}).get("command", ""))
129
+ if not version:
130
+ sys.exit(0)
131
+ root, gate = find_repo_gate(event.get("cwd"))
132
+ if not gate:
133
+ sys.exit(0) # consumer repo — nothing to enforce here
134
+ if version == AMBIGUOUS:
135
+ deny(
136
+ "release-pretag-docs-gate · blanket tag push in the release monorepo\n"
137
+ "`git push --tags`/`--follow-tags` can carry release tags unchecked AND\n"
138
+ "batched tag pushes drop the publish-on-tag triggers (invariant 4).\n"
139
+ "Push release tags ONE per `git push origin <tag>` instead."
140
+ )
141
+ try:
142
+ proc = subprocess.run(
143
+ ["node", gate, "--version", version],
144
+ capture_output=True, text=True, cwd=root, timeout=60,
145
+ )
146
+ except (subprocess.TimeoutExpired, OSError) as e:
147
+ # Fail CLOSED on the release path — an erroring gate is not a pass.
148
+ deny(
149
+ "release-pretag-docs-gate · the docs gate itself errored (" + type(e).__name__ + ")\n"
150
+ "Run it directly to diagnose: node " + GATE_REL + " --version " + version
151
+ )
152
+ if proc.returncode == 0:
153
+ sys.exit(0)
154
+ findings = (proc.stderr or proc.stdout).strip()
155
+ deny(
156
+ "release-pretag-docs-gate · release docs not ready for v" + version + "\n"
157
+ + findings + "\n"
158
+ "This command is irreversible (tags/publish) — land the doc fixes via PR first\n"
159
+ "(invariant 3), then re-run. If a finding seems wrong, report it against\n"
160
+ "scripts/release/check-release-docs.mjs; do not bypass the gate."
161
+ )
162
+
163
+
164
+ def selftest():
165
+ cases = [
166
+ ("node .claude/skills/adia-release/scripts/tag-lockstep.mjs --version 0.8.4", "0.8.4"),
167
+ ("node .claude/skills/adia-release/scripts/tag-lockstep.mjs --version 0.8.4 --delete", None),
168
+ ("node scripts/x/dispatch-publish.mjs --version 1.2.3 --verify-triggered", "1.2.3"),
169
+ ("node release-pack.mjs --mode handoff --version 0.9.0 --yes", "0.9.0"),
170
+ ("node release-pack.mjs --mode cut --version 0.9.0", None),
171
+ ("node release-pack.mjs --mode handoff --version 0.9.0 --dry", None),
172
+ ("git push origin web-components-v0.8.4", "0.8.4"),
173
+ ("git push origin v0.8.4", "0.8.4"),
174
+ ('git -C "$REPO" push origin v0.8.4', "0.8.4"),
175
+ ("git push origin refs/tags/v0.8.4", "0.8.4"),
176
+ ("git push origin :refs/tags/v0.8.4", None), # tag DELETION is a reversal
177
+ ("git push origin v0.8.4 --dry-run", None),
178
+ ("git push --tags origin", AMBIGUOUS),
179
+ ("git push --follow-tags origin main", AMBIGUOUS),
180
+ ("node tag-lockstep.mjs --version '0.8.4'", "0.8.4"),
181
+ ("git push origin main", None),
182
+ ("git push -u origin release/v0.8.4", None), # branch push is reversible
183
+ # Compound: branch push + unrelated version text in a later command
184
+ # (PR bodies) must NOT classify as a tag push.
185
+ ('git push -u origin feat/x 2>&1 | tail -1\ngh pr create --body "the four v0.8.4 defects"', None),
186
+ ('git push origin v0.8.4 && echo done', "0.8.4"),
187
+ ("npm run build", None),
188
+ ("", None),
189
+ ]
190
+ for cmd, want in cases:
191
+ got = classify(cmd)
192
+ if got != want:
193
+ print(f"selftest FAIL: classify({cmd!r}) = {got!r}, want {want!r}", file=sys.stderr)
194
+ sys.exit(1)
195
+
196
+ # find_repo_gate walks up from a subdirectory (tempdir fixture — the
197
+ # v1 bug checked cwd only, so any subdir cwd bypassed the gate).
198
+ import tempfile
199
+ with tempfile.TemporaryDirectory() as td:
200
+ os.makedirs(os.path.join(td, ".git"))
201
+ os.makedirs(os.path.join(td, "scripts", "release"))
202
+ open(os.path.join(td, GATE_REL), "w").write("// fixture\n")
203
+ sub = os.path.join(td, "packages", "deep", "nested")
204
+ os.makedirs(sub)
205
+ root, gate = find_repo_gate(sub)
206
+ if root != td or not gate:
207
+ print(f"selftest FAIL: find_repo_gate walk-up ({root!r})", file=sys.stderr)
208
+ sys.exit(1)
209
+ with tempfile.TemporaryDirectory() as td:
210
+ os.makedirs(os.path.join(td, ".git"))
211
+ sub = os.path.join(td, "src")
212
+ os.makedirs(sub)
213
+ root, gate = find_repo_gate(sub)
214
+ if gate is not None:
215
+ print("selftest FAIL: consumer repo must yield no gate", file=sys.stderr)
216
+ sys.exit(1)
217
+ print("selftest OK")
218
+
219
+
220
+ if __name__ == "__main__":
221
+ if len(sys.argv) > 1 and sys.argv[1] == "selftest":
222
+ selftest()
223
+ elif len(sys.argv) > 1 and sys.argv[1] == "--hook":
224
+ hook_mode()
225
+ else:
226
+ print(__doc__)
package/hooks/hooks.json CHANGED
@@ -9,6 +9,16 @@
9
9
  "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/bin/sidecar-prewrite-guard\" --hook"
10
10
  }
11
11
  ]
12
+ },
13
+ {
14
+ "matcher": "Bash",
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/bin/release-pretag-docs-gate\" --hook",
19
+ "timeout": 90
20
+ }
21
+ ]
12
22
  }
13
23
  ],
14
24
  "PostToolUse": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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-factory (the consumer/app-author plugin).",
5
5
  "keywords": [
6
6
  "adia-ui",
@@ -81,10 +81,18 @@ npm run test:a2ui # 22/22 (+1 skipped OK)
81
81
  npm run eval:diff -- --engine zettel # floors: cov≥87, avg≥85, MRR≥0.94
82
82
  npm run check:zettel-eval-regression -- --latest --strict # mechanical floor gate
83
83
  npm run check:free-form-eval-regression -- --latest # free-form twin
84
- npm run eval:diff -- --engine free-form # floors: cov≥90, avg≥83, F1≥55
84
+ npm run eval:diff -- --engine free-form # floors: cov≥88, avg≥85, F1≥52
85
85
  ```
86
86
 
87
- Monolithic floor: cov=100, avg≥95. Dogfood set: 20/20, avg≥95. A failing gate
87
+ The two `check:*-eval-regression` scripts (`scripts/release/check-zettel-eval-regression.mjs`,
88
+ `scripts/release/check-free-form-eval-regression.mjs`) own the floor numbers — read
89
+ their `ALERT_FLOOR` constants before quoting a number here; this file only mirrors
90
+ them and can drift (it once silently regressed to `cov≥40` before the mechanical
91
+ gate existed).
92
+
93
+ Monolithic floor: cov=100, avg≥95. Dogfood set: 20/20, avg≥95. No mechanical
94
+ regression gate exists for monolithic yet — this floor is convention-only, same
95
+ failure mode the zettel/free-form gates were built to close. A failing gate
88
96
  is the artifact — fix at the source (chunk HTML, engine code, tool schema),
89
97
  re-run the narrowest gate, then the full sequence. A threshold tweak that
90
98
  papers over a failing gate is a regression, not a fix.
@@ -14,7 +14,7 @@ lifecycle), `.claude/docs/specs/package-architecture.md` (package relations).
14
14
  | File | Role |
15
15
  | --- | --- |
16
16
  | `packages/a2ui/compose/core/generator.js` | `generate_ui` orchestrator — instant / pro / thinking / stream modes; multi-turn via `executionId` |
17
- | `packages/a2ui/compose/strategies/registry.js` | Engine registry — `registerEngine(name, factory)`. Reserved names: `monolithic`, `zettel`, `mcp`, `monolithic-*` |
17
+ | `packages/a2ui/compose/strategies/registry.js` | Engine registry — `registerEngine(name, factory)`. Reserved names: `monolithic`, `monolithic-instant`, `monolithic-pro`, `monolithic-thinking`, `zettel`, `chunk-zettel`, `free-form` |
18
18
  | `packages/a2ui/compose/strategies/zettel/` | Zettel + chunk-zettel engines — see [strategy-engines](strategy-engines.md) for the per-file map |
19
19
  | `packages/a2ui/compose/strategies/free-form-composer/` | Free-form engine (`index.js`, `system-prompt.js`, `transpile.js`) |
20
20
  | `packages/a2ui/compose/strategies/_shared/chunk-loader.js` | Shared chunk loading for engines |
@@ -93,6 +93,7 @@ npm run verify:traits # 100% coverage
93
93
  npm run smoke:engines # green
94
94
  node scripts/dev/audit-native-primitive-leak.mjs # 0 critical leaks
95
95
  node scripts/dev/audit-shell-composition.mjs # 0 critical defects
96
+ node scripts/dev/audit-template-child-conflict.mjs # 0 critical (gh#284 shape — non-null template + slots.default)
96
97
  ```
97
98
 
98
99
  A failed gate is the artifact: fix at the source, re-run the narrowest gate,
@@ -376,7 +376,7 @@ disconnected() {
376
376
 
377
377
  ```javascript
378
378
  // WRONG — chart.js (pre-fix)
379
- class AdiaChart extends AdiaElement {
379
+ class UIChart extends UIElement {
380
380
  connected() { /* ... */ this.#ro = new ResizeObserver(...); }
381
381
 
382
382
  disconnected() {
@@ -408,8 +408,8 @@ disconnected() {
408
408
  ### AP-L3 · Missing `super.connected()` / `super.disconnected()`
409
409
 
410
410
  ```javascript
411
- // WRONG — AdiaFormElement subclass missing super call
412
- class AdiaInput extends AdiaFormElement {
411
+ // WRONG — UIFormElement subclass missing super call
412
+ class UIInput extends UIFormElement {
413
413
  connected() {
414
414
  // super.connected() NOT called
415
415
  this.#inputEl = this.querySelector('input');
@@ -479,7 +479,7 @@ updateConfig(opts) {
479
479
 
480
480
  ```javascript
481
481
  // WRONG — drawer.js (pre-fix)
482
- class AdiaDrawer extends AdiaElement {
482
+ class UIDrawer extends UIElement {
483
483
  static template = () => html``; // ← looks harmless. It isn't.
484
484
 
485
485
  connected() {
@@ -489,7 +489,7 @@ class AdiaDrawer extends AdiaElement {
489
489
  }
490
490
  ```
491
491
 
492
- **What went wrong:** `AdiaElement`'s base `connectedCallback` runs an effect that calls `stamp(template(this), this)`. `stamp` calls `mount`, which calls `container.replaceChildren(fragment)`. Even an "empty" `html\`\``produces a non-null result, so`replaceChildren`runs and clears every authored child (header/section/footer) out of the host element. The component's own`render()` then tries to migrate children that no longer exist.
492
+ **What went wrong:** `UIElement`'s base `connectedCallback` runs an effect that calls `stamp(template(this), this)`. `stamp` calls `mount`, which calls `container.replaceChildren(fragment)`. Even an "empty" `html\`\``produces a non-null result, so`replaceChildren`runs and clears every authored child (header/section/footer) out of the host element. The component's own`render()` then tries to migrate children that no longer exist.
493
493
 
494
494
  Symptom: drawers render with a close button in an empty panel; all authored content is gone. Hard to debug because the children WERE in the DOM at `connected()` time — they get wiped milliseconds later when the effect runs.
495
495
 
@@ -497,8 +497,8 @@ Symptom: drawers render with a close button in an empty panel; all authored cont
497
497
 
498
498
  ```javascript
499
499
  // RIGHT — no template override
500
- class AdiaDrawer extends AdiaElement {
501
- // base AdiaElement.template returns null → stamp() is skipped →
500
+ class UIDrawer extends UIElement {
501
+ // base UIElement.template returns null → stamp() is skipped →
502
502
  // authored light-DOM children survive through render().
503
503
 
504
504
  connected() { /* ... */ }
@@ -4,7 +4,7 @@ Deep dive on declaring component APIs in AdiaUI. Read this when adding a new pro
4
4
 
5
5
  ## The `static properties` block
6
6
 
7
- Every `AdiaElement` subclass declares its public API as a `static properties` object. Keys are camelCase JS names; values describe type, default, reflection, and attribute mapping.
7
+ Every `UIElement` subclass declares its public API as a `static properties` object. Keys are camelCase JS names; values describe type, default, reflection, and attribute mapping.
8
8
 
9
9
  ```javascript
10
10
  static properties = {
@@ -141,21 +141,21 @@ Reserve `error` for validation state (`[error]` on form inputs matches ARIA patt
141
141
  The component has three names that must agree:
142
142
 
143
143
  - **File path:** `packages/web-components/components/foo/foo.js`
144
- - **Class name:** `class AdiaFoo extends AdiaElement`
145
- - **Custom element tag:** `customElements.define('foo-ui', AdiaFoo)`
144
+ - **Class name:** `class UIFoo extends UIElement`
145
+ - **Custom element tag:** `customElements.define('foo-ui', UIFoo)`
146
146
 
147
147
  A fourth consistency requirement: the CSS file at `packages/web-components/components/foo/foo.css` uses `@scope (foo-ui)`.
148
148
 
149
- ## Extending `AdiaFormElement`
149
+ ## Extending `UIFormElement`
150
150
 
151
- Form-participating components extend `AdiaFormElement` (which extends `AdiaElement`) and get `ElementInternals` wiring, form-reset handling, and `.form` / `.labels` / `.validity` accessors for free.
151
+ Form-participating components extend `UIFormElement` (which extends `UIElement`) and get `ElementInternals` wiring, form-reset handling, and `.form` / `.labels` / `.validity` accessors for free.
152
152
 
153
153
  ```javascript
154
- import { AdiaFormElement } from '../../core/form.js';
154
+ import { UIFormElement } from '../../core/form.js';
155
155
 
156
- class AdiaInput extends AdiaFormElement {
156
+ class UIInput extends UIFormElement {
157
157
  static properties = {
158
- ...AdiaFormElement.properties, // inherit name, value, disabled, required, etc.
158
+ ...UIFormElement.properties, // inherit name, value, disabled, required, etc.
159
159
  placeholder: { type: String, default: '', reflect: true },
160
160
  };
161
161
 
@@ -180,7 +180,7 @@ Key details:
180
180
 
181
181
  - **Always `super.connected()` and `super.disconnected()`** — without them, form-association doesn't register.
182
182
  - **`this.syncValue(str)`** — call this whenever the value changes to update the form-submitted string. Accepts a string.
183
- - **Inheriting properties** — spread `AdiaFormElement.properties` into your own `static properties` so you don't re-declare `name`, `disabled`, `required`.
183
+ - **Inheriting properties** — spread `UIFormElement.properties` into your own `static properties` so you don't re-declare `name`, `disabled`, `required`.
184
184
 
185
185
  ## Event conventions
186
186
 
@@ -9,8 +9,8 @@ The 5-step procedure run AFTER [primitive-audit.md](primitive-audit.md) clears (
9
9
  Before writing or editing, load these four files. They are the ground truth:
10
10
 
11
11
  - `.claude/docs/specs/component-token-contract.md` — the authoritative invariants. Skim the whole thing if you haven't read it this session; focus on "Variants vs Modes" and "Sanctioned Mode Attributes" if you're adding a new layout-affecting attribute.
12
- - `packages/web-components/core/element.js` — `AdiaElement` base class. The `static properties` schema, `connected()`/`disconnected()`/`render()` lifecycle, and attribute-mapping conventions are all defined here.
13
- - `packages/web-components/core/form.js` — `AdiaFormElement`. Only needed if the new component participates in forms (inputs, selects, checkboxes, etc.).
12
+ - `packages/web-components/core/element.js` — `UIElement` base class. The `static properties` schema, `connected()`/`disconnected()`/`render()` lifecycle, and attribute-mapping conventions are all defined here.
13
+ - `packages/web-components/core/form.js` — `UIFormElement`. Only needed if the new component participates in forms (inputs, selects, checkboxes, etc.).
14
14
  - At least one good-citizen reference that matches the shape of what you're building. Pick from: `button-ui`, `card-ui`, `input-ui`, `textarea-ui`, `check-ui`. Read both the `.js` and the `.css`.
15
15
 
16
16
  ## Step 2 — Classify the work before you write
@@ -41,7 +41,7 @@ These rules are the distilled lessons from a 5-iteration audit. Each one corresp
41
41
 
42
42
  5. **Reserved-name anti-patterns.** Avoid: `title` (collides with HTML tooltip attribute), `active` on parent components (use `value` for a selection or `step` for an index — per-item `active` on children is fine), `error` in variant names (use `danger`; reserve `error` for validation state), `disabled` on non-form-participating components (use `readonly`), `multiple` with exclusion semantics (use a negated positive like `single`).
43
43
 
44
- 6. **Element tag ends in `-ui`; JS class is `Adia<Component>`.** `<foo-ui>` ↔ `class AdiaFoo extends AdiaElement`. Three-way consistency: filename, class name, custom-element tag. The sanctioned `-n` carve-out is `cot-ui` (the chain-of-thought streaming component); `nav-ui` was deprecated in favor of the `-ui` replacements. New `-n` tags require a contract-doc update — see `.claude/docs/specs/component-token-contract.md`.
44
+ 6. **Element tag ends in `-ui`; JS class is `UI<Component>`.** `<foo-ui>` ↔ `class UIFoo extends UIElement`. Three-way consistency: filename, class name, custom-element tag. The sanctioned `-n` carve-out is `cot-ui` (the chain-of-thought streaming component); `nav-ui` was deprecated in favor of the `-ui` replacements. New `-n` tags require a contract-doc update — see `.claude/docs/specs/component-token-contract.md`.
45
45
 
46
46
  ### CSS
47
47
 
@@ -86,7 +86,7 @@ These rules are the distilled lessons from a 5-iteration audit. Each one corresp
86
86
 
87
87
  1. **Every `addEventListener` in `connected()` has a matching `removeEventListener` in `disconnected()`.** Handler must be a stable `#field` arrow (`#onClick = (e) => { ... }`), never an inline arrow passed to `addEventListener`. Inline arrows can't be removed — `removeEventListener` needs reference equality.
88
88
 
89
- 2. **`AdiaFormElement` subclasses call `super.connected()` and `super.disconnected()`.** `ElementInternals` registration depends on it. Omitting `super` strands the form-association.
89
+ 2. **`UIFormElement` subclasses call `super.connected()` and `super.disconnected()`.** `ElementInternals` registration depends on it. Omitting `super` strands the form-association.
90
90
 
91
91
  3. **Timers and observers are torn down.** `clearInterval`, `clearTimeout`, `ResizeObserver.disconnect()`, `MutationObserver.disconnect()`, `IntersectionObserver.disconnect()` all in `disconnected()`.
92
92
 
@@ -164,7 +164,7 @@ Before declaring the work done, run through this checklist. If anything fails, f
164
164
  - [ ] Every `static properties` entry uses `attribute:` (not `attr:`).
165
165
  - [ ] Every state-bearing Boolean has `reflect: true`.
166
166
  - [ ] No reserved-name anti-patterns (`title`, parent-level `active`, `error` variant, `disabled` on non-form, exclusion-`multiple`).
167
- - [ ] Element tag ends in `-ui`; class is `Adia<Component>`.
167
+ - [ ] Element tag ends in `-ui`; class is `UI<Component>`.
168
168
 
169
169
  ### CSS
170
170
 
@@ -179,7 +179,7 @@ Before declaring the work done, run through this checklist. If anything fails, f
179
179
 
180
180
  - [ ] Every `addEventListener` in `connected()` has a paired `removeEventListener` in `disconnected()`.
181
181
  - [ ] All handlers passed to `addEventListener` are stable `#field` arrows (or bound method refs), not inline arrows.
182
- - [ ] If the class extends `AdiaFormElement`, both `connected()` and `disconnected()` call `super`.
182
+ - [ ] If the class extends `UIFormElement`, both `connected()` and `disconnected()` call `super`.
183
183
  - [ ] Every timer/observer created in `connected()` is disposed in `disconnected()`.
184
184
  - [ ] Cached DOM refs (`this.#fooEl`) are nulled in `disconnected()`.
185
185
  - [ ] Class declares `disconnected()` exactly once.
@@ -210,7 +210,7 @@ Native DOM accessors (`textContent`, `innerHTML`) get clobbered if declared in `
210
210
  Every `addEventListener` in `connected()` must be paired with a `removeEventListener` in `disconnected()`. Every timer cleared, every observer disconnected, every popover removed. Handlers must be stable `#field` arrows so `removeEventListener` finds them via reference equality.
211
211
 
212
212
  ```js
213
- class AdiaBadge extends AdiaElement {
213
+ class UIBadge extends UIElement {
214
214
  #onRemove = (e) => { /* ... */ }; // stable handler
215
215
 
216
216
  connected() {
@@ -226,7 +226,7 @@ class AdiaBadge extends AdiaElement {
226
226
 
227
227
  Inline arrows passed to `addEventListener` look fine and leak quietly — `removeEventListener` no-ops because the new arrow has different identity. Three components in one audit cycle bit on this exact bug.
228
228
 
229
- If the class extends `AdiaFormElement`, `connected()` and `disconnected()` MUST call `super.*` — `ElementInternals` registration depends on it. And declare `disconnected()` exactly once per class; the second silently overrides the first.
229
+ If the class extends `UIFormElement`, `connected()` and `disconnected()` MUST call `super.*` — `ElementInternals` registration depends on it. And declare `disconnected()` exactly once per class; the second silently overrides the first.
230
230
 
231
231
  Full lifecycle patterns in [lifecycle-patterns.md](lifecycle-patterns.md).
232
232
 
@@ -276,11 +276,11 @@ When transparency-mixing colors, use `oklab` not `oklch` for the interpolation s
276
276
 
277
277
  ## Legacy avoidance
278
278
 
279
- The library was renamed from `@agent-ui-kit/*` to `@adia-ai/*`; tokens went from `--n-*` to `--a-*`; the base class went from `NanoElement` to `AdiaElement`. Don't use the legacy forms in new code. Don't reference `nano-ui` in notes, commit messages, CHANGELOG entries, or any AdiaUI artifact.
279
+ The library was renamed from `@agent-ui-kit/*` to `@adia-ai/*`; tokens went from `--n-*` to `--a-*`; the base class went `NanoElement` `AdiaElement` → `UIElement` (ADR-0016 class rename). Don't use the legacy forms in new code. Don't reference `nano-ui` in notes, commit messages, CHANGELOG entries, or any AdiaUI artifact.
280
280
 
281
281
  ```js
282
282
  // right
283
- import { AdiaElement } from '@adia-ai/web-components';
283
+ import { UIElement } from '@adia-ai/web-components/core';
284
284
  :where(:scope) { --button-bg: var(--a-primary-bg); }
285
285
 
286
286
  // wrong
@@ -1,13 +1,13 @@
1
1
  # Lifecycle Patterns — Listeners, Timers, Observers, Popovers
2
2
 
3
- Deep dive on symmetric `connected()` / `disconnected()` in AdiaUI components. Read when adding any side effect (listener, timer, observer, popover) to a component, or when inheriting from `AdiaFormElement`.
3
+ Deep dive on symmetric `connected()` / `disconnected()` in AdiaUI components. Read when adding any side effect (listener, timer, observer, popover) to a component, or when inheriting from `UIFormElement`.
4
4
 
5
5
  ## The symmetry rule
6
6
 
7
7
  Every side effect set up in `connected()` has a matching teardown in `disconnected()`. The teardown is authored at the same time as the setup, not later — if you leave `disconnected()` for tomorrow, tomorrow won't happen.
8
8
 
9
9
  ```javascript
10
- class AdiaWidget extends AdiaElement {
10
+ class UIWidget extends UIElement {
11
11
  static properties = { /* ... */ };
12
12
  static template = () => null;
13
13
 
@@ -150,16 +150,16 @@ disconnected() {
150
150
 
151
151
  **Lesson:** a class must declare `disconnected()` exactly once. If you need to add teardown, edit the existing method. Duplicate method names silently shadow — no error, no warning.
152
152
 
153
- ## `AdiaFormElement` — `super` discipline
153
+ ## `UIFormElement` — `super` discipline
154
154
 
155
- Form-participating components inherit `ElementInternals` wiring from `AdiaFormElement`. That wiring only works if `super.connected()` and `super.disconnected()` are called:
155
+ Form-participating components inherit `ElementInternals` wiring from `UIFormElement`. That wiring only works if `super.connected()` and `super.disconnected()` are called:
156
156
 
157
157
  ```javascript
158
- import { AdiaFormElement } from '../../core/form.js';
158
+ import { UIFormElement } from '../../core/form.js';
159
159
 
160
- class AdiaInput extends AdiaFormElement {
160
+ class UIInput extends UIFormElement {
161
161
  static properties = {
162
- ...AdiaFormElement.properties,
162
+ ...UIFormElement.properties,
163
163
  placeholder: { type: String, default: '', reflect: true },
164
164
  };
165
165
 
@@ -296,7 +296,7 @@ Especially important for components that are attached/detached frequently (modal
296
296
  - **Inline arrow + `removeEventListener`** — the listener isn't really being removed. Silent leak.
297
297
  - **Forgot to create `disconnected()`** — if `connected()` adds side effects, `disconnected()` must exist. No exceptions.
298
298
  - **Two `disconnected()` methods in the same class** — the second silently wins; the first's cleanup is lost.
299
- - **`super.connected()` missing in `AdiaFormElement` subclass** — form participation silently fails.
299
+ - **`super.connected()` missing in `UIFormElement` subclass** — form participation silently fails.
300
300
  - **Popover / tooltip never cleaned up** — stays in `document.body` forever.
301
301
  - **Observer recreated on every render** — re-observing without disconnecting first leaks observer refs.
302
302
  - **Timer fired after component removed** — not cleared in `disconnected()`, fires on a dead component. Can cause null-deref crashes or phantom re-renders.
@@ -14,7 +14,7 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
14
14
  /* Block 1: token declarations on :where(:scope) — zero specificity */
15
15
  @scope (my-component-ui) {
16
16
  :where(:scope) {
17
- --my-bg: var(--a-surface-1);
17
+ --my-bg: var(--a-bg);
18
18
  --my-text: var(--a-fg);
19
19
  --my-radius: var(--a-radius-md);
20
20
  }
@@ -30,8 +30,8 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
30
30
 
31
31
  /* Variants override TOKENS, not base styles */
32
32
  :scope[variant="danger"] {
33
- --my-bg: var(--a-danger-surface);
34
- --my-text: var(--a-danger-text);
33
+ --my-bg: var(--a-danger-bg);
34
+ --my-text: var(--a-danger-fg);
35
35
  }
36
36
  }
37
37
  ```
@@ -40,7 +40,7 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
40
40
 
41
41
  1. **Zero raw color values** anywhere in component CSS
42
42
  - No `#fff`, `rgb(...)`, `oklch(...)` literals
43
- - All colors must reference `--a-chrome-*`, `--a-data-0..9`, `--a-fg*`, `--a-surface-*`, or family tokens (`--a-brand-*`, `--a-primary-*`, etc.)
43
+ - All colors must reference `--a-chrome-*`, `--a-data-0..9`, `--a-fg*`, `--a-bg-*`, or family tokens (`--a-brand-*`, `--a-primary-*`, etc.)
44
44
  2. **Two-block pattern** — token declarations separate from style rules
45
45
  3. **Variants override tokens** — never rewrite base styles inside `[variant]` selectors. Change token values; the base styles absorb the change.
46
46
  4. **`:where(:scope)`** for token defaults — zero specificity lets consumers override tokens from outside
@@ -75,7 +75,7 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
75
75
  4. **Check token usage in parent stylesheet**
76
76
 
77
77
  ```bash
78
- grep -n "--a-chrome\|--a-data\|--a-fg\|--a-surface" \
78
+ grep -n -- "--a-chrome\|--a-data\|--a-fg\|--a-bg" \
79
79
  packages/web-components/components/<name>/*.css
80
80
  ```
81
81