@adia-ai/adia-ui-forge 0.8.43 → 0.8.44

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 (35) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +1 -1
  4. package/agents/a2ui-maintenance-agent.md +8 -10
  5. package/agents/a2ui-planner-agent.md +9 -11
  6. package/agents/{framework-alignment-agent.md → framework-checker.md} +10 -12
  7. package/agents/package-release-agent.md +8 -11
  8. package/agents/primitive-authoring-agent.md +6 -8
  9. package/package.json +1 -1
  10. package/skills/a2ui-maintenance/SKILL.md +49 -100
  11. package/skills/a2ui-maintenance/references/eval-diagnostics.md +18 -0
  12. package/skills/a2ui-maintenance/references/pipeline-overview.md +46 -0
  13. package/skills/demo-audit/SKILL.md +55 -120
  14. package/skills/demo-audit/references/auto-fix-allowlist.md +26 -0
  15. package/skills/demo-audit/references/mode7-status-battery.md +22 -0
  16. package/skills/demo-audit/references/output-contract-worked-example.md +24 -0
  17. package/skills/demo-audit/references/probe-discipline-and-escalation.md +27 -0
  18. package/skills/gen-ui-review/SKILL.md +68 -136
  19. package/skills/gen-ui-review/references/exit-gate-mechanics.md +26 -0
  20. package/skills/gen-ui-review/references/lookup-maintenance.md +8 -0
  21. package/skills/gen-ui-review/references/loop-protocol.md +16 -0
  22. package/skills/gen-ui-review/references/scorecard-worked-examples.md +36 -0
  23. package/skills/package-release/SKILL.md +68 -56
  24. package/skills/package-release/references/authorization-model.md +34 -0
  25. package/skills/package-release/references/invariants-detail.md +61 -0
  26. package/skills/package-release/references/mechanization.md +54 -0
  27. package/skills/package-release/references/recovery-paths.md +10 -0
  28. package/skills/primitive-authoring/SKILL.md +13 -32
  29. package/skills/primitive-authoring/references/api-contract.md +58 -0
  30. package/skills/primitive-authoring/references/code-style.md +38 -0
  31. package/skills/primitive-authoring/references/token-contract.md +61 -1
  32. package/skills/site-deployment/SKILL.md +30 -88
  33. package/skills/site-deployment/references/deploy-playbooks.md +38 -0
  34. package/skills/ssr-compatibility/SKILL.md +28 -70
  35. package/skills/ssr-compatibility/references/failure-shapes.md +23 -0
@@ -1,5 +1,30 @@
1
1
  # AdiaUI code style — best practices
2
2
 
3
+ ## First principles (SKILL.md's own summary, expanded)
4
+
5
+ 1. **Invariants are enforced by the next author, not the linter.** A component
6
+ that violates the contract silently teaches the next agent the violation is
7
+ acceptable. Write as if your component is the reference the next one is
8
+ patterned after — because it will be.
9
+ 2. **Default behavior is the absent attribute.** `<component-ui>` with no
10
+ attributes does the expected default thing; every Boolean prop defaults to
11
+ `false`. If the expected default is "on," the prop name is wrong — flip it
12
+ (`closable` → `permanent`, `animate` → `static`).
13
+ 3. **Variants change tokens; modes change layout.** A variant body contains
14
+ only `--component-*: var(...)` lines — no `padding`, `display`, `position`,
15
+ `width`, `height`, `gap`, `flex`, `grid`, `overflow`, `border-radius`.
16
+ Layout-changing attributes are modes and require a Sanctioned Mode
17
+ Attributes entry in the contract doc.
18
+ 4. **Symmetric lifecycle or it's a leak.** Every listener added in
19
+ `connected()` is removed in `disconnected()`; every timer cleared, observer
20
+ disconnected, cached ref nulled. Handlers are stable `#field` arrows so
21
+ `removeEventListener` can match — inline arrows bit three components in one
22
+ audit cycle.
23
+ 5. **Component tokens consume L3, not L2.** Alias from the role×state matrix
24
+ (`--a-primary-bg-hover`), never the family base (`--a-primary`) — bypassing
25
+ L3 strands the component outside theme / dark-mode / contrast cascades
26
+ silently.
27
+
3
28
  Modern AdiaUI is small, declarative, and token-driven. Most of the "bugs" agents write are bugs _against the conventions_ — bare `<div>`s where `col-ui` belongs, raw `<input>` where `input-ui` belongs, hex colors where tokens belong. The conventions are not stylistic preferences; each one corresponds to a working feature (theme switching, density modes, form association, focus rings) that breaks silently when the convention is violated.
