@adia-ai/adia-ui-forge 0.8.62 → 0.8.64

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 (143) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/CHANGELOG.md +317 -275
  4. package/README.md +54 -18
  5. package/__init__.py +8 -8
  6. package/agents/a2ui-maintenance-agent.corpus.json +7 -7
  7. package/agents/a2ui-maintenance-agent.md +6 -6
  8. package/agents/a2ui-planner-agent.md +11 -11
  9. package/agents/demo-audit-agent.corpus.json +4 -4
  10. package/agents/demo-audit-agent.md +8 -9
  11. package/agents/framework-checker.md +10 -11
  12. package/agents/framework-planner-agent.md +9 -10
  13. package/agents/gen-ui-review-agent.corpus.json +4 -4
  14. package/agents/gen-ui-review-agent.md +8 -8
  15. package/agents/package-release-agent.corpus.json +4 -4
  16. package/agents/package-release-agent.md +9 -10
  17. package/agents/primitive-authoring-agent.corpus.json +1 -1
  18. package/agents/primitive-authoring-agent.md +5 -5
  19. package/agents/routing-corpus.json +46 -46
  20. package/codex.interface.json +1 -1
  21. package/commands/gen-ui-review.md +1 -1
  22. package/commands/package-release.md +2 -2
  23. package/package.json +1 -1
  24. package/plugin.yaml +2 -2
  25. package/prompts/gen-ui-review.md +1 -1
  26. package/prompts/package-release.md +2 -2
  27. package/references/contracts/a2ui-mcp-surface.md +7 -7
  28. package/references/contracts/migration-guide-format.md +6 -7
  29. package/references/shared/content-trust.md +3 -5
  30. package/skills/a2ui-maintenance/SKILL.md +13 -16
  31. package/skills/a2ui-maintenance/evals/evals.json +2 -2
  32. package/skills/a2ui-maintenance/evals/routing-corpus.json +1 -1
  33. package/skills/a2ui-maintenance/references/anti-patterns.md +10 -10
  34. package/skills/a2ui-maintenance/references/chunk-authoring.md +11 -12
  35. package/skills/a2ui-maintenance/references/corpus-discipline.md +13 -14
  36. package/skills/a2ui-maintenance/references/data-model-reactivity.md +22 -25
  37. package/skills/a2ui-maintenance/references/eval-diagnostics.md +21 -21
  38. package/skills/a2ui-maintenance/references/format-extension-decisions.md +12 -13
  39. package/skills/a2ui-maintenance/references/leverage-rules.md +6 -6
  40. package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +9 -10
  41. package/skills/a2ui-maintenance/references/mcp-tool-reference.md +11 -12
  42. package/skills/a2ui-maintenance/references/pipeline-overview.md +33 -38
  43. package/skills/a2ui-maintenance/references/semantic-fail-lifting.md +13 -14
  44. package/skills/a2ui-maintenance/references/strategy-engines.md +14 -16
  45. package/skills/a2ui-maintenance/references/surface-lifecycle.md +10 -11
  46. package/skills/a2ui-maintenance/references/zettel-calibration.md +14 -16
  47. package/skills/component-md-authoring/SKILL.md +22 -22
  48. package/skills/component-md-authoring/agents/openai.yaml +1 -1
  49. package/skills/cross-harness-authoring-standards/SKILL.md +19 -21
  50. package/skills/cross-harness-authoring-standards/agents/openai.yaml +1 -1
  51. package/skills/cross-harness-compatibility-standards/SKILL.md +35 -34
  52. package/skills/cross-harness-compatibility-standards/agents/openai.yaml +1 -1
  53. package/skills/demo-audit/SKILL.md +38 -38
  54. package/skills/demo-audit/evals/routing-corpus.json +1 -1
  55. package/skills/demo-audit/references/admin-shell-anatomy.md +21 -25
  56. package/skills/demo-audit/references/app-shell-pitfalls.md +11 -13
  57. package/skills/demo-audit/references/auto-fix-allowlist.md +3 -3
  58. package/skills/demo-audit/references/card-anatomy-sweep.md +10 -11
  59. package/skills/demo-audit/references/chat-shell-anatomy.md +25 -26
  60. package/skills/demo-audit/references/editor-shell-anatomy.md +25 -28
  61. package/skills/demo-audit/references/html-attr-sweep.md +12 -13
  62. package/skills/demo-audit/references/mode7-status-battery.md +4 -4
  63. package/skills/demo-audit/references/native-leak-annotations.md +8 -8
  64. package/skills/demo-audit/references/output-contract-worked-example.md +8 -8
  65. package/skills/demo-audit/references/probe-discipline-and-escalation.md +2 -2
  66. package/skills/demo-audit/references/visual-probe-triage.md +21 -23
  67. package/skills/gen-ui-review/SKILL.md +28 -28
  68. package/skills/gen-ui-review/agents/openai.yaml +1 -1
  69. package/skills/gen-ui-review/evals/evals.json +1 -1
  70. package/skills/gen-ui-review/evals/routing-corpus.json +1 -1
  71. package/skills/gen-ui-review/references/corpus-html-patterns.md +28 -29
  72. package/skills/gen-ui-review/references/exit-gate-mechanics.md +5 -5
  73. package/skills/gen-ui-review/references/lookup-maintenance.md +2 -2
  74. package/skills/gen-ui-review/references/loop-protocol.md +25 -25
  75. package/skills/gen-ui-review/references/rubric-cosmetic.md +10 -10
  76. package/skills/gen-ui-review/references/rubric-decompose.md +15 -16
  77. package/skills/gen-ui-review/references/rubric-score.md +23 -24
  78. package/skills/gen-ui-review/references/scorecard-worked-examples.md +12 -12
  79. package/skills/gen-ui-review/references/scores.schema.json +2 -2
  80. package/skills/llm-client-maintenance/SKILL.md +12 -12
  81. package/skills/llm-client-maintenance/evals/routing-corpus.json +1 -1
  82. package/skills/llm-client-maintenance/references/adapter-contract.md +27 -27
  83. package/skills/llm-client-maintenance/references/add-a-provider.md +25 -25
  84. package/skills/llm-client-maintenance/references/bridge-facade.md +31 -31
  85. package/skills/llm-client-maintenance/references/browser-proxy-boundary.md +17 -17
  86. package/skills/llm-client-maintenance/references/model-registry.md +14 -14
  87. package/skills/llm-client-maintenance/references/streaming-sse.md +21 -21
  88. package/skills/package-release/SKILL.md +21 -23
  89. package/skills/package-release/evals/evals.json +2 -2
  90. package/skills/package-release/evals/routing-corpus.json +2 -2
  91. package/skills/package-release/references/authorization-model.md +6 -8
  92. package/skills/package-release/references/changelog-discipline.md +25 -27
  93. package/skills/package-release/references/cut-procedure.md +129 -136
  94. package/skills/package-release/references/gates-catalog.md +42 -43
  95. package/skills/package-release/references/independent-package-release.md +14 -15
  96. package/skills/package-release/references/invariants-detail.md +9 -9
  97. package/skills/package-release/references/mechanization.md +11 -12
  98. package/skills/package-release/references/migration-guide-authoring.md +20 -20
  99. package/skills/package-release/references/notes-authoring.md +20 -20
  100. package/skills/package-release/references/recovery-paths.md +28 -28
  101. package/skills/primitive-authoring/SKILL.md +19 -19
  102. package/skills/primitive-authoring/agents/openai.yaml +1 -1
  103. package/skills/primitive-authoring/evals/evals.json +2 -2
  104. package/skills/primitive-authoring/evals/routing-corpus.json +2 -2
  105. package/skills/primitive-authoring/references/INDEX.md +22 -22
  106. package/skills/primitive-authoring/references/anti-patterns.md +76 -77
  107. package/skills/primitive-authoring/references/api-contract.md +63 -65
  108. package/skills/primitive-authoring/references/authoring-cycle.md +47 -50
  109. package/skills/primitive-authoring/references/canonical-pattern-index.md +118 -118
  110. package/skills/primitive-authoring/references/code-style.md +50 -52
  111. package/skills/primitive-authoring/references/common-gotchas.md +23 -23
  112. package/skills/primitive-authoring/references/composite-demo-protocol.md +42 -45
  113. package/skills/primitive-authoring/references/css-patterns.md +97 -97
  114. package/skills/primitive-authoring/references/for-attribute-event-contract.md +12 -12
  115. package/skills/primitive-authoring/references/form-control-sizing.md +16 -18
  116. package/skills/primitive-authoring/references/lifecycle-patterns.md +42 -42
  117. package/skills/primitive-authoring/references/llm-bridge.md +21 -21
  118. package/skills/primitive-authoring/references/module-promotion.md +51 -51
  119. package/skills/primitive-authoring/references/primitive-audit.md +16 -16
  120. package/skills/primitive-authoring/references/shell-patterns.md +81 -81
  121. package/skills/primitive-authoring/references/svg-authoring.md +62 -66
  122. package/skills/primitive-authoring/references/token-contract.md +45 -49
  123. package/skills/primitive-authoring/references/trait-pages.md +8 -8
  124. package/skills/primitive-authoring/references/worked-example.md +39 -39
  125. package/skills/primitive-authoring/references/yaml-contract.md +139 -143
  126. package/skills/site-deployment/SKILL.md +25 -26
  127. package/skills/site-deployment/agents/openai.yaml +1 -1
  128. package/skills/site-deployment/evals/routing-corpus.json +1 -1
  129. package/skills/site-deployment/references/deploy-playbooks.md +51 -53
  130. package/skills/site-docs-authoring/SKILL.md +14 -14
  131. package/skills/site-docs-authoring/agents/openai.yaml +1 -1
  132. package/skills/site-docs-authoring/evals/evals.json +1 -1
  133. package/skills/site-docs-authoring/evals/routing-corpus.json +1 -1
  134. package/skills/site-docs-authoring/intent.md +18 -18
  135. package/skills/ssr-compatibility/SKILL.md +24 -25
  136. package/skills/ssr-compatibility/agents/openai.yaml +1 -1
  137. package/skills/ssr-compatibility/evals/evals.json +1 -1
  138. package/skills/ssr-compatibility/evals/routing-corpus.json +1 -1
  139. package/skills/ssr-compatibility/references/consumer-workarounds.md +22 -23
  140. package/skills/ssr-compatibility/references/failure-shapes.md +84 -91
  141. package/skills/ssr-compatibility/references/guard-patterns.md +54 -58
  142. package/skills/ssr-compatibility/references/status-ledger.md +17 -17
  143. package/skills/ssr-compatibility/references/test-without-linkedom.md +26 -27
