@adia-ai/adia-ui-forge 0.8.40 → 0.8.42

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 +22 -0
  3. package/package.json +1 -1
  4. package/scripts/release-pretag-docs-gate +1 -1
  5. package/skills/a2ui-maintenance/SKILL.md +43 -6
  6. package/skills/a2ui-maintenance/references/anti-patterns.md +1 -1
  7. package/skills/a2ui-maintenance/references/chunk-authoring.md +2 -2
  8. package/skills/a2ui-maintenance/references/eval-diagnostics.md +1 -1
  9. package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +2 -2
  10. package/skills/a2ui-maintenance/references/pipeline-overview.md +2 -2
  11. package/skills/a2ui-maintenance/references/surface-lifecycle.md +73 -0
  12. package/skills/demo-audit/references/admin-shell-anatomy.md +1 -1
  13. package/skills/demo-audit/scripts/analyze.mjs +1 -1
  14. package/skills/package-release/SKILL.md +1 -1
  15. package/skills/package-release/references/cut-procedure.md +14 -4
  16. package/skills/package-release/references/gates-catalog.md +7 -1
  17. package/skills/package-release/scripts/gate-roster.mjs +28 -1
  18. package/skills/package-release/scripts/release-pack.mjs +43 -4
  19. package/skills/primitive-authoring/SKILL.md +1 -0
  20. package/skills/primitive-authoring/references/INDEX.md +2 -1
  21. package/skills/primitive-authoring/references/anti-patterns.md +27 -0
  22. package/skills/primitive-authoring/references/api-contract.md +39 -0
  23. package/skills/primitive-authoring/references/composite-demo-protocol.md +1 -1
  24. package/skills/primitive-authoring/references/css-patterns.md +7 -1
  25. package/skills/primitive-authoring/references/shell-patterns.md +1 -1
  26. package/skills/primitive-authoring/references/svg-authoring.md +282 -0
  27. package/skills/primitive-authoring/references/token-contract.md +47 -2
  28. package/skills/primitive-authoring/references/yaml-contract.md +239 -6
  29. package/skills/site-deployment/references/deploy-playbooks.md +12 -4
  30. package/skills/ssr-compatibility/SKILL.md +10 -5
  31. package/skills/ssr-compatibility/references/consumer-workarounds.md +14 -6
  32. package/skills/ssr-compatibility/references/failure-shapes.md +30 -7
  33. package/skills/ssr-compatibility/references/guard-patterns.md +28 -0
  34. package/skills/ssr-compatibility/references/status-ledger.md +13 -0
  35. package/skills/ssr-compatibility/references/test-without-linkedom.md +29 -14
@@ -2,7 +2,7 @@
2
2
 
3
3
  Authoritative source-of-truth fields for `packages/web-components/components/<name>/<name>.yaml` and `packages/web-modules/<cluster>/<name>/<name>.yaml`. The build pipeline (`scripts/build/components.mjs`) reads these yamls + emits sidecar JSON (`<name>.a2ui.json`) that feeds the docs site, the A2UI runtime registries, and consumer harnesses.
4
4
 