4
29
 
5
30
  When in doubt: look up the catalog (the `lookup_component` MCP tool, or the adia-factory plugin's composition skill), pick the existing primitive, wire through tokens.
@@ -221,6 +246,19 @@ Default behavior is the absent attribute. Attributes exist to opt OUT or carry a
221
246
 
222
247
  State-bearing Booleans must `reflect: true` so CSS can match `:scope[disabled]`, `:scope[selected]`, etc. Without reflection, hover / active / selected styles break silently.
223
248
 
249
+ [verified 2026-08-19] **The literal string `"false"` is not "presence = true"
250
+ here.** ADR-0075 special-cases `parseAttr` for `Boolean`-typed props so the
251
+ literal attribute string `"false"` parses to JS `false`, not `true` —
252
+ deviating from strict HTML semantics on purpose, to match this repo's own
253
+ A2UI transpiler (which already special-cased it defensively) and the
254
+ authoring intuition both human and generated markup default to. Writing
255
+ `show-label="false"` on a `default: true` Boolean prop does what it looks
256
+ like it does. (`default: true` is itself the rare, ratified exception to
257
+ this section's "every Boolean defaults to `false`" rule — ADR-0063's
258
+ stamped-attribute mechanism, gh#961 — not a license to skip the flip rule
259
+ above for a new prop.) See [api-contract.md](api-contract.md)'s field-rules
260
+ section for the full rule and shipped blast radius (`core/element.js:93-94`).
261
+
224
262
  Native DOM accessors (`textContent`, `innerHTML`) get clobbered if declared in `static properties` — `installProps` overrides the native setter and `el.textContent = ''` becomes a signal write, not a child-wipe. Don't declare those names in `static properties`.
225
263
 
226
264
  ## Symmetric lifecycle
@@ -142,7 +142,7 @@ mechanics; never work from this summary alone):
142
142
  granted since: `qr-code-ui[color]` / `icon-ui[weight]`, ADR-0070).
143
143
 
144
144
  Beyond the global grammar, ADR-0063
145
- (`docs/ops/adr/adr-0063-attribute-grammar-addendum.md`) ratifies six
145
+ (`docs/ops/adr/adr-0063-attribute-grammar-addendum.md`) ratifies seven
146
146
  CROSS-SIBLING conventions for component-local attribute naming — the axis
147
147
  ADR-0053/0054 don't cover. Any new attribute follows these:
148
148
 
@@ -179,12 +179,72 @@ ADR-0053/0054 don't cover. Any new attribute follows these:
179
179
  - **`-picker` is reserved for the outer trigger+popover form-associated
180
180
  composite** — never the inline substrate it composes. `color-picker-ui`
181
181
  (the inline substrate) renames to `color-area-ui`.