@@ -1,7 +1,7 @@
1
- # Guard patterns the established fix shape per failure shape
1
+ # Guard patterns, the established fix shape per failure shape
2
2
 
3
3
  Cites the actual shipped code (gh#285 PR #292, gh#286 PR #290) as the pattern to copy
4
- for a NEW instance of the same failure class don't re-derive the shape, match it.
4
+ for a NEW instance of the same failure class, don't re-derive the shape, match it.
5
5
 
6
6
  ## §1 · A browser-only API constructor/method call
7
7
 
@@ -9,16 +9,16 @@ for a NEW instance of the same failure class — don't re-derive the shape, matc
9
9
  `IntersectionObserver`, `MutationObserver`, `PerformanceObserver`) or `typeof
10
10
  this.<method> === 'function'` (for instance methods: `attachInternals`) or `'<prop>'
11
11
  in <object>` (for object properties: `document.adoptedStyleSheets`) guards the call;
12
- the ELSE branch is picked by what downstream code does with the reference there is
12
+ the ELSE branch is picked by what downstream code does with the reference, there is
13
13
  no one boilerplate to paste everywhere.
14
14
 
15
15
  **Three concrete shapes, all live in the repo:**
16
16
 
17
- **(a) Field assigned, used later with `?.` on teardown** the majority case. Guard the
17
+ **(a) Field assigned, used later with `?.` on teardown**, the majority case. Guard the
18
18
  construction; every later reference to the field must already (or now needs to) use
19
19
  optional chaining, since the field can legitimately stay unset:
20
20
  ```js
21
- // packages/web-modules/shell/admin-sidebar/admin-sidebar.js #setupChildResizeObserver
21
+ // packages/web-modules/shell/admin-sidebar/admin-sidebar.js, #setupChildResizeObserver
22
22
  #setupChildResizeObserver() {
23
23
  this.#childRO = new ResizeObserver((entries) => { ... });
24
24
  this.#childRO.observe(this);
@@ -31,7 +31,7 @@ if (typeof ResizeObserver !== 'undefined') {
31
31
  // disconnectedCallback: this.#childRO?.disconnect(); ← already needs the `?.`
32
32
  ```