5
- This is the authoritative schema reference for the authoring lane. The full validator + JSON Schema lives at `scripts/schemas/component.yaml.schema.json` (referenced by every yaml's `$schema:` key). This file covers the human-facing contract: what each field means, when to use which value, and the canonical shape of a complete yaml.
5
+ This is the authoritative schema reference for the authoring lane. The JSON Schema lives at `scripts/schemas/component.yaml.schema.json` (referenced by every yaml's `$schema:` key). Amended 2026-08-16 per ADR-0057: that schema is the documented contract + IDE aid, not a run validator — no build step evaluates it against the yamls. The build-time checks that DO exist are hand-written throws in `compileComponent()` (`scripts/build/components.mjs`): a missing `component:` field, a `component: Surface` (reserved — the A2UI v1.0 implicit root container, SPEC REQ-011/gh#1353), a `status:` value outside the five-value enum (see §`status:` below), a missing `category:` field OR a `category:` value outside the twelve-value enum (see §`category:` below, ADR-0065), and malformed `a2ui.allowedParents`/`a2ui.allowedChildren` composition constraints (see §composition constraints below — plus a full-build cross-reference check that every referenced name is a real `component:` in the catalog). Every other schema constraint (`required: [name, tag, component, description]`, `minLength`, …) is IDE-visible only. This file covers the human-facing contract: what each field means, when to use which value, and the canonical shape of a complete yaml.
6
6
 
7
7
  ---
8
8
 
@@ -14,7 +14,7 @@ $schema: ../../../../scripts/schemas/component.yaml.schema.json
14
14
  name: UIMyComponent # Class name (PascalCase, UI-prefixed)
15
15
  tag: my-component-ui # Custom element tag (kebab-case, -ui-suffixed)
16
16
  component: MyComponent # Short component name (no UI- prefix)
17
- category: form # Category — form / display / layout / chrome / a2ui / shell
17
+ category: form # Category — see §category field below (ADR-0065, twelve-value enum)
18
18
  version: 1 # Schema version (always 1 for now)
19
19
  status: stable # Stability tier — see §status field below
20
20
  description: >-
@@ -26,13 +26,104 @@ props:
26
26
  events:
27
27
  … # Event schemas — fired by the component
28
28
  slots:
29
- … # Named slot semantics
29
+ … # Consumer-fillable light-DOM insertion points — see §slots vs parts below
30
+ parts:
31
+ … # Template-owned anatomy — see §slots vs parts below
30
32
  css-vars:
31
33
  … # CSS custom properties the component reads
32
34
  ```
33
35
 
34
36
  ---
35
37
 
38
+ ## `slots:` vs `parts:` — consumer-fillable vs template-owned anatomy (ADR-0067)
39
+
40
+ **Decision rule**: does an author (a human, or an LLM generating an A2UI
41
+ document) ever place their OWN content at this named span? If yes — even
42
+ with a stamped fallback when nothing is supplied — it's `slots:`. If the
43
+ component's own `render()`/template ALWAYS stamps it itself, from a prop or
44
+ attribute, and no author-supplied content is ever accepted there — it's
45
+ `parts:`. **Check element source, never the description prose alone**
46
+ (AGENTS.md: source wins) — a name that *sounds* internal
47
+ (`actions`, `text`, `leading`) can still be a real insertion point in a
48
+ given component; `table-toolbar.yaml`'s `actions` slot LOOKS stamped by
49
+ name but its `class.js` explicitly absorbs pre-existing `[slot="actions"]`
50
+ children (a real, author-fillable insertion point) — the opposite of
51
+ `check.yaml`'s `box`, which `static template = () => html\`<span
52
+ slot="box"></span>\`` stamps unconditionally every render.
53
+
54
+ Both keys share the identical `Slot` schema shape (`description:` required,
55
+ `fallback:` optional) — the only difference is which key an entry lives
56
+ under. `scripts/build/components.mjs` forwards both verbatim onto the
57
+ sidecar (`x-adiaui.slots` / `x-adiaui.parts`) with no other processing.
58
+
59
+ **Why the split matters — three real consumers read `slots:` and present
60
+ every entry as fillable, with no code-level filtering for anything under
61
+ `parts:`:**
62
+
63
+ - `packages/gen-ui/engine/retrieval/component-entry.js`'s
64
+ `serializeReference()` — feeds the LLM-facing `reference`-detail catalog
65
+ entry (MCP tools, `getComponentAPI()`).
66
+ - `packages/gen-ui/engine/compose/strategies/monolithic/_shared.js`'s
67
+ `adaptV09Component()` — feeds the monolithic engine's prompt catalog.
68
+ - `scripts/docs/anatomy-sweep.mjs`'s `genSlots()` — renders the docs-site
69
+ "slots" anatomy section.
70
+
71
+ A `parts:` entry never reaches any of the three above — moving template-owned
72
+ anatomy there is a structural fix, not a naming convention alone. An
73
+ existing entry under `slots:` that's actually template-owned (e.g. a
74
+ component predating this ADR) is a real bug: it advertises to an LLM that
75
+ filling it does something, when the component's own template replaces
76
+ whatever's there on the next render — the exact gh#284 destructive-replace
77
+ shape, applied to a *documented* slot instead of an undocumented one.
78
+
79
+ `scripts/dev/audit-slot-vocab-vs-css.mjs` (the yaml-vs-CSS `[slot="X"]`
80
+ cross-check) reads BOTH `slots:` and `parts:` — a `parts:` entry is still a
81
+ real `slot="X"` DOM attribute the component's own CSS may position, just
82
+ never author-fillable, so it stays in that audit's declared-vocabulary set.
83
+ `scripts/dev/audit-template-child-conflict.mjs` (the gh#284 container-shape
84
+ check) is unaffected either way — it only checks for a slot literally named
85
+ `default`.
86
+
87
+ ---
88
+
89
+ ## `a2ui.allowedParents:` / `a2ui.allowedChildren:` — composition constraints (SPEC REQ-011, gh#1353)
90
+
91
+ Optional keys inside the `a2ui:` block, alongside `rules:`. Each is a
92
+ non-empty list of catalog `component:` names (NOT tags) naming the direct
93
+ parents this component may sit under / the direct children it may contain.
94
+ The reserved name `Surface` (the A2UI v1.0 implicit root container) is legal
95
+ only in `allowedParents` and means "may sit at the surface root". **Omitted
96
+ means unconstrained** — never write an empty list (that would mean "allowed
97
+ nowhere"; the build refuses it).
98
+
99
+ ```yaml
100
+ a2ui:
101
+ allowedParents:
102
+ - Accordion # AccordionItem only makes sense inside an Accordion
103
+ rules:
104
+ - …
105
+ ```
106
+
107
+ **Authoring rule — verify against element source, exactly like the
108
+ `slots:`/`parts:` decision above.** Declare a constraint only when the
109
+ component's own source enforces or assumes it (e.g. `stepper.class.js`
110
+ queries `stepper-item-ui`; `segmented.class.js` warns on non-`segment-ui`
111
+ children). A parent that adopts items through wrappers (menu.class.js's
112
+ deliberate descendant query) must NOT constrain — a declared constraint
113
+ stricter than the source is a defect, not documentation.
114
+
115
+ Pipeline: `components.mjs` validates the shape per-yaml, cross-checks every
116
+ referenced name against the full catalog on a full build, and forwards the
117
+ lists onto `x-adiaui` → `catalog-a2ui_0_9.json`.
118
+ `scripts/build/derive-genui-catalog.mjs` then translates them into the
119
+ canonical v1.0 key space (yaml `Segmented` → catalog `SegmentedControl`) on
120
+ `base.json`/`adia-pack.json`, where the vendored `@genui/core` validator
121
+ enforces them (`UNALLOWED_PARENT`/`UNALLOWED_CHILD`). Module-tier yamls
122
+ (web-modules) carry constraints as dialect-catalog metadata only — modules
123
+ have no v1.0 sidecar.
124
+
125
+ ---
126
+
36
127
  ## `status:` field — stability tier
37
128
 
38
129
  **Required** for all new components. Existing components default to `stable` if unset, but new yamls MUST set this explicitly.
@@ -45,6 +136,8 @@ css-vars:
45
136
  | `deprecated` | Has a replacement; check the component's `related:` section. Docs site shows a `danger`-variant badge labeled "deprecated". |
46
137
  | `early-access` | Customer-preview tier; release notes gate. Docs site shows an `info`-variant badge labeled "early access". |
47
138
 
139
+ **Ratified, closed enum — compiler-enforced at build time, mirrored in the schema (ADR-0057, ratified 2026-08-15).** The five values above are the whole vocabulary; `draft` is NOT a value (the one `draft` in the estate, `embed-shell.yaml`, was corrected to `experimental` when the enum went live — a sixth value on a single occurrence is data-entry drift, not a vocabulary gap). Enforcement: `scripts/build/components.mjs:105` holds `STATUS_VALUES` and `compileComponent()` (`components.mjs:258-260`) throws on any out-of-enum `status:` at the same place it throws on a missing `component:` — so `npm run verify:components` (`node scripts/build/components.mjs --verify`, a member of the `npm run check` aggregate) hard-fails the yaml with a file-and-value error. An invalid status no longer merely skips a docs badge; it stops the build. `scripts/schemas/component.yaml.schema.json:23-27` declares the same enum (default `stable`) for the `$schema:` IDE contract, but no validator runs that file — the hand-synced constant in `components.mjs` is the live gate. `status` is orthogonal to the ADR-0050 L0–L4 tier ladder (tier = what a component is composed of; status = how much to trust its contract today), and nothing in `packages/gen-ui/engine/retrieval/` filters or ranks on it. Source: ADR-0057.
140
+
48
141
  **Guidance**:
49
142
 
50
143
  - Set `beta` or `experimental` at FIRST AUTHORING for any component that's not in the stable API contract yet. Don't default to `stable` and bump later — the badge is consumer-facing, and stable→beta is a downgrade signal.
@@ -57,6 +150,106 @@ css-vars:
57
150
 
58
151
  ---
59
152
 
153
+ ## `category:` field — functional grouping
154
+
155
+ **Required.** Every yaml sets this; `scripts/build/components.mjs` forwards it verbatim onto the sidecar as `x-adiaui.category`, and `packages/gen-ui/engine/retrieval/catalog.js` reads it from there for every YAML-backed component — no second, hand-maintained category list for anything with a yaml SoT. `catalog.js` does still carry one small, DELIBERATE exception: a 3-entry `PSEUDO_TYPE_CATEGORY` map (`section`/`header`/`footer` → `card-child`) for `@adia-ai/a2ui` registry pseudo-types that have no yaml SoT at all (v0.9 composition slot-children, not real primitives) — nothing to derive from, so this one small map stays hand-maintained by design, not drift.
156
+
157
+ | Value | When to use |
158
+ | --- | --- |
159
+ | `action` | A standalone, click-to-fire trigger (`button-ui`, `toggle-scheme-ui`). |
160
+ | `agent` | AI/agent-facing surfaces — chat, trace, tool output, tabular/chart data views (`chat-thread-ui`, `agent-trace-ui`, `table-ui`, `chart-ui`, `embed-ui`). |
161
+ | `container` | A chrome/wrapping surface that holds other content (`card-ui`, `modal-ui`, `drawer-ui`, `menu-ui`, `command-ui`). |
162
+ | `data` | Structured/tabular data display, not a full agent surface (`tree-ui`, `heatmap-ui`). |
163
+ | `display` | Passive content rendering — text, media, status glyphs (`text-ui`, `icon-ui`, `badge-ui`, `avatar-ui`, `link-ui`, `mark-ui`, `richtext-ui` — a non-editable renderer, not a form field). |
164
+ | `feedback` | Status/notification/progress communication (`spinner-ui`, `inline-message-ui`, `progress-ui`, `progress-row-ui`, `step-progress-ui`, `feed-ui`, `feed-item-ui`). |
165
+ | `form` | Data-entry composite/field-level components, not raw bindable controls (`field-ui`, `fields-ui`, `rating-ui`, `toggle-option-ui`). |
166
+ | `input` | Bindable form controls (`input-ui`, `select-ui`, `check-ui`, `switch-ui`, `textarea-ui`, `radio-ui`). |
167
+ | `layout` | Pure structural/spatial primitives — no content semantics of their own (`row-ui`, `col-ui`, `grid-ui`, `stack-ui`, `list-ui`). |
168
+ | `navigation` | Wayfinding/switcher controls, including a switcher family's child items (`breadcrumb-ui`, `pagination-ui`, `menu-item-ui`, `segmented-ui`/`segment-ui`, `tabs-ui`/`tab-ui`, `stepper-ui`/`stepper-item-ui`). `toggle-group-ui` is `navigation` too, but its child `toggle-option-ui` is `form` (a wrapper/item split, like `menu-ui`/`menu-item-ui` — not a same-category pair). |
169
+ | `shells` | Page-level app-shell composites (`simple-shell-ui` and its siblings). |
170
+ | `utility` | Non-visual/accessibility helpers (`skip-nav-ui`, `visually-hidden-ui`). |
171
+
172
+ **Ratified, closed enum — compiler-enforced at build time, mirrored in the schema (ADR-0065).** These twelve values are the whole vocabulary; the census that ratified them found 18 free-form values in live use (typos like `forms`/`data-display`, one-off singletons, and three named misclassifications) — all folded or corrected onto this set as part of the same change. Enforcement: `scripts/build/components.mjs` holds `CATEGORY_VALUES` and `compileComponent()` throws on a MISSING `category:` field (unlike `status:`, `category:` is required, not defaulted) as well as on any out-of-enum value, the same place and severity as the `status:` check above — so `npm run verify:components` hard-fails an invalid OR absent category. `scripts/schemas/component.yaml.schema.json`'s `category` enum mirrors this list for the `$schema:` IDE contract; the hand-synced constant in `components.mjs` is the live gate, same relationship as `status`. Source: ADR-0065.
173
+
174
+ **A sibling family (a wrapper + its child items, e.g. `tabs-ui`/`tab-ui`) is not required to share one category by default** — `menu-ui` (`container`) + `menu-item-ui` (`navigation`) is a deliberate, working split. Where a family's sibling values disagreed with no evident rationale, ADR-0065 unified them; new families should pick per-component, not assume unification is required.
175
+
176
+ **Sidecar emission**: `x-adiaui.category` field in `<name>.a2ui.json`. `packages/gen-ui/engine/retrieval/catalog.js`'s `buildCatalog()` reads this directly per entry — no separate registration step.
177
+
178
+ ---
179
+
180
+ ## Semantic color-family axis — two role-classes, two names (ADR-0044, ADR-0064)
181
+
182
+ Every component carries at most one STYLE axis and one FAMILY axis (the
183
+ semantic color family: `default | info | success | warning | danger`,
184
+ `+primary` where the role-class already carries brand emphasis), and every
185
+ enum value belongs to exactly one axis. The family axis's ATTRIBUTE NAME is
186
+ decided by role-class — never one universal name (a same-name meaning-flip
187
+ is a silent-failure migration and poisons the trained corpus, per ADR-0044
188
+ LLD §3):
189
+
190
+ - **Role-class A — `variant` is unclaimed:** the family axis is named
191
+ `variant`. Badge/tag's ratified shape (ADR-0044), plus `rating-ui` and the
192
+ `variant`-only siblings (inline-message, feed-item, empty-state, menu-item,
193
+ progress-row).
194
+ - **Role-class B — an existing identity/style axis already claims the
195
+ selector slot:** the family axis is named `color`. Button's shape
196
+ (`variant`=style, `color`=family), extended by ADR-0064 to `text-ui`
197
+ (`variant`=typography role), `chart-ui`/`heatmap-ui` (`type`=kind),
198
+ `icon-ui` (`weight`=glyph style), `spinner-ui` (`variant`=animation), and
199
+ `toggle-scheme-ui`. Renames owed by this ruling (follow-on build, gh#1376
200
+ — not yet landed): `icon-ui[tone]` → `[color]`, `heatmap-ui[colorScheme]`
201
+ → `[color]`, `spinner-ui[tone]` → `[color]`.
202
+
203
+ `accent` is RETIRED from the family enum everywhere — ADR-0044 removed it
204
+ from badge/tag/button; ADR-0064 removes it from the seven stragglers
205
+ (text, chart, icon, heatmap, rating, spinner, toggle-scheme) with no
206
+ replacement value. Never mint `accent` in a new enum.
207
+
208
+ Two ratified non-family exceptions — the name without the semantics:
209
+
210
+ - `swatch-ui[color]` / `noodles-ui[color]` — an arbitrary CSS color string,
211
+ not a semantic enum (ADR-0054 §11 exemption, unchanged).
212
+ - `spinner-ui[color]` (post-rename) — a closed contrast-mode enum
213
+ (`current | subtle | inverse`), NOT the family vocabulary; never assume it
214
+ accepts `info`/`success`/`warning`/`danger` by analogy.
215
+
216
+ ---
217
+
218
+ ## Catalog tiers — L0–L4, `origin`, and the promotion rule (ADR-0050, ADR-0066)
219
+
220
+ ADR-0050's L0–L4 ladder is the ONLY tier grammar — never mint a second
221
+ manifest format. ADR-0066 refines it three ways:
222
+
223
+ - **`origin: primitive | module` on every L0 entry** — both YAML source
224
+ roots (web-components primitives AND web-modules composites) compile into
225
+ the same `catalog-a2ui_0_9.json` through one shared contract;
226
+ `derive-catalog-tiers.mjs` stamps which root an entry came from onto
227
+ `tier-index.json`. A module is legitimately a member of TWO rungs at two
228
+ grains: its component API (props/events/slots) is L0, its assembled shell
229
+ composition is L3 — ruled correct, not a modeling defect.
230
+ - **The promotion rule, stated once:** patterns/zettel compositions are the
231
+ SOLE promotion source, and they enter the ladder at exactly one point —
232
+ pattern → L1 widget, through `curate-l1-widgets.mjs`'s gates (which writes
233
+ only `l1-widgets.json`). The higher rungs (L1 → L2 → L3 → L4) are AUTHORED
234
+ edges — hand-written `tiers/l*-*.json` manifests whose `composes` reference
235
+ the rung below, reserved/unblocked per ADR-0050's own phasing; a module's
236
+ L3 membership comes from an authored L3 manifest, never from its yaml
237
+ (which contributes only the L0 entry + `origin`). There is no
238
+ patterns↔module edge, primitives and modules never "promote" into each
239
+ other, and nothing promotes automatically or in reverse — curation is the
240
+ one-way valve (ADR-0050: "corpus derives from catalogs, never the
241
+ reverse").
242
+ - **The two pattern-facing outputs stay separate by design:**
243
+ `site/patterns-index.json` / `pattern-index.md` are a generated,
244
+ developer-facing index over the FULL pattern/template census, intentionally
245
+ independent of the L0–L4 machine-validated schema — a different audience,
246
+ never a convergence gap to "fix".
247
+
248
+ (`status:` above is orthogonal to the tier ladder — tier = what an entry is
249
+ composed of; status = how much to trust its contract today.)
250
+
251
+ ---
252
+
60
253
  ## `props:` field — prop schemas
61
254
 
62
255
  Each prop is a top-level key inside `props:`. The full prop schema:
@@ -146,7 +339,7 @@ string-list schema is the exact silent-mismatch shape.
146
339
  | --- | --- | --- |
147
340
  | `[data-col]` | `display: flex; flex-direction: column; gap: var(--page-grid-gap)` | column layout helper |
148
341
  | `[data-row]` | `display: flex; align-items: center; gap: var(--page-grid-gap)` | row layout helper |
149
- | `[data-grid]` | `display: grid; grid-template-columns: 1fr 1fr` (or `1fr 1fr 1fr` for `data-grid="3"`) | 2- or 3-col grid helper |
342
+ | `[data-layout-grid]` | `display: grid; grid-template-columns: 1fr 1fr` (or `1fr 1fr 1fr` for `data-layout-grid="3"`) | 2- or 3-col grid helper |
150
343
  | `[data-actions]` | `display: flex; align-items: center; gap: var(--page-actions-gap)` | action button cluster |
151
344
  | `[data-spacer]` | `flex: 1` | flex spacer for pushing content to edges |
152
345
 
@@ -160,6 +353,46 @@ string-list schema is the exact silent-mismatch shape.
160
353
 
161
354
  ---
162
355
 
356
+ ## `data-msg-*` — exempt component-side config family (gh#1332/#1464, ADR-0060)
357
+
358
+ `data-msg-required` / `data-msg-pattern` / `data-msg-minlength` /
359
+ `data-msg-maxlength` / `data-msg-min` / `data-msg-max` / `data-msg-bad-input`
360
+ are read directly by form-associated components (`core/form.js`'s shared
361
+ `UIFormElement` validation path, plus `input`, `select`, `tags-input`,
362
+ `code`, `date-range-picker`, `datetime-picker`, and
363
+ `payment-method-form.class.js`) to override a native constraint-violation's
364
+ default message with a consumer-supplied string.
365
+
366
+ **Disposition: EXEMPT — never declared as a yaml `states:`/`props:` entry.**
367
+ Both yaml surfaces this contract offers are the wrong shape for this family:
368
+
369
+ - `states:` declares **presence-boolean host state the component itself
370
+ reflects outward** (idle/loaded/error, this file's own §Reserved section's
371
+ neighbor pattern) — `data-msg-*` carries no state at all; it is a
372
+ consumer-authored string the component only ever *reads*, never sets.
373
+ - `props:` would need one string prop per validation-message key, repeated
374
+ across every one of the 7+ consuming components — but the read path is
375
+ `core/form.js`'s shared mixin, not any single component's own yaml SoT.
376
+ Declaring it per-component would multiply one shared mixin contract across
377
+ every consumer's yaml with no single owning SoT to declare it once — a
378
+ cross-cutting mixin-contract change, not a per-component yaml edit.
379
+
380
+ ADR-0060's own boundary discriminator (§Decision 3) already places this
381
+ family outside the trait-tier `data-*` ratification and explicitly routes it
382
+ "to gh#1332's Category A/D triage for its own converge-or-ratify call" — this
383
+ section IS that call. The family stays `data-*`, undeclared in any yaml,
384
+ with its contract documented at the shared source instead: `core/form.js`'s
385
+ own header JSDoc (the mixin all consumers share) and the canonical
386
+ `form-system` pattern doc (`packages/web-components/patterns/form-system/
387
+ form-system.examples.html`, mirrored at `site-a2ui/pages/
388
+ site__patterns__form-system.a2ui.json`) — both already enumerate the full
389
+ family with a worked example. A future architectural pass that wants to
390
+ promote this to a declared per-component contract needs its own ADR (the
391
+ scope is a mixin-wide contract change, not a small-ticket edit); nothing
392
+ here forecloses that, it only records today's call.
393
+
394
+ ---
395
+
163
396
  ## Build pipeline
164
397
 
165
398
  ```bash
@@ -171,7 +404,7 @@ node scripts/build/components.mjs --verify # same as above, direct invocation
171
404
  The build:
172
405
 
173
406
  1. Reads every `<name>.yaml` under `packages/web-components/components/` and `packages/web-modules/<cluster>/`
174
- 2. Validates against `scripts/schemas/component.yaml.schema.json`
407
+ 2. Hand-checks the source yaml in `compileComponent()` — missing `component:` and out-of-enum `status:` both throw (`components.mjs:255-260`). Amended 2026-08-16 per ADR-0057: it does NOT run `scripts/schemas/component.yaml.schema.json` as a validator (an earlier revision of this list claimed it did); the schema file is documentation + IDE contract only.
175
408
  3. Emits `<name>.a2ui.json` (the sidecar) co-located with the yaml + js + css
176
409
  4. Emits the `traits/_catalog.json` aggregate
177
410
  5. `--verify` mode: re-runs steps 1-4 in-memory and fails if any sidecar drifts from disk content (CI hard-fail)
@@ -263,4 +496,4 @@ After the playbook, the component is consumable by the docs site, the A2UI runti
263
496
  - [css-patterns.md](css-patterns.md) — light-DOM CSS cascade rules
264
497
  - [api-contract.md](api-contract.md) — props/events/slots conventions
265
498
  - [authoring-cycle.md](authoring-cycle.md) — the 5-step authoring procedure
266
- - `scripts/schemas/component.yaml.schema.json` — JSON Schema (authoritative)
499
+ - `scripts/schemas/component.yaml.schema.json` — JSON Schema (documented contract + IDE aid; not run as a validator — `compileComponent()` enforces only `component:` and the `status` enum, see §Build pipeline)
@@ -139,10 +139,18 @@ itself is unavailable.
139
139
  - **`build:site` copies packages but does NOT rebuild their dist bundles** — after
140
140
  component `.css`/`.js` source changes, rebuild first (`npm run build -w
141
141
  @adia-ai/llm`, then `npm run build:bundles`) or the deployed bundles are stale.
142
- - **`scripts/build/site.mjs` is per-package (`copyX()` functions), not glob-based** —
143
- every new or renamed package needs its own copy fn or `dist/` silently omits it
144
- (local Vite works, prod 404s; caused the v0.3.0 `@adia-ai/llm` + `a2ui-runtime`
145
- deploy gaps). After any package add/rename, check `site.mjs` before deploying.
142
+ - **Package registration is one manifest, `scripts/build/site-package-registry.mjs`
143
+ (ADR-0062)** every `@adia-ai/*` package the site ships is one registry entry, from
144
+ which the dist copy, BOTH importmaps (one shared `renderImportMapBlock()` the
145
+ `site/index.html` copy is a generated artifact, `npm run build:site-importmap`),
146
+ the `dist/node_modules` symlinks, and the CI build order all derive. The old
147
+ failure class (per-package `copyX()` hand-edits; local Vite works, prod 404s —
148
+ v0.3.0 llm/a2ui-runtime, v0.8.27 persona+agent) is gated mechanically:
149
+ `check:site-packages-registered` scans every shipped source root for an
150
+ unregistered bare `@adia-ai/*` import and fails naming file:line, alongside
151
+ `check:site-importmap-fresh` and `check:deploy-workflow-build-order` — all three
152
+ in `npm run check` and early in `deploy-site.yml`. After any package add/rename,
153
+ add the registry entry; the gates say the rest.
146
154
  2. **Dry-run first, and adjudicate every delete — BEFORE the real rsync, never after:**
147
155
  ```sh
148
156
  rsync -azni --delete --exclude='packages/gen-ui/a2ui/corpus/feedback/' \
@@ -3,9 +3,10 @@ name: ssr-compatibility
3
3
  description: >-
4
4
  Answers why an AdiaUI component crashes, drops content, or renders wrong
5
5
  under SSR (linkedom/Astro consumers) — the four known failure shapes,
6
- what's fixed vs open, how to test without a linkedom install. Use when
6
+ what's fixed vs open, how to prove a fix under the linkedom shim gate. Use when
7
7
  asked "does this work under SSR", why a component crashes on
8
- attachInternals/ResizeObserver/adoptedStyleSheets under a DOM shim, why
8
+ attachInternals/ResizeObserver/adoptedStyleSheets/matchMedia/`instanceof Node`
9
+ under a DOM shim, why
9
10
  table-ui/chart-ui/select-ui or a container CE renders empty or loses its
10
11
  nested children when server-rendered, whether it's safe to call
11
12
  getBoundingClientRect() synchronously in connectedCallback, or whether a
@@ -64,10 +65,10 @@ Full symptom → root-cause → status detail, cited to the actual shipped/open
64
65
  | Ask | Answer from |
65
66
  | --- | --- |
66
67
  | "why does `<text-ui>`/`<avatar-ui>`/a container lose its content under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §2 — shape 2, NARROWED (doesn't currently reproduce against any shipped component; see the survey before assuming a new report fits this shape) |
67
- | "why does this crash / throw at construction under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §1 — shape 1, FIXED; the exact guard shape to copy for a NEW instance is [`guard-patterns.md`](references/guard-patterns.md) §1 |
68
+ | "why does this crash / throw at import, construction or connect under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §1 — shape 1, FIXED twice (gh#285, gh#1430/#1436) and now GATED by `scripts/dev/ssr-linkedom-smoke.mjs`; the exact guard shape to copy for a NEW instance is [`guard-patterns.md`](references/guard-patterns.md) §1 — feature-detect the API, never `typeof window` |
68
69
  | "is this connect-time `getBoundingClientRect()`/measurement read safe" | [`failure-shapes.md`](references/failure-shapes.md) §3 + [`guard-patterns.md`](references/guard-patterns.md) §3 — the fixed component's exact shape, and the "unknown ≠ confirmed" principle to apply elsewhere |
69
70
  | "what's fixed vs still open for SSR support" | [`status-ledger.md`](references/status-ledger.md) — re-verify against `gh issue view` before trusting it, it drifts |
70
- | "how do I test an SSR gap we have no linkedom installed" | [`test-without-linkedom.md`](references/test-without-linkedom.md) — the delete/try/finally pattern, and what it does NOT prove |
71
+ | "how do I test / prove an SSR gap or fix" | [`test-without-linkedom.md`](references/test-without-linkedom.md) — run the linkedom shim gate first (`node scripts/dev/ssr-linkedom-smoke.mjs`, the consumer's exact global surface), then the unit-level delete/try/finally pattern and what it does NOT prove |
71
72
  | "what's the consumer's current workaround, and can they drop it yet" | [`consumer-workarounds.md`](references/consumer-workarounds.md) |
72
73
  | "table/chart/select renders empty in the SSR response" | [`failure-shapes.md`](references/failure-shapes.md) §4 — shape 5, CLOSED (table-ui's `data="[…]"` attribute); check whether the reporting component is registered server-side first if it still reproduces |
73
74
 
@@ -76,7 +77,11 @@ Full symptom → root-cause → status detail, cited to the actual shipped/open
76
77
  Every fix pattern this pack cites ([`guard-patterns.md`](references/guard-patterns.md))
77
78
  carries the reasoning for why it looks the way it does — the no-op `ElementInternals`
78
79
  shim exists because leaving the field `undefined` would relocate a crash, not remove
79
- it; deletion-based testing exists because this repo has no `linkedom` devDependency.
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.
80
85
  If a new case doesn't fit an existing pattern's reasoning, that's a signal to design a
81
86
  new pattern. Route it through `primitive-authoring` — don't force-fit the nearest existing
82
87
  shape.
@@ -21,12 +21,20 @@ and force-assigned `document.adoptedStyleSheets = []`. Two named fragilities:
21
21
  `globalThis.ResizeObserver` affects EVERY component and every other library in the
22
22
  same process, not just AdiaUI's.
23
23
 
24
- **Status as of gh#285's fix (PR #292, merged 2026-07-17): this shim should no longer
25
- be necessary** — the framework guards its own call sites now. This pack's own routing
26
- corpus / the issue thread is where confirmation from the consumer would land; don't
27
- assume it's been removed without checking the issue's comment thread for that
28
- confirmation, since a consumer removing a workaround is THEIR change, not something
29
- this fix does automatically.
24
+ **Status as of gh#285's fix (PR #292, merged 2026-07-17): the AdiaUI-facing sections
25
+ of this shim are no longer necessary** — the framework guards its own call sites now.
26
+ **Consumer-confirmed 2026-08-17 (gh#1430 §Additional context, against 0.8.40):**
27
+ `attachInternals`, the four Observer stubs and `adoptedStyleSheets` are all confirmed
28
+ unnecessary. **But gh#285's closing comment over-reached** in saying the shim "as a
29
+ whole" can go: two of its sections patch `CustomElementRender.prototype` — a
30
+ `setAttribute` null-guard and a `renderShadow` null-guard — i.e. they patch
31
+ `custom-elements-ssr` itself, which reads `shadowRoot.innerHTML` unconditionally and
32
+ AdiaUI is light-DOM-only, so `shadowRoot` is always `null`. Remove those two and every
33
+ SSR'd fixture dies with `Cannot read properties of null (reading 'innerHTML')`. No fix
34
+ in this repo can retire them; they belong to the renderer. Tell the next consumer that
35
+ explicitly rather than "the shim is unnecessary". (Also from that trial: `matchMedia`
36
+ was a further AdiaUI-side gap the #292 sweep missed — fixed by gh#1430, so a
37
+ `matchMedia` stub is not needed either from the version carrying it.)
30
38
 
31
39
  ## Attribute-only SSR registration restriction (gh#284 — likely no longer necessary, unconfirmed)
32
40
 
@@ -19,14 +19,37 @@ or a write to `document.adoptedStyleSheets`.
19
19
  not a partial/quirky implementation, an absence. Any unconditional call throws
20
20
  `TypeError` or `ReferenceError` (undefined global) immediately.
21
21
 
22
- **Status: FIXED** (gh#285, PR #292, merged 2026-07-17). `UIElement`'s constructor
23
- (`packages/web-components/core/element.js`) plus a sweep of component/trait/module
24
- files had this shape — the per-file tally is
22
+ **Status: FIXED, twice — and now GATED.** First wave gh#285 (PR #292, merged
23
+ 2026-07-17): `UIElement`'s constructor (`packages/web-components/core/element.js`)
24
+ plus a sweep of component/trait/module files — the per-file tally is
25
25
  [`status-ledger.md`](status-ledger.md)'s #285 row (the ledger, not this line, is
26
- the count of record). The fix pattern (feature-detect + fallback matched to how the reference is
27
- used downstream) is [`guard-patterns.md`](guard-patterns.md) apply that pattern to
28
- any NEW file that construct-calls one of these APIs; don't re-derive the shape from
29
- scratch.
26
+ the count of record). Second wave gh#1430 + gh#1436 (2026-08-17, found by adiav2's
27
+ first SSR admission trial at 0.8.40): the sweep had missed
28
+ - **environment-detection instead of feature-detection at MODULE scope**
29
+ `core/responsive.js` guarded `window.matchMedia(...)` behind `typeof window !==
30
+ 'undefined'`; linkedom HAS a `window`, just no `matchMedia` and no numeric
31
+ `innerWidth`, so six components (`block`/`col`/`grid`/`row`/`text`/`demo-toggle`)
32
+ could not be IMPORTED server-side at all;
33
+ - **bare `instanceof Node|Element|HTMLElement`** (15 sites — `core/template.js`
34
+ `applyValue`, table/list-window renderer results, event-target checks) — a
35
+ `ReferenceError`, not `false`, because custom-elements-ssr installs ONLY
36
+ `HTMLElement` as a global (never `Node`, `Element`, `Text`, `DocumentFragment`);
37
+ now `core/dom.js` `isNode()`/`isElement()` (structural `nodeType` tests);
38
+ - **`requestAnimationFrame` / `MutationObserver` / `getBoundingClientRect` at
39
+ connect** in feed-item/toast, noodles, preview, toolbar (rAF), nav-group,
40
+ stepper (MutationObserver — the #292 sweep guarded four Observer sites, not
41
+ these two) and nav-ui (`getBoundingClientRect` is absent, not zero, on linkedom).
42
+
43
+ **Why happy-dom could not see any of it:** it implements every one of those APIs.
44
+ The gate that closes the class is `scripts/dev/ssr-linkedom-smoke.mjs`, run by
45
+ `packages/web-components/test/ssr-linkedom-smoke.test.js` — a real `linkedom`
46
+ devDependency installing EXACTLY the six globals `custom-elements-ssr/server-shim.js`
47
+ installs, then importing every `components/*/*.js` entry and constructing +
48
+ connecting every registered tag (124/124 import, every tag renders). A new
49
+ shape-1 instance fails that test, not a consumer's build. The fix pattern
50
+ (feature-detect + fallback matched to how the reference is used downstream) is
51
+ [`guard-patterns.md`](guard-patterns.md) — apply it to any NEW call site; don't
52
+ re-derive the shape from scratch, and don't guard on `typeof window`.
30
53
 
31
54
  ## 2 · `connectedCallback` destructively re-stamps existing DOM → content loss
32
55
 
@@ -51,6 +51,34 @@ const observer = typeof MutationObserver !== 'undefined'
51
51
  // every later use: observer?.observe(...) / bootstrap() no-ops via the same check
52
52
  ```
53
53
 
54
+ **(d) Feature, not environment — `typeof window` is NOT a guard (gh#1430).**
55
+ linkedom defines `window` (an object) but neither `matchMedia` nor a numeric
56
+ `innerWidth`, so `typeof window !== 'undefined'` passes and the call still throws —
57
+ at MODULE scope that takes every importer off the server. Test the member:
58
+ ```js
59
+ // packages/web-components/core/responsive.js
60
+ const w = typeof window !== 'undefined' ? window.innerWidth : undefined;
61
+ if (typeof w !== 'number') return 'lg'; // fallback
62
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { … }
63
+ ```
64
+
65
+ **(e) `instanceof <DOM constructor>` — use `core/dom.js` (gh#1436).** Only
66
+ `HTMLElement` is a global under custom-elements-ssr; `v instanceof Node` /
67
+ `Element` / `Text` / `DocumentFragment` is a `ReferenceError`, not `false`.
68
+ `isNode(v)` / `isElement(v)` test `nodeType` structurally (and also accept nodes
69
+ from another realm, which the constructor identity check never did):
70
+ ```js
71
+ import { isNode, isElement } from '../../core/dom.js';
72
+ if (isNode(result)) cell.replaceChildren(result); // renderer output
73
+ const row = isElement(e.target) ? e.target.closest('…') : null; // event target
74
+ ```
75
+ `isElement` is deliberately the SUPERSET of a former `instanceof HTMLElement` (it
76
+ admits SVG elements too) — every swept site only needed "an element", none needed
77
+ "an HTML element specifically". Absent-method form of the same idea:
78
+ `typeof this.getBoundingClientRect === 'function' && …` (nav-ui) — linkedom has no
79
+ rect API at all, so an unguarded call is a TypeError, not a zero rect (that zero-rect
80
+ case is §3).
81
+
54
82
  **The `this.internals` special case — a shim, not `undefined`.** `UIElement`'s
55
83
  constructor (`packages/web-components/core/element.js`) needed a DIFFERENT answer
56
84
  than "guard and leave unset," because ~12 files across the framework call
@@ -1,5 +1,15 @@
1
1
  # SSR support — status ledger
2
2
 
3
+ **Filing a new downstream SSR-seam report?** Use the `ssr` label plus
4
+ [`.github/ISSUE_TEMPLATE/ssr-seam-report.md`](../../../../../../.github/ISSUE_TEMPLATE/ssr-seam-report.md)
5
+ (gh#1384) — it captures the resolved `@adia-ai/web-components` version, the
6
+ downstream kit version, the component + fixture, an SSR-vs-CSR diff or
7
+ minimal repro, and a cosmetic/behavioral/blocked-upstream classification, so
8
+ a report lands triageable instead of free-form (the archaeology the #284/#288
9
+ rows below needed). `docs/ops/spec/spec-ssr-kit.md` is referenced by some
10
+ downstream reports but does not exist in this repo — it lives in adiav2's own
11
+ repo (`@adiav2/ssr-kit`'s spec); do not look for it here.
12
+
3
13
  **This file drifts fast — re-verify against the live issue tracker
4
14
  (`gh issue view <n>`) before trusting it for anything beyond a quick orientation.**
5
15
  Every row was true as of 2026-07-17; a closed issue below may have re-opened, a PR
@@ -9,6 +19,9 @@ the territory — the territory is `gh issue list` / `gh issue view`.
9
19
  | Issue | Failure shape | Status | Shipped in | Notes |
10
20
  |---|---|---|---|---|
11
21
  | [#285](https://github.com/adiahealth/gen-ui-kit/issues/285) | §1 — browser-only API called unconditionally | **CLOSED** | PR #292 (merged 2026-07-17) | `UIElement` constructor + `adoptStyles()` + 22 component/trait/module files guarded; 10 more already correct from an earlier pass |
22
+ | [#1430](https://github.com/adiahealth/gen-ui-kit/issues/1430) | §1 — browser-only API called unconditionally (module scope: `window.matchMedia` behind a `typeof window` guard) | **FIXED 2026-08-17** | `core/responsive.js` feature-detects `matchMedia` + `innerWidth`; `swiper.class.js` `play()` guarded; `scripts/dev/ssr-linkedom-smoke.mjs` + `test/ssr-linkedom-smoke.test.js` gate the whole catalog under linkedom | 6 components (`block`/`col`/`grid`/`row`/`text`/`demo-toggle`) were un-importable server-side; 118→124 of 124. Side findings on the issue: gh#285's "shim no longer necessary" over-reached (two of the consumer's shim sections patch `custom-elements-ssr` itself, not this library — see `consumer-workarounds.md`); `audit-template-child-conflict.mjs` isn't shipped in the package. |
23
+ | [#1436](https://github.com/adiahealth/gen-ui-kit/issues/1436) | §1 — bare `instanceof Node/Element/HTMLElement` (ReferenceError under linkedom, only `HTMLElement` is global) | **FIXED 2026-08-17** | `core/dom.js` `isNode()`/`isElement()`; 15 sites swept; plus rAF/MutationObserver/getBoundingClientRect guards in feed, noodles, preview, toolbar, nav-group, stepper, nav found by the same gate's render sweep | Same PR as #1430 — one root-cause class. |
24
+ | [#1435](https://github.com/adiahealth/gen-ui-kit/issues/1435) | reported as §2 — check/radio/switch "lose" a `<span slot="label">` child | **NOT an SSR seam** (2026-08-17) | yaml/a2ui `slots` descriptions clarified for the three (separate follow-up PR — a docs-contract change, not a shape-1 guard) | The fixture is not a supported form in CSR either: `label` on all three is ATTRIBUTE-driven (`label="…"` → `aria-label` + `::after content: attr(label)` / template-owned span); a late CSR upgrade replaces the foreign child the same way. The yaml `slots:` entries name template-OWNED anatomy parts (`box`/`dot`/`track`/`thumb`/`label`), which a downstream reimplementation of the audit read as consumer insertion points — the audit's own definition (`slots.default` + non-null template) is what "consumer children" means here. |
12
25
  | [#286](https://github.com/adiahealth/gen-ui-kit/issues/286) | §3 — connect-time measurement treated as confirmed | **CLOSED** (for `admin-sidebar` specifically) | PR #290 (merged 2026-07-17) | The GENERAL pattern (any other component reading a rect/size synchronously at connect) is NOT swept — only this one instance is fixed |
13
26
  | [#284](https://github.com/adiahealth/gen-ui-kit/issues/284) | §2 — destructive `stamp()` on connect | **CLOSED 2026-07-18 — narrowed AND separately fixed** | `scripts/dev/audit-template-child-conflict.mjs` (PR #295) + `packages/web-components/core/element.js` connectedCallback resync (PR #309) | Two distinct findings. (1) The ORIGINAL diagnosis (destructive `stamp()`) was narrowed 2026-07-17: every component the issue names has `static template = () => null`, so `stamp()` never runs for them — zero shipped components exposed; a static audit gate catches a future regression instead of a lifecycle rewrite. (2) A SEPARATE, real, live bug was found in the same investigation area and fixed 2026-07-18: happy-dom/linkedom don't replay `attributeChangedCallback` for attributes already present at custom-element upgrade (spec §4.13.5 step 6) — any `reflect: true` property seeded only from pre-parsed/SSR HTML stayed at its class default after upgrade (e.g. `<nav-item-ui text="Profile">` rendered with an empty label). `connectedCallback` now re-syncs every declared property from its live attribute before `connected()` runs. See `failure-shapes.md` §2 for the full narrative. |
14
27
  | [#288](https://github.com/adiahealth/gen-ui-kit/issues/288) | §4 — property-only components can't seed from SSR HTML | **CLOSED 2026-07-18** | `table.class.js` `data="[…]"` attribute hydration | Never actually blocked on #284 (table-ui/chart-ui/select-ui all use `static template = () => null`, so the stamp() question never applied). Scope was narrower than filed: `select-ui` already parsed declarative `<option>` children, `chart-ui` already hydrated `.data` from a `data="[…]"` attribute — only `table-ui`'s `.data` had no declarative form. Fixed in the same attribute-hydration shape as chart-ui, not the JSON-script-child form originally proposed. |
@@ -1,10 +1,24 @@
1
- # Testing an SSR gap without a real linkedom install
1
+ # Testing an SSR gap the linkedom gate first, then the deletion pattern
2
2
 
3
- This repo has **no `linkedom` devDependency** (checked 2026-07-17: not in
4
- `package.json` at any workspace level). The test suite runs on `happy-dom`/`jsdom`
5
- (whichever `vitest` is configured with), and BOTH of those implement most of the APIs
6
- linkedom lacks so a naive test using the default environment will not reproduce an
7
- SSR-shaped bug. Two consequences:
3
+ **UPDATE 2026-08-17 (gh#1430/#1436): this repo now HAS a `linkedom` root
4
+ devDependency and a shipped shim gate.** `scripts/dev/ssr-linkedom-smoke.mjs`
5
+ installs exactly the six globals `custom-elements-ssr/server-shim.js` installs
6
+ (document, window, customElements, HTMLElement, Event, CustomEvent and nothing
7
+ else: no Node/Element/Text/DocumentFragment, no matchMedia, no
8
+ requestAnimationFrame, no getBoundingClientRect), imports every
9
+ `components/*/*.js` entry, and constructs + connects every registered tag the way
10
+ `CustomElementRender` does; `packages/web-components/test/ssr-linkedom-smoke.test.js`
11
+ runs it as a Node child process and fails on any import or render throw. **For a
12
+ shape-1 question ("does this crash under SSR"), run that first** — `node
13
+ scripts/dev/ssr-linkedom-smoke.mjs` — it is the consumer's environment, not an
14
+ approximation. Add a row to its `FIXTURES` table when a guard lands on a branch only
15
+ some attribute value reaches (`swiper-ui[autoplay]` is the model). The rest of this
16
+ file is the UNIT-level pattern for a focused regression test in the happy-dom
17
+ suite; it stays valid, but it is no longer the only proof available.
18
+
19
+ The main test suite still runs on `happy-dom` (`vitest.config.js`), which
20
+ implements most of the APIs linkedom lacks — so a naive test using the default
21
+ environment will not reproduce an SSR-shaped bug. Two consequences:
8
22
 
9
23
  1. **You cannot trust "the tests pass" as proof an SSR fix works** unless the test
10
24
  itself removes the API under test. A guard around `attachInternals` that's never
@@ -118,12 +132,13 @@ actually present. Both gh#285 and gh#286's fixes were verified this second way b
118
132
  shipping (a real `ElementInternals` instance still used and functional; a real
119
133
  `ResizeObserver` tick still corrects state) — do both, never one instead of the other.
120
134
 
121
- ## If a deeper simulation becomes worth the cost
135
+ ## The deeper simulation DONE 2026-08-17
122
136
 
123
- A real `linkedom` install (as a devDependency, used only in a dedicated SSR-simulation
124
- test file, never in the main suite) would let a test actually import `linkedom`,
125
- create a document with it, and run REAL component code against REAL absent APIs
126
- end-to-end closer to the consumer's actual failure mode than deletion, and the only
127
- way to test shape 2/4 meaningfully. This has not been done as of 2026-07-17 — it's a
128
- tooling investment, not a quick addition; raise it as its own decision if shape 2's
129
- investigation (gh#284) needs it.
137
+ The real-`linkedom` simulation this section used to propose is now the shipped
138
+ gate described at the top of this file (`scripts/dev/ssr-linkedom-smoke.mjs`,
139
+ `linkedom` pinned as a root devDependency, used only there the main suite stays
140
+ on happy-dom). It sweeps import + connect for the whole catalog; a shape-2/4
141
+ question (content preserved across a late upgrade, declarative data seeding) is
142
+ still answered by a targeted test extend the script's `FIXTURES` table with the
143
+ attrs/children in question, or write the assertion in
144
+ `test/ssr-upgrade-adoption-contract.test.js`'s late-upgrade shape.