182
+ - **[verified 2026-08-19] Preset-boolean-vs-alias-retirement boundary**
183
+ (ADR-0076): a boolean attribute that is additive sugar over several
184
+ existing granular `no-*` opt-outs — `table-toolbar-ui[chrome-only]`
185
+ equivalent to setting all four of `no-filter`, `no-sort`, `no-columns`,
186
+ `no-search` — is NOT an ADR-0063-style alias-retirement case (the
187
+ granular attributes aren't duplicate spellings of one concept the way
188
+ `alert[dismissible]`/`[closable]` were; a consumer may legitimately want
189
+ a subset off, a combination the preset alone can't express), so the
190
+ granular attributes stay shipped, independently-addressable API with no
191
+ deprecation. The preset's precedence is **pure, absolute OR — never a
192
+ tri-state**: while set, all covered controls are off, full stop, with no
193
+ partial re-enable via clearing an individual `no-*` while the preset
194
+ remains set (ADR-0076: "`no-*` attributes are presence-based booleans …
195
+ meaning 'presence forces off' is expressible but 'absence means defer to
196
+ [the preset]'s own OR' is not without a tri-state amendment"). A second
197
+ auto-snap-enum precedent alongside ADR-0074's chart `ratio` lands here
198
+ too: `table-toolbar-ui[stage]` (`full | search-tight | icon-only |
199
+ overflow`) — unset auto-snaps via `@container` queries against studied
200
+ breakpoints, an explicit value pins and overrides the query, no
201
+ interpolation between stages.
182
202
 
183
203
  The renames above have LANDED: dual-read compat shims shipped via gh#1563,
184
204
  and the breaking cut removed the old names in 0.8.43 (gh#1617). The new
185
205
  spellings are the sole ones — a yaml or demo still showing an old name is
186
206
  stale and should be fixed.
187
207
 
208
+ ## Disabled-state tokens
209
+
210
+ [verified 2026-08-19] ADR-0073 ratifies the standing convention for any
211
+ control supporting the `disabled` boolean attribute/state:
212
+
213
+ - **Shared bg role, container-low tier.** `--a-ui-bg-disabled`
214
+ (`styles/colors/semantics/features.css`) resolves to
215
+ `var(--md-sys-color-neutral-container-low)` — the same 10%-tint role
216
+ `--a-bg-hover` / `--a-bg-muted` ride for REST-state de-emphasis. A
217
+ component's own `--<component>-bg-disabled` indirection aliases this
218
+ shared role — never a raw color, never a per-family `*-container-low`
219
+ variant (none exists in the disabled path).
220
+ - **`[state][disabled]` specificity override for checked/selected fills.**
221
+ A `[checked]`/`[selected]` selector outranks a plain `[disabled]` rule on
222
+ CSS specificity (2 attribute selectors beat 1), so a checked+disabled
223
+ control silently keeps its active-state fill unless the component adds an
224
+ explicit higher-specificity override — `[checked][disabled]` /
225
+ `[selected][disabled]` — routed through its own
226
+ `--<component>-...-checked-disabled` (or `-selected-disabled`) custom
227
+ prop, itself aliasing `--a-ui-bg-disabled`.
228
+ - **Reduced-contrast disabled border — the `--input-border-disabled`
229
+ pattern.** Any control that renders a border when disabled adds
230
+ `--<component>-border-disabled: var(--a-ui-border-disabled)`, applied as
231
+ `border-color` under `:scope[disabled]`.
232
+
233
+ Quoting ADR-0073's Decision 5: "a control supporting `disabled` uses the
234
+ shared, single neutral `--md-sys-color-neutral-container-low` role for
235
+ background … always through a `--<component>-bg-disabled` … indirection
236
+ aliasing `--a-ui-bg-disabled` … uses the reduced-contrast
237
+ `--input-border-disabled` pattern … for any border it renders when disabled
238
+ … overrides any checked/selected state fill at `[state][disabled]`
239
+ specificity rather than relying on `[disabled]` alone."
240
+
241
+ The re-runnable check is `scripts/audit/audit-disabled-fill-tokens.mjs`
242
+ (`check:disabled-fill-audit`, advisory) — it fails any `[disabled]`-scoped
243
+ `background`/`background-color` that doesn't resolve through a
244
+ disabled-aware indirection.
245
+
246
+ Source: [ADR-0073](../../../../../../docs/ops/adr/adr-0073-disabled-state-container-low-token-convention.md).
247
+
188
248
  ## When to update this reference
189
249
 
190
250
  If you add a new token category (like `--a-chrome-*` was added), update both this file and `.claude/docs/specs/component-token-contract.md`. The spec doc is the live source of truth; this file is the practitioner's checklist.
@@ -28,13 +28,11 @@ directives are findings.
28
28
 
29
29
  ## Platform contract (the non-obvious bits)
30
30
 
31
- - The app binds **`:8000` plain HTTP**; the exe.dev edge terminates TLS on
32
- `<host>.exe.xyz` and forwards to VM `:8000`. Nothing listening ⇒ exe.dev
33
- serves its "**Port 8000 unbound.**" error page (its nginx hint is just an
34
- example anything binding :8000 works). Don't bind :443 on the VM.
35
- - Default user **`exedev`** (uid 1000, in `sudo` + `docker`); service
36
- processes run as it. Preinstalled: `git`, `rsync`, `docker`. NOT
37
- preinstalled: `caddy`, `node`, `nginx` (apt + NodeSource).
31
+ - The app binds **`:8000` plain HTTP**; the exe.dev edge terminates TLS and
32
+ forwards to VM `:8000`. Nothing listening ⇒ "**Port 8000 unbound.**"
33
+ Don't bind :443 on the VM.
34
+ - Default user **`exedev`** (uid 1000, `sudo` + `docker`); service runs as
35
+ it. Preinstalled: `git`, `rsync`, `docker`. NOT: `caddy`, `node`, `nginx`.
38
36
  - **`127.0.0.1:9999` runs `shelley`** — exe.dev's internal agent,
39
37
  localhost-only. Leave it running; don't bind 9999.
40
38
  - Disk: 25 GB on `/`. New VMs ship RSA-2048-only host keys — verify the
@@ -47,18 +45,15 @@ Standard layout: `/srv/<app>/dist/` webroot (exedev-owned) ·
47
45
 
48
46
  ## Deploy-freshness cadence — a lockstep cut is not a site deploy
49
47
 
50
- `package-release` cutting and publishing the lockstep version (the roster in
51
- `scripts/package-paths.mjs` is the live count) does
52
- **not** itself update `ui-kit.exe.xyz` that only happens on a `site-v*` tag
53
- push (see below). Any lockstep cut that changes a package the site actually
54
- serves `web-components`, `web-modules`, `llm`, or `a2ui/*` **owes a site
55
- deploy in the same release cycle**, or an explicit operator decision to skip
56
- it, recorded in the release notes (who decided, why). "The release finished"
57
- is not evidence the site is current: the v0.8.x window is the proof case a
58
- lockstep cut touching served packages landed with no matching `site-v*` tag,
59
- and the deployed site sat a week behind npm before anyone noticed. When
60
- handing off from a release, check whether the cut touched a served package
61
- and close the loop before calling the cycle done.
48
+ `package-release` cutting and publishing does **not** itself update
49
+ `ui-kit.exe.xyz` only a `site-v*` tag push does. Any lockstep cut that
50
+ changes a package the site actually serves (`web-components`,
51
+ `web-modules`, `llm`, `a2ui/*`) **owes a site deploy in the same release
52
+ cycle**, or an explicit, recorded operator decision to skip it. "The
53
+ release finished" is not evidence the site is current (the v0.8.x window:
54
+ a served-package cut landed with no matching tag, site sat a week stale).
55
+ When handing off from a release, check whether the cut touched a served
56
+ package before calling the cycle done.
62
57
 
63
58
  ## Current deployments
64
59
 
@@ -72,60 +67,22 @@ VM artifacts (Caddyfile, unit, env example) live in repo `deploy/`.
72
67
 
73
68
  **Never run `npm run deploy:site` from a local shell.** Push a tag matching
74
69
  `site-v*` (or run the workflow via `workflow_dispatch`) — `deploy-site.yml`
75
- builds, dry-runs, and (once the `production-site` GitHub Environment has
76
- required reviewers configured a one-time repo-settings step, Settings
77
- Environments) waits for a human to read the dry-run's delete summary in the
78
- job before the destructive `deploy` job runs. The hardened sequence below
79
- is what the workflow automates; it's kept here as the reference for what
80
- the workflow does and as a manual fallback if CI itself is unavailable.
81
-
82
- `npm run deploy:site` still exists locally (`build:site` + `rsync -az
83
- --delete dist/ /srv/adia-ui/dist/`) for that fallback case only. It is
84
- destructive (a 2026-06-08 manual run deleted 3,572 files) — every step
85
- below is incident-earned; the reference carries the full commands and the
86
- delete-adjudication classes.
87
-
88
- 1. Build from clean, fully-merged `main`. In a fresh worktree,
89
- `npm run build -w @adia-ai/llm` runs **before** `build:site` — llm compiles
90
- at publish time and its outputs are gitignored, so without it
91
- `/packages/llm/core/index.js` 404s and component registration breaks site-wide
92
- (found live 2026-06-09).
93
- 2. Dry-run `rsync -azni --delete --exclude='packages/gen-ui/a2ui/corpus/feedback/'`
94
- and bucket **every** `*deleting` line into a known-safe class; any
95
- unexplained served-content delete aborts the deploy. The exclude protects
96
- prod-only runtime-written files (feedback logs wiped 2026-06-10).
97
- 3. Snapshot prod: `cp -al /srv/<app>/dist /srv/<app>/dist.bak-<date>`
98
- (hardlink farm — instant, and the only rollback).
99
- 4. Real rsync — the dry-run minus `-n`, SAME excludes, and the SAME BYTES:
100
- both jobs rsync the artifact-round-tripped tree, and the deploy job aborts
101
- if its tree's sha256 fingerprint differs from the one whose delete summary
102
- was approved (gh#425 — the dry-run used to read the build job's own
103
- `dist/`, so the v0.8.14 summary listed 3,128 deletions the real send never
104
- performed; an over-reporting summary trains reviewers to approve past scary
105
- numbers, and the same gap can under-report).
106
- 5. Verify a fixture **file** that only exists in the new build, then
107
- render-check a `/site/components/*` page headlessly (SPA returns 200 shell
108
- for any route; file presence alone misses the llm-404 class).
109
- 6. On verify failure: `rm -rf dist && mv dist.bak-<date> dist`. Keep the
110
- snapshot until the deploy is confirmed good.
70
+ builds, dry-runs, and waits for a human to read the delete summary before
71
+ the destructive `deploy` job runs. `npm run deploy:site` still exists
72
+ locally for CI-unavailable fallback only, and is destructive (a 2026-06-08
73
+ manual run deleted 3,572 files). The full step-by-step hardened sequence
74
+ (llm-build-first gotcha, delete-adjudication classes, snapshot, verified
75
+ rsync, fixture+render verify, rollback) is in
76
+ [deploy-playbooks.md](references/deploy-playbooks.md)'s "Hardened
77
+ `--delete` deploy sequence" every step there is incident-earned; read it
78
+ before running the fallback by hand.
111
79
 
112
80
  If `server.js` changed: rsync it, then `sudo systemctl restart <app>`.
113
81
 
114
- ### One-time CI setup for `ui-kit.exe.xyz`
115
-
116
- - **Repo secret `SITE_DEPLOY_SSH_KEY`** — done. An ed25519 keypair generated
117
- *by a human*, never by the agent (Hard gate 1). Public half goes in the
118
- VM's `~exedev/.ssh/authorized_keys`; private half goes in
119
- Settings → Secrets and variables → Actions, pasted directly — it should
120
- never appear in an agent's Bash context or a commit.
121
- - **Environment `production-site` reviewer gate — CONFIGURED** (verified
122
- live 2026-08-11, `gh api repos/<org>/<repo>/environments`:
123
- `protection_rules` carries `required_reviewers`). The `deploy` job in
124
- `deploy-site.yml` therefore blocks on a human approval after its dry-run
125
- job — the delete-adjudication gate this skill's hardened-deploy design
126
- assumes. Changing the reviewer set is operator-only (repo Settings →
127
- Environments → `production-site`) — no agent can configure it. Re-check
128
- the API output before trusting this line; it drifts with repo settings.
82
+ One-time CI setup for `ui-kit.exe.xyz` (the deploy SSH key, the
83
+ `production-site` reviewer gate) is in
84
+ [deploy-playbooks.md](references/deploy-playbooks.md)'s own section by
85
+ that name.
129
86
 
130
87
  ## Other playbooks (reference §-anchors)
131
88
 
@@ -161,24 +118,9 @@ rollback state: not-needed | rolled-back — <if rolled back, what triggered
161
118
  verdict: shipped | held — <one line>
162
119
  ```
163
120
 
164
- Filled example (a real cut, `gh run view 29586391343`):
165
-
166
- ```text
167
- Deploy Record
168
- tag / run id: site-v4 (workflow run 29586391343, 2026-07-17T14:03:15Z)
169
- dry-run deletes: see the run's dry-run job log for the class breakdown
170
- fixture verified: pass — CI's post-deploy verify step, run marked success
171
- render verified: pass — CI's post-deploy verify step, run marked success
172
- snapshot: CI pre-deploy hardlink step (deploy-site.yml)
173
- rollback state: not-needed
174
- verdict: shipped
175
- ```
176
-
177
- This example cites the run URL rather than restating its log inline — the
178
- record's job is to point at the evidence, not transcribe it; re-derive the
179
- dry-run/fixture/render lines from `gh run view <id> --log` if the detail is
180
- ever needed, don't assume this filled example's prose stays current with a
181
- run that already happened.
121
+ A filled worked example (a real cut) is in
122
+ [deploy-playbooks.md](references/deploy-playbooks.md)'s own "Deploy Record"
123
+ section.
182
124
 
183
125
  ## Hard gates
184
126
 
@@ -97,6 +97,22 @@ sudo vim /etc/<app>.env
97
97
  sudo systemctl restart <app>
98
98
  ```
99
99
 
100
+ ## One-time CI setup for `ui-kit.exe.xyz`
101
+
102
+ - **Repo secret `SITE_DEPLOY_SSH_KEY`** — done. An ed25519 keypair generated
103
+ *by a human*, never by the agent (Hard gate 1). Public half goes in the
104
+ VM's `~exedev/.ssh/authorized_keys`; private half goes in
105
+ Settings → Secrets and variables → Actions, pasted directly — it should
106
+ never appear in an agent's Bash context or a commit.
107
+ - **Environment `production-site` reviewer gate — CONFIGURED** (verified
108
+ live 2026-08-11, `gh api repos/<org>/<repo>/environments`:
109
+ `protection_rules` carries `required_reviewers`). The `deploy` job in
110
+ `deploy-site.yml` therefore blocks on a human approval after its dry-run
111
+ job — the delete-adjudication gate this skill's hardened-deploy design
112
+ assumes. Changing the reviewer set is operator-only (repo Settings →
113
+ Environments → `production-site`) — no agent can configure it. Re-check
114
+ the API output before trusting this line; it drifts with repo settings.
115
+
100
116
  ## Playbook: deploy an update
101
117
 
102
118
  **Push a tag matching `site-v*`** (or trigger `.github/workflows/deploy-site.yml`
@@ -282,3 +298,25 @@ Add new hosts here as they come online.
282
298
 
283
299
  Release engineering (`package-release`) builds and publishes artifacts; this
284
300
  playbook owns the deploy step that pushes them to the VM.
301
+
302
+ ## Deploy Record — a filled example
303
+
304
+ The schema lives in SKILL.md's own "The Deploy Record" section; this is a
305
+ worked example (a real cut, `gh run view 29586391343`):
306
+
307
+ ```text
308
+ Deploy Record
309
+ tag / run id: site-v4 (workflow run 29586391343, 2026-07-17T14:03:15Z)
310
+ dry-run deletes: see the run's dry-run job log for the class breakdown
311
+ fixture verified: pass — CI's post-deploy verify step, run marked success
312
+ render verified: pass — CI's post-deploy verify step, run marked success
313
+ snapshot: CI pre-deploy hardlink step (deploy-site.yml)
314
+ rollback state: not-needed
315
+ verdict: shipped
316
+ ```
317
+
318
+ This example cites the run URL rather than restating its log inline — the
319
+ record's job is to point at the evidence, not transcribe it; re-derive the
320
+ dry-run/fixture/render lines from `gh run view <id> --log` if the detail is
321
+ ever needed, don't assume this filled example's prose stays current with a
322
+ run that already happened.
@@ -2,17 +2,16 @@
2
2
  name: ssr-compatibility
3
3
  description: >-
4
4
  Answers why an AdiaUI component crashes, drops content, or renders wrong
5
- under SSR (linkedom/Astro consumers) — the four known failure shapes,
6
- what's fixed vs open, how to prove a fix under the linkedom shim gate. Use when
7
- asked "does this work under SSR", why a component crashes on
8
- attachInternals/ResizeObserver/adoptedStyleSheets/matchMedia/`instanceof Node`
9
- under a DOM shim, why
10
- table-ui/chart-ui/select-ui or a container CE renders empty or loses its
11
- nested children when server-rendered, whether it's safe to call
12
- getBoundingClientRect() synchronously in connectedCallback, or whether a
13
- browser-API shim/workaround can finally be deleted after a fix ships.
14
- ANSWERS only. NOT for implementing a fix (primitive-authoring) or consumer
15
- host/hydration wiring (host-wiring, adia-ui-factory plugin).
5
+ under SSR (linkedom/Astro) — the four known failure shapes, what's fixed
6
+ vs open, how to prove a fix under the linkedom shim gate. Use for "does
7
+ this work under SSR", why a component crashes on
8
+ attachInternals/ResizeObserver/adoptedStyleSheets/matchMedia/`instanceof
9
+ Node` under a DOM shim, why table-ui/chart-ui/select-ui or a container CE
10
+ renders empty or drops nested children server-rendered, whether
11
+ getBoundingClientRect() is safe in connectedCallback, or whether a shim
12
+ can be deleted after a fix ships. ANSWERS only. NOT for a fix
13
+ (primitive-authoring) or host/hydration wiring (host-wiring,
14
+ adia-ui-factory).
16
15
  disable-model-invocation: false
17
16
  user-invocable: false
18
17
  ---
@@ -29,36 +28,14 @@ this" and "what's the state of each shape's fix" — it never carries the fix it
29
28
 
30
29
  ## The four shapes, in one line each
31
30
 
32
- 1. **A browser-only API is called unconditionally → crash.** `attachInternals`,
33
- the four Observer constructors, `document.adoptedStyleSheets`. **Fixed** (gh#285).
34
- 2. **`connectedCallback` destructively re-stamps existing DOMsilent content loss.**
35
- The ORIGINAL diagnosis, narrowed to a static audit 2026-07-17 (gh#284) zero
36
- shipped components currently pair a non-null template with real light-DOM
37
- content, so this specific mechanism isn't exposed today; a forward audit catches
38
- a future regression. **Do not stop here** — see shape 2b, the actual live bug
39
- found investigating the same report.
40
- 2b. **Custom-element upgrade doesn't replay `attributeChangedCallback` for
41
- pre-existing attributes → reflected properties stuck at their class default.**
42
- The REAL mechanism behind gh#284's reported symptom. happy-dom and (by strong
43
- inference) linkedom both skip the custom-elements spec's upgrade-time
44
- attribute replay (§4.13.5 step 6); any `reflect: true` property seeded only
45
- from pre-parsed/SSR HTML never initializes. **Fixed 2026-07-18** (gh#284,
46
- PR #309) — `connectedCallback` now re-syncs every declared property from its
47
- live attribute before `connected()` runs.
48
- 3. **A connect-time layout measurement is treated as confirmed, not unknown.** A zero
49
- rect (shim, or real pre-layout connect) drives a wrong persistent decision.
50
- **Fixed for one component** (gh#286); the general pattern is unswept.
31
+ 1. **A browser-only API is called unconditionally → crash** (`attachInternals`, Observers, `adoptedStyleSheets`). **Fixed** (gh#285).
32
+ 2. **`connectedCallback` destructively re-stamps existing DOM → silent content loss.** ORIGINAL diagnosis, narrowed to a static audit 2026-07-17 (gh#284) — doesn't currently expose against any shipped component.
33
+ 2b. **Custom-element upgrade doesn't replay `attributeChangedCallback` for pre-existing attributesreflected properties stuck at class default.** The REAL mechanism behind gh#284's symptom. **Fixed 2026-07-18** (PR #309).
34
+ 3. **A connect-time layout measurement is treated as confirmed, not unknown.** **Fixed for one component** (gh#286); the general pattern is unswept.
35
+ 4. **Property-only components can't seed initial state from SSR HTML** (gh#288) — a feature gap, **CLOSED 2026-07-18** (table-ui's `data="[…]"` attribute).
51
36
 
52
- A fifth item, **property-only components can't seed initial state from SSR HTML**
53
- (gh#288), was a feature gap — **CLOSED 2026-07-18**. It was never actually blocked
54
- on shape 2/2b (table-ui/chart-ui/select-ui all use `static template = () => null`
55
- too), and the real scope was narrower than filed: `select-ui` already declares
56
- options via native `<option>` children, `chart-ui` already hydrates `.data` from a
57
- JSON `data="[…]"` attribute — only `table-ui`'s `.data` had no declarative form,
58
- now fixed the same way.
59
-
60
- Full symptom → root-cause → status detail, cited to the actual shipped/open code:
61
- [`references/failure-shapes.md`](references/failure-shapes.md).
37
+ Full symptom root-cause status detail, cited to the actual shipped/open
38
+ code: [failure-shapes.md](references/failure-shapes.md).
62
39
 
63
40
  ## Consult table
64
41
 
@@ -74,17 +51,12 @@ Full symptom → root-cause → status detail, cited to the actual shipped/open
74
51
 
75
52
  ## Deviation doctrine
76
53
 
77
- Every fix pattern this pack cites ([`guard-patterns.md`](references/guard-patterns.md))
78
- carries the reasoning for why it looks the way it does the no-op `ElementInternals`
79
- shim exists because leaving the field `undefined` would relocate a crash, not remove
80
- it; the linkedom shim gate (`scripts/dev/ssr-linkedom-smoke.mjs`, gh#1430) exists
81
- because happy-dom implements every API in the documented shape-1 cases that linkedom
82
- lacks (matchMedia, rAF, the Observers, `Node`/`Element` globals, rect APIs), so the
83
- unit suite is blind to that class by construction — deletion-based testing is the unit-level complement, not
84
- the proof.
85
- If a new case doesn't fit an existing pattern's reasoning, that's a signal to design a
86
- new pattern. Route it through `primitive-authoring` — don't force-fit the nearest existing
87
- shape.
54
+ Every fix pattern this pack cites ([guard-patterns.md](references/guard-patterns.md))
55
+ carries the reasoning for why it looks the way it does (e.g. the no-op
56
+ `ElementInternals` shim exists because `undefined` would relocate the crash,
57
+ not remove it). If a new case doesn't fit an existing pattern's reasoning,
58
+ that's a signal to design a new pattern route through `primitive-authoring`,
59
+ don't force-fit the nearest existing shape.
88
60
 
89
61
  ## Boundaries
90
62
 
@@ -104,25 +76,11 @@ shape.
104
76
 
105
77
  ## Worked example — the answer contract
106
78
 
107
- **Ask:** "`<text-ui>Adia Admin</text-ui>` renders as an empty tag in our SSR output —
108
- is this a known issue?"
109
-
110
- **Answer:** It matches shape 2's SYMPTOM (`connectedCallback` destructively
111
- re-stamping existing DOM), but shape 2 was narrowed on 2026-07-17 — verify against
112
- the CURRENT code before reusing the old answer, because this is exactly the case it
113
- no longer covers. `text-ui`'s `static template` (`packages/web-components/components/text/text.class.js`)
114
- is `() => null` — `connectedCallback`'s `if (result) stamp(result, this)` never
115
- enters the branch, so `stamp()` never touches `<text-ui>`'s children at all. This
116
- component isn't exposed to shape 2; something else is dropping the text — check
117
- whether `text-ui` is even registered server-side (a different, structural gap: is
118
- the tag defined before the SSR pass runs?), or whether another mutation (a parent
119
- re-render, `innerHTML` elsewhere) is clearing it. **The general lesson, not just
120
- this one component:** before answering "yes, known issue, shape 2" for ANY new
121
- report, grep the component's own `static template` — if it's the literal
122
- `() => null`, shape 2 cannot be the cause, no matter how closely the symptom
123
- matches the old description. [`failure-shapes.md`](references/failure-shapes.md)
124
- §2 has the full survey (150 components at the 2026-07 survey — the census has since grown) and cites exactly why every
125
- current children-accepting component is unaffected.
79
+ A "content vanished" report can match shape 2's symptom while shape 2
80
+ itself no longer applies (it was narrowed 2026-07-17) — the general lesson
81
+ (grep the component's `static template` before answering "known issue,
82
+ shape 2") plus a full worked ask/answer are in
83
+ [failure-shapes.md](references/failure-shapes.md) §2's own Worked example.
126
84
 
127
85
  ## Corpus of record
128
86
 
@@ -131,6 +131,29 @@ the attribute-upgrade-replay gap was real-and-live (now fixed in #309). A
131
131
  future "content vanished under SSR" report should check the attribute-replay
132
132
  mechanism FIRST — it's the one that was actually firing.
133
133
 
134
+ ### Worked example — answering a new "content vanished" report
135
+
136
+ **Ask:** "`<text-ui>Adia Admin</text-ui>` renders as an empty tag in our SSR
137
+ output — is this a known issue?"
138
+
139
+ **Answer:** It matches shape 2's SYMPTOM (`connectedCallback` destructively
140
+ re-stamping existing DOM), but shape 2 was narrowed on 2026-07-17 — verify
141
+ against the CURRENT code before reusing the old answer, because this is
142
+ exactly the case it no longer covers. `text-ui`'s `static template`
143
+ (`packages/web-components/components/text/text.class.js`) is `() => null` —
144
+ `connectedCallback`'s `if (result) stamp(result, this)` never enters the
145
+ branch, so `stamp()` never touches `<text-ui>`'s children at all. This
146
+ component isn't exposed to shape 2; something else is dropping the text —
147
+ check whether `text-ui` is even registered server-side (a different,
148
+ structural gap: is the tag defined before the SSR pass runs?), or whether
149
+ another mutation (a parent re-render, `innerHTML` elsewhere) is clearing
150
+ it. **The general lesson, not just this one component:** before answering
151
+ "yes, known issue, shape 2" for ANY new report, grep the component's own
152
+ `static template` — if it's the literal `() => null`, shape 2 cannot be the
153
+ cause, no matter how closely the symptom matches the old description. This
154
+ survey (150 components at the 2026-07 survey — the census has since grown)
155
+ cites exactly why every current children-accepting component is unaffected.
156
+
134
157
  ## 3 · A connect-time layout MEASUREMENT is meaningless before real layout exists
135
158
 
136
159
  **Symptom:** a component makes a decision (a boolean state, a mode, a snapped value)