33
33
 
34
- **(b) Early-return no-op** when the entire trait/method's purpose IS the observer;
34
+ **(b) Early-return no-op**, when the entire trait/method's purpose IS the observer;
35
35
  nothing else in the function is worth running without it:
36
36
  ```js
37
37
  // packages/web-components/traits/resize-observer/resize-observer.js
@@ -41,7 +41,7 @@ setup({ host }) {
41
41
  }
42
42
  ```
43
43
 
44
- **(c) Module-level `const`, ternary at declaration** when the observer is a
44
+ **(c) Module-level `const`, ternary at declaration**, when the observer is a
45
45
  singleton constructed at import time, not per-instance:
46
46
  ```js
47
47
  // packages/web-components/core/data-stream.js
@@ -51,10 +51,9 @@ 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).**
54
+ **(d) Feature, not environment, `typeof window` is NOT a guard (gh#1430).**
55
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:
56
+ `innerWidth`, so `typeof window !== 'undefined'` passes and the call still throws, at MODULE scope that takes every importer off the server. Test the member:
58
57
  ```js
59
58
  // packages/web-components/core/responsive.js
60
59
  const w = typeof window !== 'undefined' ? window.innerWidth : undefined;
@@ -62,7 +61,7 @@ if (typeof w !== 'number') return 'lg'; // fal
62
61
  if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { … }
63
62
  ```
64
63
 
65
- **(e) `instanceof <DOM constructor>` use `core/dom.js` (gh#1436).** Only
64
+ **(e) `instanceof <DOM constructor>`, use `core/dom.js` (gh#1436).** Only
66
65
  `HTMLElement` is a global under custom-elements-ssr; `v instanceof Node` /
67
66
  `Element` / `Text` / `DocumentFragment` is a `ReferenceError`, not `false`.
68
67
  `isNode(v)` / `isElement(v)` test `nodeType` structurally (and also accept nodes
@@ -73,13 +72,13 @@ if (isNode(result)) cell.replaceChildren(result); // renderer output
73
72
  const row = isElement(e.target) ? e.target.closest('…') : null; // event target
74
73
  ```
75
74
  `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
75
+ admits SVG elements too), every swept site only needed "an element", none needed
77
76
  "an HTML element specifically". Absent-method form of the same idea:
78
- `typeof this.getBoundingClientRect === 'function' && …` (nav-ui) linkedom has no
77
+ `typeof this.getBoundingClientRect === 'function' && …` (nav-ui), linkedom has no
79
78
  rect API at all, so an unguarded call is a TypeError, not a zero rect (that zero-rect
80
79
  case is §3).
81
80
 
82
- **The `this.internals` special case a shim, not `undefined`.** `UIElement`'s
81
+ **The `this.internals` special case, a shim, not `undefined`.** `UIElement`'s
83
82
  constructor (`packages/web-components/core/element.js`) needed a DIFFERENT answer
84
83
  than "guard and leave unset," because ~12 files across the framework call
85
84
  `this.internals.setValidity(...)` / `.setFormValue(...)` unconditionally (form
@@ -108,33 +107,33 @@ returned object/value is always truthy and call methods on it unconditionally?"
108
107
  yes, a no-op shim matching the ACTUAL consumed surface (grep for it, don't guess) is
109
108
  the right answer, not leaving the field `undefined`. If the API is purely
110
109
  fire-and-forget infrastructure with no return value anyone depends on (most
111
- Observers), a plain construction guard (shapes a/b/c above) is sufficient no shim
110
+ Observers), a plain construction guard (shapes a/b/c above) is sufficient, no shim
112
111
  needed.
113
112
 
114
- ## §2 · Destructive `stamp()` on connect (gh#284 narrowed to a static audit, not a stamp() patch)
113
+ ## §2 · Destructive `stamp()` on connect (gh#284, narrowed to a static audit, not a stamp() patch)
115
114
 
116
115
  There is no `stamp()`/`connectedCallback` fix, and per the 2026-07-17 narrowing
117
- there isn't meant to be one the mechanism is real but zero shipped components
116
+ there isn't meant to be one: the mechanism is real but zero shipped components
118
117
  are exposed (see [`failure-shapes.md`](failure-shapes.md) §2 for the full survey).
119
118
  The shipped guard is `scripts/dev/audit-template-child-conflict.mjs`: it flags a
120
119
  NEW component that pairs a non-null `static template` with a yaml `slots.default`
121
- entry (critical) or a body-text usage example (advisory) i.e. it prevents the
120
+ entry (critical) or a body-text usage example (advisory), i.e. it prevents the
122
121
  conflict shape from being reintroduced, rather than patching the render lifecycle
123
122
  every component goes through (150 at the 2026-07 survey; the census grows). **A component author who hits this
124
123
  audit's finding fixes it by making the template `() => null`** (the pattern every
125
- current children-accepting component already uses compose via CSS + `render()`'s
124
+ current children-accepting component already uses, compose via CSS + `render()`'s
126
125
  own surgical DOM manipulation, matching `avatar-ui`'s `#imgEl`/`#initialsEl`
127
- pattern) NOT by inventing a component-local stamp-skip flag or a marker
126
+ pattern), NOT by inventing a component-local stamp-skip flag or a marker
128
127
  attribute; either of those drifts from the established convention and isn't
129
128
  covered by this audit's "safe" classification. If a genuinely new component NEEDS
130
129
  both a non-null template AND real consumer children (unlike anything shipped
131
- today), route through `primitive-authoring` before authoring it that's a real design
130
+ today), route through `primitive-authoring` before authoring it, that's a real design
132
131
  question, not a mechanical fix.
133
132
 
134
133
  ## §2b · Reflected properties not initialized from pre-existing attributes on upgrade (gh#284's REAL fix, PR #309)
135
134
 
136
135
  **Shape:** a `reflect: true` property stays at its class default after a LATE
137
- custom-element upgrade the element was parsed/appended (or server-rendered)
136
+ custom-element upgrade: the element was parsed/appended (or server-rendered)
138
137
  with its attribute already present, but the tag wasn't `customElements.define`d
