@adia-ai/adia-ui-forge 0.8.62 → 0.8.63
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.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/CHANGELOG.md +300 -275
- package/README.md +54 -18
- package/__init__.py +8 -8
- package/agents/a2ui-maintenance-agent.corpus.json +7 -7
- package/agents/a2ui-maintenance-agent.md +6 -6
- package/agents/a2ui-planner-agent.md +11 -11
- package/agents/demo-audit-agent.corpus.json +4 -4
- package/agents/demo-audit-agent.md +8 -9
- package/agents/framework-checker.md +10 -11
- package/agents/framework-planner-agent.md +9 -10
- package/agents/gen-ui-review-agent.corpus.json +4 -4
- package/agents/gen-ui-review-agent.md +8 -8
- package/agents/package-release-agent.corpus.json +4 -4
- package/agents/package-release-agent.md +9 -10
- package/agents/primitive-authoring-agent.corpus.json +1 -1
- package/agents/primitive-authoring-agent.md +5 -5
- package/agents/routing-corpus.json +46 -46
- package/codex.interface.json +1 -1
- package/commands/gen-ui-review.md +1 -1
- package/commands/package-release.md +2 -2
- package/package.json +1 -1
- package/plugin.yaml +2 -2
- package/prompts/gen-ui-review.md +1 -1
- package/prompts/package-release.md +2 -2
- package/references/contracts/a2ui-mcp-surface.md +7 -7
- package/references/contracts/migration-guide-format.md +6 -7
- package/references/shared/content-trust.md +3 -5
- package/skills/a2ui-maintenance/SKILL.md +13 -16
- package/skills/a2ui-maintenance/evals/evals.json +2 -2
- package/skills/a2ui-maintenance/evals/routing-corpus.json +1 -1
- package/skills/a2ui-maintenance/references/anti-patterns.md +10 -10
- package/skills/a2ui-maintenance/references/chunk-authoring.md +11 -12
- package/skills/a2ui-maintenance/references/corpus-discipline.md +13 -14
- package/skills/a2ui-maintenance/references/data-model-reactivity.md +22 -25
- package/skills/a2ui-maintenance/references/eval-diagnostics.md +21 -21
- package/skills/a2ui-maintenance/references/format-extension-decisions.md +12 -13
- package/skills/a2ui-maintenance/references/leverage-rules.md +6 -6
- package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +9 -10
- package/skills/a2ui-maintenance/references/mcp-tool-reference.md +11 -12
- package/skills/a2ui-maintenance/references/pipeline-overview.md +33 -38
- package/skills/a2ui-maintenance/references/semantic-fail-lifting.md +13 -14
- package/skills/a2ui-maintenance/references/strategy-engines.md +14 -16
- package/skills/a2ui-maintenance/references/surface-lifecycle.md +10 -11
- package/skills/a2ui-maintenance/references/zettel-calibration.md +14 -16
- package/skills/component-md-authoring/SKILL.md +20 -21
- package/skills/component-md-authoring/agents/openai.yaml +1 -1
- package/skills/cross-harness-authoring-standards/SKILL.md +19 -21
- package/skills/cross-harness-authoring-standards/agents/openai.yaml +1 -1
- package/skills/cross-harness-compatibility-standards/SKILL.md +34 -35
- package/skills/cross-harness-compatibility-standards/agents/openai.yaml +1 -1
- package/skills/demo-audit/SKILL.md +38 -38
- package/skills/demo-audit/evals/routing-corpus.json +1 -1
- package/skills/demo-audit/references/admin-shell-anatomy.md +21 -25
- package/skills/demo-audit/references/app-shell-pitfalls.md +11 -13
- package/skills/demo-audit/references/auto-fix-allowlist.md +3 -3
- package/skills/demo-audit/references/card-anatomy-sweep.md +10 -11
- package/skills/demo-audit/references/chat-shell-anatomy.md +25 -26
- package/skills/demo-audit/references/editor-shell-anatomy.md +25 -28
- package/skills/demo-audit/references/html-attr-sweep.md +12 -13
- package/skills/demo-audit/references/mode7-status-battery.md +4 -4
- package/skills/demo-audit/references/native-leak-annotations.md +8 -8
- package/skills/demo-audit/references/output-contract-worked-example.md +8 -8
- package/skills/demo-audit/references/probe-discipline-and-escalation.md +2 -2
- package/skills/demo-audit/references/visual-probe-triage.md +21 -23
- package/skills/gen-ui-review/SKILL.md +27 -27
- package/skills/gen-ui-review/agents/openai.yaml +1 -1
- package/skills/gen-ui-review/evals/routing-corpus.json +1 -1
- package/skills/gen-ui-review/references/corpus-html-patterns.md +28 -29
- package/skills/gen-ui-review/references/exit-gate-mechanics.md +5 -5
- package/skills/gen-ui-review/references/lookup-maintenance.md +2 -2
- package/skills/gen-ui-review/references/loop-protocol.md +25 -25
- package/skills/gen-ui-review/references/rubric-cosmetic.md +10 -10
- package/skills/gen-ui-review/references/rubric-decompose.md +15 -16
- package/skills/gen-ui-review/references/rubric-score.md +23 -24
- package/skills/gen-ui-review/references/scorecard-worked-examples.md +12 -12
- package/skills/gen-ui-review/references/scores.schema.json +2 -2
- package/skills/llm-client-maintenance/SKILL.md +12 -12
- package/skills/llm-client-maintenance/evals/routing-corpus.json +1 -1
- package/skills/llm-client-maintenance/references/adapter-contract.md +27 -27
- package/skills/llm-client-maintenance/references/add-a-provider.md +25 -25
- package/skills/llm-client-maintenance/references/bridge-facade.md +31 -31
- package/skills/llm-client-maintenance/references/browser-proxy-boundary.md +17 -17
- package/skills/llm-client-maintenance/references/model-registry.md +14 -14
- package/skills/llm-client-maintenance/references/streaming-sse.md +21 -21
- package/skills/package-release/SKILL.md +21 -23
- package/skills/package-release/evals/evals.json +2 -2
- package/skills/package-release/evals/routing-corpus.json +2 -2
- package/skills/package-release/references/authorization-model.md +6 -8
- package/skills/package-release/references/changelog-discipline.md +25 -27
- package/skills/package-release/references/cut-procedure.md +129 -136
- package/skills/package-release/references/gates-catalog.md +42 -43
- package/skills/package-release/references/independent-package-release.md +14 -15
- package/skills/package-release/references/invariants-detail.md +9 -9
- package/skills/package-release/references/mechanization.md +11 -12
- package/skills/package-release/references/migration-guide-authoring.md +20 -20
- package/skills/package-release/references/notes-authoring.md +20 -20
- package/skills/package-release/references/recovery-paths.md +28 -28
- package/skills/primitive-authoring/SKILL.md +19 -19
- package/skills/primitive-authoring/agents/openai.yaml +1 -1
- package/skills/primitive-authoring/evals/evals.json +2 -2
- package/skills/primitive-authoring/evals/routing-corpus.json +2 -2
- package/skills/primitive-authoring/references/INDEX.md +22 -22
- package/skills/primitive-authoring/references/anti-patterns.md +76 -77
- package/skills/primitive-authoring/references/api-contract.md +63 -65
- package/skills/primitive-authoring/references/authoring-cycle.md +47 -50
- package/skills/primitive-authoring/references/canonical-pattern-index.md +118 -118
- package/skills/primitive-authoring/references/code-style.md +50 -52
- package/skills/primitive-authoring/references/common-gotchas.md +23 -23
- package/skills/primitive-authoring/references/composite-demo-protocol.md +42 -45
- package/skills/primitive-authoring/references/css-patterns.md +97 -97
- package/skills/primitive-authoring/references/for-attribute-event-contract.md +12 -12
- package/skills/primitive-authoring/references/form-control-sizing.md +16 -18
- package/skills/primitive-authoring/references/lifecycle-patterns.md +42 -42
- package/skills/primitive-authoring/references/llm-bridge.md +21 -21
- package/skills/primitive-authoring/references/module-promotion.md +51 -51
- package/skills/primitive-authoring/references/primitive-audit.md +16 -16
- package/skills/primitive-authoring/references/shell-patterns.md +81 -81
- package/skills/primitive-authoring/references/svg-authoring.md +62 -66
- package/skills/primitive-authoring/references/token-contract.md +45 -49
- package/skills/primitive-authoring/references/trait-pages.md +8 -8
- package/skills/primitive-authoring/references/worked-example.md +39 -39
- package/skills/primitive-authoring/references/yaml-contract.md +139 -143
- package/skills/site-deployment/SKILL.md +25 -26
- package/skills/site-deployment/agents/openai.yaml +1 -1
- package/skills/site-deployment/evals/routing-corpus.json +1 -1
- package/skills/site-deployment/references/deploy-playbooks.md +51 -53
- package/skills/site-docs-authoring/SKILL.md +14 -14
- package/skills/site-docs-authoring/agents/openai.yaml +1 -1
- package/skills/site-docs-authoring/evals/evals.json +1 -1
- package/skills/site-docs-authoring/evals/routing-corpus.json +1 -1
- package/skills/site-docs-authoring/intent.md +18 -18
- package/skills/ssr-compatibility/SKILL.md +24 -25
- package/skills/ssr-compatibility/agents/openai.yaml +1 -1
- package/skills/ssr-compatibility/evals/evals.json +1 -1
- package/skills/ssr-compatibility/evals/routing-corpus.json +1 -1
- package/skills/ssr-compatibility/references/consumer-workarounds.md +22 -23
- package/skills/ssr-compatibility/references/failure-shapes.md +84 -91
- package/skills/ssr-compatibility/references/guard-patterns.md +54 -58
- package/skills/ssr-compatibility/references/status-ledger.md +17 -17
- package/skills/ssr-compatibility/references/test-without-linkedom.md +26 -27
|
@@ -1,206 +1,200 @@
|
|
|
1
|
-
# SSR failure shapes
|
|
1
|
+
# SSR failure shapes, symptom → root cause → status
|
|
2
2
|
|
|
3
|
-
The §-numbered root-cause classes below (count the `##` headings
|
|
3
|
+
The §-numbered root-cause classes below (count the `##` headings, the list
|
|
4
4
|
grows) have surfaced from real SSR consumers (adiav2's
|
|
5
5
|
`admin-portal-fe` and `factory-dashboard`, server-rendering AdiaUI via Astro 5 +
|
|
6
|
-
`custom-elements-ssr`, which runs on linkedom
|
|
6
|
+
`custom-elements-ssr`, which runs on linkedom, a DOM shim with no layout engine and
|
|
7
7
|
missing many browser APIs). A new SSR bug report almost always maps onto one of
|
|
8
8
|
them; misclassifying it (e.g. treating a measurement-timing bug as a missing-API
|
|
9
9
|
bug) sends the fix to the wrong place. Check symptom against this table first.
|
|
10
10
|
|
|
11
11
|
## 1 · Browser-only API called unconditionally → crash
|
|
12
12
|
|
|
13
|
-
**Symptom:** the SSR pass throws
|
|
13
|
+
**Symptom:** the SSR pass throws, often at construction, before any component-specific
|
|
14
14
|
code runs. Stack trace points at `attachInternals`, `new ResizeObserver(...)`, `new
|
|
15
15
|
IntersectionObserver(...)`, `new MutationObserver(...)`, `new PerformanceObserver(...)`,
|
|
16
16
|
or a write to `document.adoptedStyleSheets`.
|
|
17
17
|
|
|
18
|
-
**Root cause:** the call site assumes the API exists. linkedom implements none of these
|
|
19
|
-
not a partial/quirky implementation, an absence. Any unconditional call throws
|
|
18
|
+
**Root cause:** the call site assumes the API exists. linkedom implements none of these, not a partial/quirky implementation, an absence. Any unconditional call throws
|
|
20
19
|
`TypeError` or `ReferenceError` (undefined global) immediately.
|
|
21
20
|
|
|
22
|
-
**Status: FIXED, twice
|
|
21
|
+
**Status: FIXED, twice, and now GATED.** First wave gh#285 (PR #292, merged
|
|
23
22
|
2026-07-17): `UIElement`'s constructor (`packages/web-components/core/element.js`)
|
|
24
|
-
plus a sweep of component/trait/module files
|
|
23
|
+
plus a sweep of component/trait/module files: the per-file tally is
|
|
25
24
|
[`status-ledger.md`](status-ledger.md)'s #285 row (the ledger, not this line, is
|
|
26
25
|
the count of record). Second wave gh#1430 + gh#1436 (2026-08-17, found by adiav2's
|
|
27
26
|
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 !==
|
|
27
|
+
- **environment-detection instead of feature-detection at MODULE scope**, `core/responsive.js` guarded `window.matchMedia(...)` behind `typeof window !==
|
|
30
28
|
'undefined'`; linkedom HAS a `window`, just no `matchMedia` and no numeric
|
|
31
29
|
`innerWidth`, so six components (`block`/`col`/`grid`/`row`/`text`/`demo-toggle`)
|
|
32
30
|
could not be IMPORTED server-side at all;
|
|
33
|
-
- **bare `instanceof Node|Element|HTMLElement`** (15 sites
|
|
34
|
-
`applyValue`, table/list-window renderer results, event-target checks)
|
|
31
|
+
- **bare `instanceof Node|Element|HTMLElement`** (15 sites, `core/template.js`
|
|
32
|
+
`applyValue`, table/list-window renderer results, event-target checks), a
|
|
35
33
|
`ReferenceError`, not `false`, because custom-elements-ssr installs ONLY
|
|
36
34
|
`HTMLElement` as a global (never `Node`, `Element`, `Text`, `DocumentFragment`);
|
|
37
35
|
now `core/dom.js` `isNode()`/`isElement()` (structural `nodeType` tests);
|
|
38
36
|
- **`requestAnimationFrame` / `MutationObserver` / `getBoundingClientRect` at
|
|
39
37
|
connect** in feed-item/toast, noodles, preview, toolbar (rAF), nav-group,
|
|
40
|
-
stepper (MutationObserver
|
|
38
|
+
stepper (MutationObserver, the #292 sweep guarded four Observer sites, not
|
|
41
39
|
these two) and nav-ui (`getBoundingClientRect` is absent, not zero, on linkedom).
|
|
42
40
|
|
|
43
41
|
**Why happy-dom could not see any of it:** it implements every one of those APIs.
|
|
44
42
|
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
|
|
43
|
+
`packages/web-components/test/ssr-linkedom-smoke.test.js`, a real `linkedom`
|
|
46
44
|
devDependency installing EXACTLY the six globals `custom-elements-ssr/server-shim.js`
|
|
47
45
|
installs, then importing every `components/*/*.js` entry and constructing +
|
|
48
46
|
connecting every registered tag (124/124 import, every tag renders). A new
|
|
49
47
|
shape-1 instance fails that test, not a consumer's build. The fix pattern
|
|
50
48
|
(feature-detect + fallback matched to how the reference is used downstream) is
|
|
51
|
-
[`guard-patterns.md`](guard-patterns.md)
|
|
49
|
+
[`guard-patterns.md`](guard-patterns.md), apply it to any NEW call site; don't
|
|
52
50
|
re-derive the shape from scratch, and don't guard on `typeof window`.
|
|
53
51
|
|
|
54
52
|
## 2 · `connectedCallback` destructively re-stamps existing DOM → content loss
|
|
55
53
|
|
|
56
54
|
**Symptom (as originally reported):** the component renders in the SSR HTML response
|
|
57
|
-
as an EMPTY or STRUCTURALLY WRONG tag
|
|
55
|
+
as an EMPTY or STRUCTURALLY WRONG tag, not a crash, a silent loss. Two named
|
|
58
56
|
variants:
|
|
59
57
|
- A **container** (`admin-shell`, `admin-sidebar`, `nav-ui`) loses its nested custom-element
|
|
60
|
-
children
|
|
58
|
+
children, `<nav-item-ui>` inside `<nav-ui>` simply isn't in the response.
|
|
61
59
|
- A **projected-text** leaf (`<text-ui>Adia Admin</text-ui>`, `<avatar-ui>A</avatar-ui>`,
|
|
62
|
-
`<badge-ui>warning</badge-ui>`) goes out as an empty tag
|
|
60
|
+
`<badge-ui>warning</badge-ui>`) goes out as an empty tag: the authored text is gone.
|
|
63
61
|
|
|
64
62
|
**Root cause (still real):** `UIElement.connectedCallback` (`packages/web-components/core/element.js`)
|
|
65
63
|
runs `stamp(result, this)` (the `stamp` function in `packages/web-components/core/template.js`,
|
|
66
|
-
currently at line 194) with the OWN template's output
|
|
64
|
+
currently at line 194) with the OWN template's output, this OVERWRITES the element's
|
|
67
65
|
existing children rather than adopting/patching them. In a real browser this is
|
|
68
66
|
correct and invisible: nothing exists inside the element yet at first connect. Under
|
|
69
67
|
SSR, `connectedCallback` runs a SECOND TIME against the server-parsed DOM (which
|
|
70
|
-
already has real, meaningful children from the HTML response)
|
|
68
|
+
already has real, meaningful children from the HTML response), the stamp silently
|
|
71
69
|
replaces them.
|
|
72
70
|
|
|
73
|
-
**Status: NARROWED, downgraded 2026-07-17** (gh#284). Verified against 0.8.4
|
|
71
|
+
**Status: NARROWED, downgraded 2026-07-17** (gh#284). Verified against 0.8.4: the
|
|
74
72
|
mechanism is real (a synthetic repro, a component with a non-null template AND
|
|
75
73
|
projected text, does lose it), but **every named example in the issue currently
|
|
76
74
|
has `static template = () => null`**: `admin-shell`, `admin-sidebar`, `nav-ui`,
|
|
77
75
|
`text-ui`, `badge-ui`, `avatar-ui` all skip `stamp()` entirely (`if (result)
|
|
78
|
-
stamp(result, this)
|
|
76
|
+
stamp(result, this)`, `null` never enters the branch). A framework-wide survey
|
|
79
77
|
(150 components at the time) found every component with a NON-null template derives its
|
|
80
78
|
visible content from properties/attributes only (`check-ui`'s `label=`,
|
|
81
|
-
`switch-ui`'s `label=`/`hint=`, `skip-nav`'s `text=`)
|
|
82
|
-
children
|
|
79
|
+
`switch-ui`'s `label=`/`hint=`, `skip-nav`'s `text=`), never from light-DOM
|
|
80
|
+
children, so the destructive replace, where it does fire, only ever regenerates
|
|
83
81
|
identical, template-owned content. Zero shipped components are exposed to the
|
|
84
82
|
originally-reported symptom right now.
|
|
85
83
|
|
|
86
84
|
**What shipped instead of a `stamp()` fix:** the two candidate directions the issue
|
|
87
85
|
named (adopt-and-patch, or skip-on-marker) were explicitly rejected as
|
|
88
|
-
disproportionate
|
|
89
|
-
framework (127+), not a mechanical guard sweep"
|
|
86
|
+
disproportionate, "changes the render lifecycle of every primitive in the
|
|
87
|
+
framework (127+), not a mechanical guard sweep", for a risk with zero current
|
|
90
88
|
instances. Shipped a forward-looking STATIC AUDIT instead:
|
|
91
|
-
`scripts/dev/audit-template-child-conflict.mjs
|
|
89
|
+
`scripts/dev/audit-template-child-conflict.mjs`, flags (critical) any future
|
|
92
90
|
component pairing a non-null `static template` with a yaml `slots.default` entry
|
|
93
91
|
(the container shape), and (advisory) a non-null template paired with a body-text
|
|
94
92
|
usage example (the projected-text shape, no mechanical yaml signal to check). Wired
|
|
95
93
|
into `primitive-authoring`'s structural-gate sequence. This is an operator ruling (not a
|
|
96
|
-
unilateral call)
|
|
94
|
+
unilateral call), see gh#284's comment thread for the full reasoning and the
|
|
97
95
|
empirical survey it's based on.
|
|
98
96
|
|
|
99
97
|
**Current consumer workaround still exists but may be over-conservative:**
|
|
100
98
|
`adiav2`'s SSR component registration is restricted to attribute-only-content
|
|
101
|
-
components (`button-ui text="…"`, `icon-ui name="…"`)
|
|
99
|
+
components (`button-ui text="…"`, `icon-ui name="…"`), per this narrowing, EVERY
|
|
102
100
|
currently-shipped component (container or leaf) with real content already
|
|
103
101
|
qualifies as attribute/property-driven, so the restriction may no longer be
|
|
104
102
|
necessary for content-loss reasons. Don't assume this without the consumer
|
|
105
|
-
confirming it via the issue thread
|
|
103
|
+
confirming it via the issue thread, see [`consumer-workarounds.md`](consumer-workarounds.md).
|
|
106
104
|
|
|
107
|
-
**UPDATE 2026-07-18
|
|
105
|
+
**UPDATE 2026-07-18, a DIFFERENT, real bug was found and fixed in the same
|
|
108
106
|
investigation area (gh#284's comment thread, PR #309).** The narrowing above
|
|
109
|
-
rules out `stamp()`'s destructive replace as a live risk
|
|
107
|
+
rules out `stamp()`'s destructive replace as a live risk, but it does NOT mean
|
|
110
108
|
attribute/property-driven components were actually safe under a late/SSR
|
|
111
109
|
upgrade. They weren't, for an unrelated reason: the custom-elements spec's
|
|
112
110
|
"upgrade an element" algorithm (§4.13.5 step 6) requires replaying
|
|
113
111
|
`attributeChangedCallback` for every attribute already present on an element
|
|
114
|
-
BEFORE `connectedCallback` fires on upgrade
|
|
112
|
+
BEFORE `connectedCallback` fires on upgrade, and happy-dom (this repo's test
|
|
115
113
|
DOM) skips that replay entirely, confirmed with a bare, framework-free custom
|
|
116
114
|
element, no AdiaUI code involved. linkedom (the real SSR consumer's shim) is a
|
|
117
115
|
similarly from-scratch custom-elements registry, so the same gap is expected
|
|
118
|
-
there too. Net effect: `<nav-item-ui text="Profile"
|
|
119
|
-
attribute-driven shape this narrowing said was safe
|
|
116
|
+
there too. Net effect: `<nav-item-ui text="Profile">`, exactly the
|
|
117
|
+
attribute-driven shape this narrowing said was safe, rendered with an EMPTY
|
|
120
118
|
label after a late upgrade, because `this.text` never got initialized from the
|
|
121
119
|
attribute at all. **Fixed**: `UIElement.connectedCallback` now re-syncs every
|
|
122
120
|
declared property from its live attribute value before `connected()` runs
|
|
123
|
-
(`packages/web-components/core/element.js`)
|
|
121
|
+
(`packages/web-components/core/element.js`), see
|
|
124
122
|
[`guard-patterns.md`](guard-patterns.md) §2b for the fix shape. Regression
|
|
125
123
|
tests: `packages/web-components/core/element.test.js`,
|
|
126
|
-
`describe('UIElement
|
|
124
|
+
`describe('UIElement: SSR attribute-upgrade replay (gh#284)')`.
|
|
127
125
|
|
|
128
126
|
This means the FULL current picture for shape 2 is: destructive `stamp()`
|
|
129
127
|
re-mounting is real-but-latent (static audit catches a future regression);
|
|
130
128
|
the attribute-upgrade-replay gap was real-and-live (now fixed in #309). A
|
|
131
129
|
future "content vanished under SSR" report should check the attribute-replay
|
|
132
|
-
mechanism FIRST
|
|
130
|
+
mechanism FIRST: it's the one that was actually firing.
|
|
133
131
|
|
|
134
|
-
### Worked example
|
|
132
|
+
### Worked example, answering a new "content vanished" report
|
|
135
133
|
|
|
136
134
|
**Ask:** "`<text-ui>Adia Admin</text-ui>` renders as an empty tag in our SSR
|
|
137
|
-
output
|
|
135
|
+
output, is this a known issue?"
|
|
138
136
|
|
|
139
137
|
**Answer:** It matches shape 2's SYMPTOM (`connectedCallback` destructively
|
|
140
|
-
re-stamping existing DOM), but shape 2 was narrowed on 2026-07-17
|
|
138
|
+
re-stamping existing DOM), but shape 2 was narrowed on 2026-07-17, verify
|
|
141
139
|
against the CURRENT code before reusing the old answer, because this is
|
|
142
140
|
exactly the case it no longer covers. `text-ui`'s `static template`
|
|
143
|
-
(`packages/web-components/components/text/text.class.js`) is `() => null`
|
|
144
|
-
`connectedCallback`'s `if (result) stamp(result, this)` never enters the
|
|
141
|
+
(`packages/web-components/components/text/text.class.js`) is `() => null`, `connectedCallback`'s `if (result) stamp(result, this)` never enters the
|
|
145
142
|
branch, so `stamp()` never touches `<text-ui>`'s children at all. This
|
|
146
|
-
component isn't exposed to shape 2; something else is dropping the text
|
|
147
|
-
check whether `text-ui` is even registered server-side (a different,
|
|
143
|
+
component isn't exposed to shape 2; something else is dropping the text, check whether `text-ui` is even registered server-side (a different,
|
|
148
144
|
structural gap: is the tag defined before the SSR pass runs?), or whether
|
|
149
145
|
another mutation (a parent re-render, `innerHTML` elsewhere) is clearing
|
|
150
146
|
it. **The general lesson, not just this one component:** before answering
|
|
151
147
|
"yes, known issue, shape 2" for ANY new report, grep the component's own
|
|
152
|
-
`static template
|
|
148
|
+
`static template`, if it's the literal `() => null`, shape 2 cannot be the
|
|
153
149
|
cause, no matter how closely the symptom matches the old description. This
|
|
154
|
-
survey (150 components at the 2026-07 survey
|
|
150
|
+
survey (150 components at the 2026-07 survey, the census has since grown)
|
|
155
151
|
cites exactly why every current children-accepting component is unaffected.
|
|
156
152
|
|
|
157
153
|
## 3 · A connect-time layout MEASUREMENT is meaningless before real layout exists
|
|
158
154
|
|
|
159
155
|
**Symptom:** a component makes a decision (a boolean state, a mode, a snapped value)
|
|
160
156
|
by synchronously reading `getBoundingClientRect()` (or similar) inside `connected()`,
|
|
161
|
-
and that decision comes out WRONG
|
|
157
|
+
and that decision comes out WRONG, not crashed, not empty, just incorrect, in any
|
|
162
158
|
environment where real layout hasn't happened yet. This includes linkedom (no layout
|
|
163
|
-
engine at all
|
|
164
|
-
element before its first layout pass (inside a `display:none` ancestor, for instance)
|
|
165
|
-
SSR is the environment that surfaces it reliably, but the bug is not SSR-specific.
|
|
159
|
+
engine at all, always returns a 0×0 rect) but ALSO a real browser connecting an
|
|
160
|
+
element before its first layout pass (inside a `display:none` ancestor, for instance), SSR is the environment that surfaces it reliably, but the bug is not SSR-specific.
|
|
166
161
|
|
|
167
|
-
**Root cause:** treating "the rect read 0" as equivalent to "the rect really is 0"
|
|
168
|
-
they are NOT the same fact. A zero rect from a shim/pre-layout read means "unknown,"
|
|
162
|
+
**Root cause:** treating "the rect read 0" as equivalent to "the rect really is 0", they are NOT the same fact. A zero rect from a shim/pre-layout read means "unknown,"
|
|
169
163
|
and a decision derived from "unknown" as if it were "confirmed small" is a category
|
|
170
164
|
error, not a rendering gap.
|
|
171
165
|
|
|
172
166
|
**Status: FIXED for `admin-sidebar`'s specific instance** (gh#286, PR #290, merged
|
|
173
|
-
2026-07-17)
|
|
167
|
+
2026-07-17), see [`guard-patterns.md`](guard-patterns.md) §3 for the fix shape
|
|
174
168
|
(treat a zero read as unknown, defer to the component's own `ResizeObserver`'s first
|
|
175
|
-
real tick). **The general pattern is NOT swept framework-wide
|
|
169
|
+
real tick). **The general pattern is NOT swept framework-wide**, any OTHER component
|
|
176
170
|
that derives a persistent decision from a synchronous connect-time measurement
|
|
177
171
|
carries the same latent bug, undiscovered until someone hits it. If you're
|
|
178
172
|
investigating a "wrong initial state under SSR" report that ISN'T a missing-API crash
|
|
179
173
|
(shape 1) or a content-loss (shape 2), check whether the component reads a rect/size
|
|
180
174
|
synchronously at connect and treat that as the working hypothesis first.
|
|
181
175
|
|
|
182
|
-
## 4 · Feature gap, not a bug
|
|
176
|
+
## 4 · Feature gap, not a bug, property-only components can't seed from SSR HTML
|
|
183
177
|
|
|
184
|
-
**Symptom:** not a bug report at all
|
|
178
|
+
**Symptom:** not a bug report at all, a `table-ui`/`chart-ui`/`select-ui` with
|
|
185
179
|
programmatic-only content (`.columns`, `.data`, `.options` set as JS properties)
|
|
186
180
|
renders correctly in the browser but is STRUCTURALLY ABSENT from the SSR HTML response,
|
|
187
181
|
because JS property assignments don't serialize into server-rendered markup. The
|
|
188
182
|
component pops in empty and fills in after a post-hydration wiring script runs.
|
|
189
183
|
|
|
190
184
|
**Root cause:** no declarative (HTML-serializable) form of the data these components
|
|
191
|
-
need exists yet
|
|
185
|
+
need exists yet, property-only content is a deliberate API shape for CONSUMERS with a
|
|
192
186
|
live client, not a gap in any individual component's code.
|
|
193
187
|
|
|
194
188
|
**Status: CLOSED 2026-07-18** (gh#288). The premise that this was blocked on shape
|
|
195
|
-
2/2b never held
|
|
189
|
+
2/2b never held, `table-ui`/`chart-ui`/`select-ui` all use `static template = ()
|
|
196
190
|
=> null`, so neither the narrowed stamp() mechanism nor its later fix ever bore on
|
|
197
191
|
this at all. On investigation the scope was also narrower than filed:
|
|
198
192
|
|
|
199
193
|
- **`select-ui`** already parsed native `<option>`/`<optgroup>` children
|
|
200
|
-
declaratively at connect (`#parseOptions()`, `select.class.js`)
|
|
194
|
+
declaratively at connect (`#parseOptions()`, `select.class.js`), plain HTML,
|
|
201
195
|
serializes into SSR output fine. No gap, no fix needed.
|
|
202
196
|
- **`chart-ui`** already hydrated `.data` from a JSON-array `data="[…]"` HTML
|
|
203
|
-
attribute at connect (`chart.class.js` `connected()`)
|
|
197
|
+
attribute at connect (`chart.class.js` `connected()`), shipped in earlier
|
|
204
198
|
work, just never reconciled against this issue.
|
|
205
199
|
- **`table-ui`** was the actual gap: `.columns` had `<col-def>` children as its
|
|
206
200
|
declarative form, but `.data` (row records) had none. Fixed in the SAME shape
|
|
@@ -210,18 +204,18 @@ this at all. On investigation the scope was also narrower than filed:
|
|
|
210
204
|
a real programmatic `.data =` set before connect always wins. Works together
|
|
211
205
|
with `<col-def>` children for a fully static-HTML table. Tests:
|
|
212
206
|
`packages/web-components/components/table/table.test.js`,
|
|
213
|
-
`describe('table-ui
|
|
207
|
+
`describe('table-ui: declarative data="[…]" attribute (gh#288)')`.
|
|
214
208
|
|
|
215
|
-
## 5 · A custom render path unconditionally rebuilds a subtree that already matches
|
|
209
|
+
## 5 · A custom render path unconditionally rebuilds a subtree that already matches, SSR adopt-in-place
|
|
216
210
|
|
|
217
|
-
**Symptom:** distinct from shape 2
|
|
211
|
+
**Symptom:** distinct from shape 2: this is not `stamp()`'s destructive
|
|
218
212
|
replace (shape 2 is a `static template` mechanism, narrowed to zero live
|
|
219
213
|
instances). This is a component with `static template = () => null` whose
|
|
220
214
|
OWN hand-written `render()`/`connected()` still unconditionally
|
|
221
215
|
`replaceChild()`s or `setAttribute()`s every position on every invocation,
|
|
222
216
|
including the very first upgrade render against a byte-identical
|
|
223
217
|
server-rendered subtree. Not a crash (shape 1), not empty content (shape 2),
|
|
224
|
-
not a wrong measurement (shape 3)
|
|
218
|
+
not a wrong measurement (shape 3): the rendered RESULT is correct, but a
|
|
225
219
|
server-rendered subtree that already matched it gets torn down and rebuilt
|
|
226
220
|
anyway, violating a consumer's zero-subtree-mutation adoption contract
|
|
227
221
|
(adiav2's spec-ssr-kit AC-004a) and showing up as spurious host-attribute
|
|
@@ -234,77 +228,76 @@ values never actually change.
|
|
|
234
228
|
already there. `setAttribute()` queues a mutation record even when the new
|
|
235
229
|
value is byte-identical to the old one (confirmed directly: happy-dom and
|
|
236
230
|
linkedom both fire a record on a same-value `setAttribute` call, but never
|
|
237
|
-
fire one for a `removeAttribute()` on an already-absent attribute
|
|
238
|
-
spec's own asymmetry)
|
|
231
|
+
fire one for a `removeAttribute()` on an already-absent attribute, the
|
|
232
|
+
spec's own asymmetry), so "the value happens to match" is never enough on
|
|
239
233
|
its own; the write itself has to be skipped.
|
|
240
234
|
- **No adopt-existing-DOM path at all** for the container-level rebuild
|
|
241
|
-
(rows, cells, header)
|
|
235
|
+
(rows, cells, header), every position gets a fresh node and a
|
|
242
236
|
`replaceChild()`, whether or not the existing one is already correct.
|
|
243
237
|
|
|
244
238
|
**Status: implemented for `table-ui` in open PR #1756 (gh#1678), pending
|
|
245
|
-
merge
|
|
239
|
+
merge, not yet fixed/shipped.** `table.class.js`
|
|
246
240
|
`render()`/`connected()`: a module-level `adoptOrDiffChildren()` helper
|
|
247
241
|
compares each freshly-computed cell against its existing DOM position via
|
|
248
|
-
`Node.isEqualNode()
|
|
249
|
-
real browsers alike, never one of the browser-only APIs §1 above guards
|
|
250
|
-
and only calls `replaceChild()` on an actual mismatch; a match adopts the
|
|
242
|
+
`Node.isEqualNode()`, standard DOM, present under linkedom, happy-dom, and
|
|
243
|
+
real browsers alike, never one of the browser-only APIs §1 above guards, and only calls `replaceChild()` on an actual mismatch; a match adopts the
|
|
251
244
|
existing node in place, zero mutation. Host/row-level attribute writes
|
|
252
245
|
(`role`, `tabindex`, the grid-template-columns inline style, `data-index`,
|
|
253
246
|
`aria-selected`) go through a `setAttrIfChanged()` guard for the same
|
|
254
247
|
reason. This generalizes pagination-ui's own first-connect adoption fix
|
|
255
|
-
(gh#1687
|
|
248
|
+
(gh#1687, see `guard-patterns.md` §4) from a flat, keyed item list to an
|
|
256
249
|
arbitrary positional child (a header cell, a row cell) via a value check
|
|
257
250
|
instead of a shape/key check, since a table cell has no stable identity key
|
|
258
251
|
of its own the way a pagination item does. Tests:
|
|
259
252
|
`packages/web-components/components/table/table.test.js`,
|
|
260
|
-
`describe('table-ui
|
|
253
|
+
`describe('table-ui: SSR adopt-or-diff render path (gh#1678)')`, a real
|
|
261
254
|
`MutationObserver` proves zero mutations on a byte-identical upgrade, and a
|
|
262
255
|
deliberately-corrupted single cell proves the fallback rebuilds ONLY that
|
|
263
256
|
position, never a wider or a half-adopted rebuild.
|
|
264
257
|
|
|
265
|
-
### 5.1 · Structural equality is not sufficient
|
|
258
|
+
### 5.1 · Structural equality is not sufficient, renderer-owned runtime state
|
|
266
259
|
|
|
267
260
|
**A second, distinct hazard inside the same fix, found by CodeRabbit on
|
|
268
261
|
PR #1756 and closed in the same PR before merge.** `Node.isEqualNode()` is a
|
|
269
262
|
*necessary* adoption test (structurally different nodes obviously can't be
|
|
270
263
|
adopted) but not a *sufficient* one: it compares tag/attributes/text/
|
|
271
|
-
descendants only
|
|
264
|
+
descendants only, it has no way to see an event listener a renderer
|
|
272
265
|
attached to the node it returned. `table.class.js`'s `#updateRow()` runs
|
|
273
266
|
`col.render()` (an arbitrary consumer-supplied cell renderer) or a built-in
|
|
274
267
|
cell-type renderer (`typeDef.render`) BEFORE `adoptOrDiffChildren()` ever
|
|
275
268
|
compares the result. If that renderer attaches a listener to the node it
|
|
276
269
|
hands back, the candidate can still be structurally byte-identical to the
|
|
277
|
-
existing (listener-less, e.g. SSR-parsed) DOM
|
|
270
|
+
existing (listener-less, e.g. SSR-parsed) DOM, `isEqualNode()` reports a
|
|
278
271
|
match, the guard adopts the OLD node and silently discards the fresh one,
|
|
279
272
|
and the listener is gone. The pre-fix unconditional-`replaceChild()`
|
|
280
273
|
behavior never had this bug, because it always installed whatever the
|
|
281
274
|
renderer had just built.
|
|
282
275
|
|
|
283
276
|
**Fix shape:** a module-level `RENDERER_OWNED` `WeakSet` tags exactly the
|
|
284
|
-
candidate cells built by something free to attach runtime state
|
|
277
|
+
candidate cells built by something free to attach runtime state, `col.render()`
|
|
285
278
|
always (arbitrary code, impossible to introspect for safety), and a
|
|
286
279
|
built-in cell-type renderer only when its registration explicitly declares
|
|
287
280
|
`attachesListeners: true` (currently only `cellTypes.actions`, the one
|
|
288
|
-
built-in type that calls `addEventListener()` directly
|
|
281
|
+
built-in type that calls `addEventListener()` directly, see
|
|
289
282
|
`cell-types.js`). `adoptOrDiffChildren()` always replaces a
|
|
290
283
|
`RENDERER_OWNED` candidate, never adopts it via the structural-equality
|
|
291
284
|
path, even on an `isEqualNode()` match. Deliberately NOT tagged: `col.format()`,
|
|
292
285
|
the plain-text fallback, and every other built-in cell type
|
|
293
286
|
(text/number/currency/percent/date/datetime/boolean/badge/avatar/link/
|
|
294
|
-
markdown/progress)
|
|
287
|
+
markdown/progress), each of those only sets attributes on already-
|
|
295
288
|
declarative custom elements or plain nodes with no listeners, so tagging
|
|
296
289
|
them would trade away the zero-mutation benefit for the overwhelming common
|
|
297
290
|
case with no correctness gain. A first attempt at this fix tagged EVERY
|
|
298
291
|
`typeDef.render` cell unconditionally and broke the AC-004a zero-mutation
|
|
299
|
-
test above for exactly that reason
|
|
292
|
+
test above for exactly that reason, narrowed to the declared-flag form
|
|
300
293
|
before merge. Test: `table.test.js`, `'a renderer-owned cell (col.render
|
|
301
|
-
attaching a listener) stays interactive after an SSR-adopted upgrade'
|
|
294
|
+
attaching a listener) stays interactive after an SSR-adopted upgrade'`, a
|
|
302
295
|
button's click listener, attached inside `col.render()`, still fires after
|
|
303
296
|
an SSR-parsed (listener-less, structurally identical) upgrade.
|
|
304
297
|
|
|
305
298
|
**The general lesson for any OTHER adopt-or-diff work** (elsewhere in this
|
|
306
299
|
framework, or a future component): `isEqualNode()`/any purely-structural
|
|
307
|
-
diff can only prove a node's DECLARATIVE shape is safe to keep
|
|
300
|
+
diff can only prove a node's DECLARATIVE shape is safe to keep, never that
|
|
308
301
|
its imperative/runtime state (listeners, closures, anything a renderer
|
|
309
302
|
callback stashed on it) is. A renderer whose output is reused across
|
|
310
303
|
positions or invocations needs its own explicit "does this renderer attach
|
|
@@ -315,7 +308,7 @@ equivalent) rather than assuming structural equality is enough. See
|
|
|
315
308
|
**Not (yet) swept framework-wide.** Any OTHER component with a hand-written
|
|
316
309
|
`render()`/`connected()` that unconditionally rebuilds or re-stamps a
|
|
317
310
|
subtree carries the same latent gap until it's individually checked against
|
|
318
|
-
this shape
|
|
311
|
+
this shape, there is no static audit for this one the way shape 2 has
|
|
319
312
|
`audit-template-child-conflict.mjs`. Check for: a `replaceChild()`/
|
|
320
313
|
`setAttribute()` call inside a `render()`/`connected()` with no preceding
|
|
321
314
|
comparison against the existing DOM. The renderer-owned hazard in §5.1 is a
|
|
@@ -324,22 +317,22 @@ exists: does any renderer this component invokes attach a listener or
|
|
|
324
317
|
other runtime state, and if so, is it excluded from the structural-adopt
|
|
325
318
|
path the way `RENDERER_OWNED` excludes it here.
|
|
326
319
|
|
|
327
|
-
## 6 · The conditional-inject class
|
|
320
|
+
## 6 · The conditional-inject class, querySelector-guard-before-innerHTML, decision recorded (gh#1678)
|
|
328
321
|
|
|
329
322
|
**Shape:** `connected()` checks for a pre-existing structural child before
|
|
330
|
-
stamping one
|
|
323
|
+
stamping one, `if (!this.querySelector('input-ui')) { this.innerHTML =
|
|
331
324
|
…; }` (`search-ui`) or `this.#nav = this.querySelector(':scope >
|
|
332
325
|
nav[slot="nav"]'); if (!this.#nav) { … create fresh … }` (`pagination-ui`,
|
|
333
326
|
gh#1687). Two real, already-shipped instances; this is not a hypothetical
|
|
334
327
|
pattern.
|
|
335
328
|
|
|
336
329
|
**Decision (gh#1678 requirement 3):** this IS the correct, SSR-safe shape
|
|
337
|
-
for a component that owns exactly one structural child slot
|
|
330
|
+
for a component that owns exactly one structural child slot: it is the
|
|
338
331
|
SAME "adopt when it structurally matches, rebuild fresh when it doesn't"
|
|
339
332
|
principle §5 above ships for table-ui's cells and gh#1687 ships for
|
|
340
333
|
pagination-ui's item list, one level coarser (a single child, not a keyed
|
|
341
334
|
list or a per-cell diff). It is SSR-safe on exactly one condition: **the
|
|
342
|
-
guard's own `querySelector` target must be STRUCTURALLY specific
|
|
335
|
+
guard's own `querySelector` target must be STRUCTURALLY specific**, the
|
|
343
336
|
exact expected tag (`input-ui`) or slot (`nav[slot="nav"]`), never a
|
|
344
337
|
generic "does this element have any children at all" check, or a
|
|
345
338
|
mismatched pre-existing child (stale markup, a different component's
|
|
@@ -347,16 +340,16 @@ leftover DOM) gets silently adopted and mis-rendered. Both shipped
|
|
|
347
340
|
instances already satisfy this.
|
|
348
341
|
|
|
349
342
|
**What this decision does NOT yet close.** Adoption alone doesn't reach the
|
|
350
|
-
zero-mutation bar §5 establishes
|
|
343
|
+
zero-mutation bar §5 establishes, a component can correctly ADOPT the
|
|
351
344
|
pre-existing child and then still unconditionally re-`setAttribute()` it in
|
|
352
345
|
every subsequent `render()` pass, the exact §5 sub-cause. `search-ui`'s
|
|
353
346
|
`render()` (`this.#inputEl.setAttribute('placeholder', this.placeholder)`,
|
|
354
|
-
the `disabled` set/remove pair) does this today
|
|
347
|
+
the `disabled` set/remove pair) does this today, a byte-identical SSR
|
|
355
348
|
`<search-ui>` fragment upgrades with the right element adopted, but still
|
|
356
349
|
takes 1–2 redundant attribute-mutation records on that first render.
|
|
357
350
|
`pagination-ui`'s own `reconcile()`-driven `#updateItem()` writes are the
|
|
358
351
|
same shape one level down. **Scoped OUT of gh#1678** (table-ui's own render
|
|
359
|
-
path is that ticket's actual evidence and fix)
|
|
352
|
+
path is that ticket's actual evidence and fix), tracked as a follow-up:
|
|
360
353
|
extend `setAttrIfChanged()`-style idempotent guards to `search-ui`'s
|
|
361
354
|
`render()` and `pagination-ui`'s `#updateItem()`/`#createItem()` writes,
|
|
362
355
|
gh#1755 (filed alongside gh#1678's PR).
|