139
138
  until afterward. `<nav-item-ui text="Profile">` renders an empty label; any
140
139
  other reflected prop on any component is at equal risk under the same
@@ -143,10 +142,10 @@ sequence.
143
142
  **Root cause:** the custom-elements spec's "upgrade an element" algorithm
144
143
  (§4.13.5 step 6) requires replaying `attributeChangedCallback` for every
145
144
  attribute already on the element BEFORE `connectedCallback` fires on upgrade.
146
- happy-dom (this repo's test DOM) does not do this confirmed directly with a
145
+ happy-dom (this repo's test DOM) does not do this, confirmed directly with a
147
146
  bare `HTMLElement` subclass, no AdiaUI code involved
148
147
  (`packages/web-components/core/element.test.js`,
149
- `describe('UIElement SSR attribute-upgrade replay (gh#284)')`, first test).
148
+ `describe('UIElement: SSR attribute-upgrade replay (gh#284)')`, first test).
150
149
  linkedom is a similarly from-scratch custom-elements registry, so the same gap
151
150
  is the working hypothesis there too pending direct confirmation.
152
151
 
@@ -165,29 +164,28 @@ for (const [key, cfg] of Object.entries(ctor.properties)) {
165
164
 
166
165
  Safe on every environment: `connectedCallback` fires reliably everywhere
167
166
  (unlike the shimmed `attributeChangedCallback` replay), and the resync is a
168
- no-op where the replay already happened correctly the property setter's
167
+ no-op where the replay already happened correctly: the property setter's
169
168
  `Object.is` check short-circuits when the value already matches, so no extra
170
- render pass. This is a FRAMEWORK-LEVEL fix, already shipped a component
169
+ render pass. This is a FRAMEWORK-LEVEL fix, already shipped, a component
171
170
  author hitting this shape doesn't need to do anything component-local; if a
172
171
  reflected prop still looks wrong after upgrade on a version carrying this fix,
173
- that's a new, different bug don't assume it's this one recurring.
172
+ that's a new, different bug, don't assume it's this one recurring.
174
173
 
175
174
  ## §3 · A connect-time measurement is "unknown," not "confirmed"
176
175
 
177
176
  **Shape:** treat a zero (or otherwise clearly-bogus) synchronous read as "no signal
178
177
  yet" and DEFER the decision to whatever async correction mechanism the component
179
- already has don't add a new one if an existing `ResizeObserver`/similar already
178
+ already has, don't add a new one if an existing `ResizeObserver`/similar already
180
179
  watches the same dimension.
181
180
  ```js
182
- // packages/web-modules/shell/admin-sidebar/admin-sidebar.js #syncCollapsedFromWidth
181
+ // packages/web-modules/shell/admin-sidebar/admin-sidebar.js, #syncCollapsedFromWidth
183
182
  #syncCollapsedFromWidth() {
184
183
  const w = this.getBoundingClientRect().width;
185
- if (w === 0) return; // ← unknown, not "confirmed collapsed"
186
- this.collapsed = w <= SNAP_THRESHOLD; // leaves whatever connected() already
184
+ if (w === 0) return; // ← unknown, not "confirmed collapsed", this.collapsed = w <= SNAP_THRESHOLD; // leaves whatever connected() already
187
185
  } // restored (persisted state, or default)
188
186
 
189
187
  // the component's EXISTING ResizeObserver callback (already watching this.host for
190
- // an unrelated reason flipping select-ui placement) gains ONE more line, so its
188
+ // an unrelated reason, flipping select-ui placement) gains ONE more line, so its
191
189
  // first REAL tick (post-hydration, or after the display:none ancestor is revealed)
192
190
  // resolves what connect-time couldn't:
193
191
  #setupChildResizeObserver() {
@@ -201,25 +199,24 @@ watches the same dimension.
201
199
  }
202
200
  ```
203
201
  The key move: reuse the observer that's ALREADY there rather than adding a
204
- purpose-built one a second observer watching the same element for two unrelated
202
+ purpose-built one, a second observer watching the same element for two unrelated
205
203
  reasons is a maintenance smell, and per §1 above, a NEW observer needs its own
206
204
  construction guard anyway.
207
205
 
208
- ## §4 · Adopt-or-diff value-diff before mutating, never guess or half-adopt (gh#1678, gh#1687)
206
+ ## §4 · Adopt-or-diff, value-diff before mutating, never guess or half-adopt (gh#1678, gh#1687)
209
207
 
210
208
  **Shape:** don't mutate a DOM position until a comparison proves it actually
211
209
  needs to change. `setAttribute()` queues a mutation record even when the new
212
210
  value is byte-identical to the old one (confirmed directly against both
213
211
  happy-dom and linkedom); `removeAttribute()` on an already-absent attribute
214
- does NOT (the spec's own asymmetry no compare-first guard needed there).
212
+ does NOT (the spec's own asymmetry, no compare-first guard needed there).
215
213
  Two concrete shapes, both shipped, picked by whether the reconciled unit
216
214
  carries a stable identity key:
217
215
 
218
- **(a) A flat, keyed list seed the keyed-reconcile map from the adopted
216
+ **(a) A flat, keyed list, seed the keyed-reconcile map from the adopted
219
217
  DOM (`pagination-ui`, gh#1687).** `connected()` adopts a pre-existing
220
218
  server-rendered structural child (`this.#nav = this.querySelector(':scope
221
- > nav[slot="nav"]')`) instead of unconditionally creating a fresh one
222
- but adopting the CONTAINER alone isn't sufficient: `reconcile()`'s own
219
+ > nav[slot="nav"]')`) instead of unconditionally creating a fresh one, but adopting the CONTAINER alone isn't sufficient: `reconcile()`'s own
223
220
  keyed diff (`core/element.js`) keys off a `parent[KEY_MAP]` populated by
224
221
  this element's OWN prior render calls, which a freshly-parsed SSR fragment
225
222
  never has. Without seeding it, the very first render treats every adopted
@@ -230,13 +227,13 @@ mismatch (`#childMatchesItem()` checking tag + marker, not just count) is
230
227
  left unseeded so reconcile falls back to a genuine clean rebuild instead of
231
228
  miskeying a wrong-tagged survivor in place.
232
229
 
233
- **(b) An arbitrary positional child with no stable key compare via
230
+ **(b) An arbitrary positional child with no stable key, compare via
234
231
  `Node.isEqualNode()` (`table-ui`, gh#1678).** Table cells have no identity
235
232
  key the way a pagination item does (no natural "this is always the id
236
233
  column" marker independent of position), so `adoptOrDiffChildren()`
237
234
  (`table.class.js`) builds each fresh candidate exactly as before, then
238
235
  compares it against the existing child at that position with
239
- `existing.isEqualNode(fresh)` standard DOM, present under every
236
+ `existing.isEqualNode(fresh)`, standard DOM, present under every
240
237
  environment this framework runs in (browsers, happy-dom, linkedom), never
241
238
  one of §1's browser-only APIs:
242
239
  ```js
@@ -248,12 +245,12 @@ function adoptOrDiffChildren(container, freshChildren) {
248
245
  if (!existing) container.appendChild(fresh);
249
246
  else if (RENDERER_OWNED.has(fresh) || !existing.isEqualNode(fresh)) container.replaceChild(fresh, existing);
250
247
  // else: matches byte-for-byte AND carries no renderer-owned runtime
251
- // state (§4.1 below) adopt in place, touch nothing.
248
+ // state (§4.1 below), adopt in place, touch nothing.
252
249
  }
253
250
  }
254
251
  ```
255
252
  A match adopts in place (zero mutation); a mismatch replaces the position
256
- wholesale exactly as the pre-fix code always did never a partial patch of
253
+ wholesale exactly as the pre-fix code always did, never a partial patch of
257
254
  a mismatched node's individual attributes, which would risk leaving a
258
255
  wrong-tagged or wrong-shaped survivor "fixed" in place instead of really
259
256
  rebuilt (the same failure mode (a) above guards against via
@@ -261,7 +258,7 @@ rebuilt (the same failure mode (a) above guards against via
261
258
 
262
259
  ### §4.1 · `isEqualNode()` proves structural safety, never runtime-state safety (gh#1678 CodeRabbit follow-up, closed on PR #1756 before merge)
263
260
 
264
- **The gap:** (b) above is a *structural* diff tag, attributes, text,
261
+ **The gap:** (b) above is a *structural* diff, tag, attributes, text,
265
262
  descendants. It's a necessary adoption test but not a sufficient one: it
266
263
  cannot see an event listener (or any other runtime/imperative state) that a
267
264
  renderer attached to the candidate node it returned. `table.class.js`'s
@@ -269,16 +266,16 @@ renderer attached to the candidate node it returned. `table.class.js`'s
269
266
  consumer function; or a built-in cell-type renderer, `typeDef.render`)
270
267
  BEFORE the candidate ever reaches `adoptOrDiffChildren()`. If that renderer
271
268
  attached a listener, the candidate can still be `isEqualNode()`-identical to
272
- the existing (e.g. listener-less, SSR-parsed) DOM at that position the
269
+ the existing (e.g. listener-less, SSR-parsed) DOM at that position, the
273
270
  guard reports a match, adopts the OLD node, discards the fresh one, and the
274
271
  listener silently never lands. The pre-adopt-or-diff, unconditional-
275
272
  `replaceChild()` code never had this bug, because it always installed
276
273
  whatever the renderer had just built, every time.
277
274
 
278
- **The fix an explicit renderer-owned marker, not a deeper structural
275
+ **The fix, an explicit renderer-owned marker, not a deeper structural
279
276
  check.** A `Node.isEqualNode()`-shaped fix can only ever prove declarative
280
277
  shape; it structurally cannot see a listener, so the fix isn't "compare
281
- harder" it's "know which candidates a comparison can't clear in the first
278
+ harder", it's "know which candidates a comparison can't clear in the first
282
279
  place, and never let structural equality alone adopt one of those." A
283
280
  module-level `RENDERER_OWNED` `WeakSet` tags exactly the cells built by
284
281
  something free to attach listeners/runtime state:
@@ -288,7 +285,7 @@ const RENDERER_OWNED = new WeakSet();
288
285
  if (typeof col.render === 'function') {
289
286
  const result = col.render(value, data, cell, dataIndex);
290
287
  // ... apply result to cell ...
291
- RENDERER_OWNED.add(cell); // arbitrary code always tag
288
+ RENDERER_OWNED.add(cell); // arbitrary code, always tag
292
289
  } else if (typeof col.format !== 'function') {
293
290
  const typeDef = cellTypes[col.type || 'text'];
294
291
  if (typeDef?.render) {
@@ -297,19 +294,19 @@ if (typeof col.render === 'function') {
297
294
  }
298
295
  }
299
296
  ```
300
- `col.render()` is always tagged it's opaque consumer code, impossible to
297
+ `col.render()` is always tagged, it's opaque consumer code, impossible to
301
298
  introspect for safety. A built-in cell-type renderer is tagged only when
302
- its own registration declares `attachesListeners: true` in this
299
+ its own registration declares `attachesListeners: true`, in this
303
300
  framework, currently just `cellTypes.actions` (`cell-types.js`), the one
304
301
  built-in type that calls `addEventListener()` directly on a node it builds.
305
302
  Every OTHER built-in cell type (text/number/currency/percent/date/datetime/
306
303
  boolean/badge/avatar/link/markdown/progress) only sets attributes on
307
- already-declarative custom elements or plain nodes no listeners, so
304
+ already-declarative custom elements or plain nodes, no listeners, so
308
305
  tagging them buys nothing and costs the zero-mutation benefit for the
309
306
  overwhelmingly common case. **This was measured, not assumed**: an earlier
310
307
  draft of this fix tagged every `typeDef.render` cell unconditionally and
311
308
  broke the AC-004a zero-mutation test (§ above) for plain text/number/date
312
- cells narrowed to the declared-flag form before merge.
309
+ cells, narrowed to the declared-flag form before merge.
313
310
 
314
311
  **Applying this pattern to a NEW adopt-or-diff instance (elsewhere in this
315
312
  framework, or #1755/#1754 if either goes this direction):** before trusting
@@ -317,17 +314,17 @@ framework, or #1755/#1754 if either goes this direction):** before trusting
317
314
  invokes to build a candidate node is free to attach a listener or stash
318
315
  other runtime state on it. If yes, that candidate needs its own
319
316
  `RENDERER_OWNED`-shaped tag (or equivalent) and must always be replaced,
320
- never adopted on structural equality alone an activation step that tries
317
+ never adopted on structural equality alone, an activation step that tries
321
318
  to re-attach the listener onto the ADOPTED node instead is a fragile
322
319
  protocol this fix deliberately did not attempt (unclear how to discover
323
320
  "what would the renderer have attached" without re-running the renderer,
324
321
  at which point you already have the fresh node to just use).
325
322
 
326
- **(c) Idempotent attribute writes the write itself must be guarded, not
323
+ **(c) Idempotent attribute writes, the write itself must be guarded, not
327
324
  just the value.** Any attribute set that runs on EVERY render/connect
328
325
  regardless of whether the value changed (a host's `role`/`tabindex`, an
329
326
  inline computed style, a row's `data-index`/`aria-selected`) needs a
330
- compare-before-write wrapper `setAttrIfChanged(el, name, value)` because
327
+ compare-before-write wrapper, `setAttrIfChanged(el, name, value)`, because
331
328
  a bare `setAttribute(el, name, sameValue)` still mutates. `removeAttribute`
332
329
  needs no equivalent guard (already a no-op on an absent attribute per
333
330
  spec, confirmed directly).
@@ -338,9 +335,8 @@ of cells/fields with no such per-position identity → (b). Both fall back to
338
335
  the SAME principle on a mismatch: rebuild for real, never guess and never
339
336
  half-adopt (leave a wrong node "patched" in place instead of replaced).
340
337
 
341
- **The conditional-inject class (querySelector-guard-before-innerHTML
342
- `search-ui`, `pagination-ui`) is this same principle at container
343
- granularity** — `if (!this.querySelector('input-ui')) this.innerHTML = …`
338
+ **The conditional-inject class (querySelector-guard-before-innerHTML, `search-ui`, `pagination-ui`) is this same principle at container
339
+ granularity**, `if (!this.querySelector('input-ui')) this.innerHTML = …`
344
340
  IS an adopt-or-diff check, just a boolean presence check instead of a value
345
341
  comparison. It's SSR-safe exactly when the querySelector target is
346
342
  STRUCTURALLY SPECIFIC (an exact tag/slot, never "has any children at all").
@@ -351,4 +347,4 @@ idempotent-write guard, not yet applied to `search-ui`/`pagination-ui`).
351
347
  ## Verify targets for a new guard
352
348
 
353
349
  See [`test-without-linkedom.md`](test-without-linkedom.md) for how to prove a new
354
- guard actually works deleting the real API in a test, not mocking it.
350
+ guard actually works, deleting the real API in a test, not mocking it.
@@ -1,41 +1,41 @@
1
- # SSR support status ledger
1
+ # SSR support, status ledger
2
2
 
3
3
  **Filing a new downstream SSR-seam report?** Use the `ssr` label plus
4
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
5
+ (gh#1384), it captures the resolved `@adia-ai/web-components` version, the
6
6
  downstream kit version, the component + fixture, an SSR-vs-CSR diff or
7
7
  minimal repro, and a cosmetic/behavioral/blocked-upstream classification, so
8
8
  a report lands triageable instead of free-form (the archaeology the #284/#288
9
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
10
+ downstream reports but does not exist in this repo, it lives in adiav2's own
11
11
  repo (`@adiav2/ssr-kit`'s spec); do not look for it here.
12
12
 
13
- **This file drifts fast re-verify against the live issue tracker
13
+ **This file drifts fast, re-verify against the live issue tracker
14
14
  (`gh issue view <n>`) before trusting it for anything beyond a quick orientation.**
15
15
  Every row was true as of 2026-07-17; a closed issue below may have re-opened, a PR
16
16
  number may have been superseded by a follow-up fix, etc. This ledger is a map, not
17
- the territory the territory is `gh issue list` / `gh issue view`.
17
+ the territory: the territory is `gh issue list` / `gh issue view`.
18
18
 
19
19
  | Issue | Failure shape | Status | Shipped in | Notes |
20
20
  |---|---|---|---|---|
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. |
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 |
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. |
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. |
28
- | [#287](https://github.com/adiahealth/gen-ui-kit/issues/287) | Unrelated to the three SSR shapes above a Phosphor-icon `import.meta.glob` path failure under workspace hoisting, plus the dep-optimizer silently emptying the icon registry | **CLOSED** | PR #294 (merged 2026-07-17) | Root cause confirmed empirically (a from-scratch pnpm workspace repro): the glob's leading `/` joined to Vite's configured root, which only reaches a transitive dependency's assets when hoisted there. Fixed by switching to an entry-file-relative glob (`../node_modules/...`). A second, independent cause (Vite's dev-server `optimizeDeps` pre-bundling) has no package-side fix documented as a consumer `optimizeDeps.exclude` workaround. A bundler/workspace-resolution bug, not an SSR-lifecycle bug still doesn't route through this pack's failure taxonomy. |
29
- | [#1678](https://github.com/adiahealth/gen-ui-kit/issues/1678) | §5 (new) a custom `render()`/`connected()` unconditionally rebuilds a subtree that already matches; §5.1 (new) `isEqualNode()` proves structural safety only, never runtime-state safety (a renderer-owned listener can be silently dropped on adoption CodeRabbit finding, closed in the same PR); §6 (new) the conditional-inject class, decision recorded | **PR #1756 opened 2026-08-20 (branch `1678-table-ssr-adopt-or-diff`), NOT yet merged human review required before landing (core table-ui render path)** | `table.class.js` `adoptOrDiffChildren()` + `RENDERER_OWNED` + `setAttrIfChanged()`/`removeAttrIfPresent()`; `cell-types.js`'s `attachesListeners: true` on `actions` | §5/§5.1 implemented for table-ui only, pending merge no framework-wide static audit exists for this shape yet. §6's decision: querySelector-guard-before-innerHTML (`search-ui`, `pagination-ui`) is SSR-safe when the guard target is structurally specific; [#1755](https://github.com/adiahealth/gen-ui-kit/issues/1755) (filed alongside this PR, already merged) extends the idempotent-write guard to those two components' own post-adopt attribute writes, scoped OUT of this PR. |
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. |
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 |
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. |
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. |
28
+ | [#287](https://github.com/adiahealth/gen-ui-kit/issues/287) | Unrelated to the three SSR shapes above, a Phosphor-icon `import.meta.glob` path failure under workspace hoisting, plus the dep-optimizer silently emptying the icon registry | **CLOSED** | PR #294 (merged 2026-07-17) | Root cause confirmed empirically (a from-scratch pnpm workspace repro): the glob's leading `/` joined to Vite's configured root, which only reaches a transitive dependency's assets when hoisted there. Fixed by switching to an entry-file-relative glob (`../node_modules/...`). A second, independent cause (Vite's dev-server `optimizeDeps` pre-bundling) has no package-side fix, documented as a consumer `optimizeDeps.exclude` workaround. A bundler/workspace-resolution bug, not an SSR-lifecycle bug, still doesn't route through this pack's failure taxonomy. |
29
+ | [#1678](https://github.com/adiahealth/gen-ui-kit/issues/1678) | §5 (new), a custom `render()`/`connected()` unconditionally rebuilds a subtree that already matches; §5.1 (new), `isEqualNode()` proves structural safety only, never runtime-state safety (a renderer-owned listener can be silently dropped on adoption, CodeRabbit finding, closed in the same PR); §6 (new), the conditional-inject class, decision recorded | **PR #1756 opened 2026-08-20 (branch `1678-table-ssr-adopt-or-diff`), NOT yet merged, human review required before landing (core table-ui render path)** | `table.class.js` `adoptOrDiffChildren()` + `RENDERER_OWNED` + `setAttrIfChanged()`/`removeAttrIfPresent()`; `cell-types.js`'s `attachesListeners: true` on `actions` | §5/§5.1 implemented for table-ui only, pending merge, no framework-wide static audit exists for this shape yet. §6's decision: querySelector-guard-before-innerHTML (`search-ui`, `pagination-ui`) is SSR-safe when the guard target is structurally specific; [#1755](https://github.com/adiahealth/gen-ui-kit/issues/1755) (filed alongside this PR, already merged) extends the idempotent-write guard to those two components' own post-adopt attribute writes, scoped OUT of this PR. |
30
30
 
31
31
  ## What "done" looks like for the open items
32
32
 
33
- - **#284's original scope is closed as a non-issue against 0.8.4** see the ledger
33
+ - **#284's original scope is closed as a non-issue against 0.8.4**, see the ledger
34
34
  row above. Re-open only if a FUTURE component's yaml/class.js trips
35
35
  `audit-template-child-conflict.mjs`, or if `adiav2`'s production workaround
36
36
  (`consumer-workarounds.md`) turns out to be catching something this repo's
37
37
  synthetic repro didn't (confirm via the issue thread before assuming so).
38
- - **#288 is done** see the ledger row above. `table-ui`'s `data="[…]"` attribute
38
+ - **#288 is done**, see the ledger row above. `table-ui`'s `data="[…]"` attribute
39
39
  is the shipped form; `select-ui` and `chart-ui` already had their own declarative
40
40
  forms before this issue was even filed.
41
41
 
@@ -43,5 +43,5 @@ the territory — the territory is `gh issue list` / `gh issue view`.
43
43
 
44
44
  When any row's status changes, update the table AND re-check whether
45
45
  [`consumer-workarounds.md`](consumer-workarounds.md)'s claims about what's "now
46
- deletable" are still accurate a workaround section that says "should no longer be
46
+ deletable" are still accurate, a workaround section that says "should no longer be
47
47
  necessary" for a shape that's since re-opened is a false manifest, not a stale detail.
@@ -1,51 +1,51 @@
1
- # Testing an SSR gap the linkedom gate first, then the deletion pattern
1
+ # Testing an SSR gap, the linkedom gate first, then the deletion pattern
2
2
 
3
3
  **UPDATE 2026-08-17 (gh#1430/#1436): this repo now HAS a `linkedom` root
4
4
  devDependency and a shipped shim gate.** `scripts/dev/ssr-linkedom-smoke.mjs`
5
5
  installs exactly the six globals `custom-elements-ssr/server-shim.js` installs
6
- (document, window, customElements, HTMLElement, Event, CustomEvent and nothing
6
+ (document, window, customElements, HTMLElement, Event, CustomEvent, and nothing
7
7
  else: no Node/Element/Text/DocumentFragment, no matchMedia, no
8
8
  requestAnimationFrame, no getBoundingClientRect), imports every
9
9
  `components/*/*.js` entry, and constructs + connects every registered tag the way
10
10
  `CustomElementRender` does; `packages/web-components/test/ssr-linkedom-smoke.test.js`
11
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
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
14
  approximation. Add a row to its `FIXTURES` table when a guard lands on a branch only
15
15
  some attribute value reaches (`swiper-ui[autoplay]` is the model). The rest of this
16
16
  file is the UNIT-level pattern for a focused regression test in the happy-dom
17
17
  suite; it stays valid, but it is no longer the only proof available.
18
18
 
19
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
20
+ implements most of the APIs linkedom lacks, so a naive test using the default
21
21
  environment will not reproduce an SSR-shaped bug. Two consequences:
22
22
 
23
23
  1. **You cannot trust "the tests pass" as proof an SSR fix works** unless the test
24
24
  itself removes the API under test. A guard around `attachInternals` that's never
25
25
  exercised without `attachInternals` proves nothing.
26
- 2. **The house pattern is deletion, not mocking** `delete
26
+ 2. **The house pattern is deletion, not mocking**, `delete
27
27
  HTMLElement.prototype.attachInternals` (or `delete
28
28
  Document.prototype.adoptedStyleSheets`, or `delete globalThis.ResizeObserver`)
29
29
  inside a `try { ... } finally { restore }` block, in the SAME test environment the
30
30
  suite already runs in. This is a closer approximation of "the API is genuinely
31
31
  absent" than a mock that might itself paper over the exact code path that would
32
- throw deleting the real thing exercises the REAL `typeof x === 'undefined'` /
32
+ throw, deleting the real thing exercises the REAL `typeof x === 'undefined'` /
33
33
  `typeof x === 'function'` check the guard actually uses.
34
34
 
35
- ## The pattern (descriptor-based restore copy this shape for a new test)
35
+ ## The pattern (descriptor-based restore, copy this shape for a new test)
36
36
 
37
37
  The shipped test (`packages/web-components/core/element.test.js`, describe block
38
- `'UIElement SSR browser-API absence (gh#285)'`) restores via a plain `=` assignment
38
+ `'UIElement, SSR browser-API absence (gh#285)'`) restores via a plain `=` assignment
39
39
  (`HTMLElement.prototype.attachInternals = original`). That works there ONLY because
40
40
  `original` is always a real function in this repo's test environment (happy-dom always
41
- provides `attachInternals`) it never actually exercises the absent-original case.
41
+ provides `attachInternals`), it never actually exercises the absent-original case.
42
42
  **Don't copy that shortcut.** If `original` is ever genuinely `undefined` (a NEW test
43
43
  targeting an API this environment doesn't have either), a plain assignment leaves the
44
- property present with value `undefined` instead of restoring true absence later
44
+ property present with value `undefined` instead of restoring true absence, later
45
45
  tests checking `'attachInternals' in HTMLElement.prototype` would wrongly see `true`,
46
46
  making pass/fail order-dependent (flagged by review on this pack's own PR #293).
47
- Use the descriptor-based restore the exact shape `adoptedStyleSheets` already uses
48
- below for every new instance of this pattern:
47
+ Use the descriptor-based restore, the exact shape `adoptedStyleSheets` already uses
48
+ below, for every new instance of this pattern:
49
49
 
50
50
  ```js
51
51
  it('constructs without throwing when attachInternals does not exist', () => {
@@ -61,8 +61,7 @@ it('constructs without throwing when attachInternals does not exist', () => {
61
61
  });
62
62
  ```
63
63
 
64
- For a global (not a prototype method), the same descriptor-based restore applies
65
- globals are own-properties of `globalThis`, so `Object.getOwnPropertyDescriptor`
64
+ For a global (not a prototype method), the same descriptor-based restore applies, globals are own-properties of `globalThis`, so `Object.getOwnPropertyDescriptor`
66
65
  works identically:
67
66
  ```js
68
67
  const desc = Object.getOwnPropertyDescriptor(globalThis, 'ResizeObserver');
@@ -91,31 +90,31 @@ try {
91
90
  Deletion proves "the guarded code path doesn't throw when the API is absent." It does
92
91
  **not** prove:
93
92
  - that linkedom's ACTUAL behavior matches "absent" exactly (linkedom may implement a
94
- PARTIAL or subtly-wrong version of an API rather than nothing at all always check
93
+ PARTIAL or subtly-wrong version of an API rather than nothing at all, always check
95
94
  the consumer's bug report for the exact error, don't assume "missing" when the
96
95
  report says something more specific);
97
96
  - anything about shape 2's ORIGINAL destructive-`stamp()` diagnosis (narrowed
98
- 2026-07-17) or shape 5 (declarative data binding, gh#288) neither is an
97
+ 2026-07-17) or shape 5 (declarative data binding, gh#288), neither is an
99
98
  API-absence bug, so this deletion-based method doesn't apply to them. Shape
100
99
  2's narrowing was verified with exactly the shape of test this section
101
- describes a container element given real children (or text) BEFORE
100
+ describes, a container element given real children (or text) BEFORE
102
101
  `document.appendChild()` connects it (simulating server-parsed markup that
103
- already has content when `connectedCallback` first runs) confirming
102
+ already has content when `connectedCallback` first runs), confirming
104
103
  `nav-ui`/`check-ui` preserve pre-existing content while a synthetic
105
104
  non-null-template component loses it (see
106
105
  [`failure-shapes.md`](failure-shapes.md) §2). That reproduction was a
107
- throwaway, not a shipped permanent test if shape 2 needs re-verifying later
106
+ throwaway, not a shipped permanent test, if shape 2 needs re-verifying later
108
107
  (a new component trips `audit-template-child-conflict.mjs`, or the consumer
109
108
  reports something new), rebuild it the same way rather than assuming the old
110
109
  narrowing still holds.
111
110
 
112
111
  **Shape 2b (the attribute-upgrade-replay gap, gh#284's real fix, 2026-07-18)
113
- IS testable this way, and now HAS a shipped permanent test** it's not an
112
+ IS testable this way, and now HAS a shipped permanent test**, it's not an
114
113
  API-absence bug either, but the exact same "define the tag AFTER the markup
115
114
  already exists" sequence reproduces it directly, no deletion trick needed
116
115
  (the gap is happy-dom's own upgrade behavior, not a missing API to delete).
117
116
  See `packages/web-components/core/element.test.js`,
118
- `describe('UIElement SSR attribute-upgrade replay (gh#284)')`, for the
117
+ `describe('UIElement: SSR attribute-upgrade replay (gh#284)')`, for the
119
118
  canonical `mountLateUpgrade()` helper and four tests: confirms the
120
119
  environment gap on a bare custom element, confirms a reflected string prop
121
120
  and a reflected boolean prop both resolve correctly post-fix, and confirms
@@ -126,19 +125,19 @@ Deletion proves "the guarded code path doesn't throw when the API is absent." It
126
125
  ## Live-browser confirmation is still required, separately
127
126
 
128
127
  The delete/try/finally tests prove the GUARD doesn't crash. They do not prove real
129
- browsers are unaffected that's a SEPARATE check, done live (Playwright or the
128
+ browsers are unaffected, that's a SEPARATE check, done live (Playwright or the
130
129
  Chrome extension), confirming the real API still gets used correctly when it's
131
130
  actually present. Both gh#285 and gh#286's fixes were verified this second way before
132
131
  shipping (a real `ElementInternals` instance still used and functional; a real
133
- `ResizeObserver` tick still corrects state) do both, never one instead of the other.
132
+ `ResizeObserver` tick still corrects state), do both, never one instead of the other.
134
133
 
135
- ## The deeper simulation DONE 2026-08-17
134
+ ## The deeper simulation, DONE 2026-08-17
136
135
 
137
136
  The real-`linkedom` simulation this section used to propose is now the shipped
138
137
  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
138
+ `linkedom` pinned as a root devDependency, used only there, the main suite stays
140
139
  on happy-dom). It sweeps import + connect for the whole catalog; a shape-2/4
141
140
  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
141
+ still answered by a targeted test, extend the script's `FIXTURES` table with the
143
142
  attrs/children in question, or write the assertion in
144
143
  `test/ssr-upgrade-adoption-contract.test.js`'s late-upgrade shape.