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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/agents/framework-verifier.md +13 -5
  4. package/bin/release-pretag-docs-gate +226 -0
  5. package/hooks/hooks.json +10 -0
  6. package/package.json +1 -1
  7. package/skills/adia-a2ui/SKILL.md +10 -2
  8. package/skills/adia-a2ui/references/pipeline-overview.md +1 -1
  9. package/skills/adia-author/SKILL.md +1 -0
  10. package/skills/adia-author/references/anti-patterns.md +7 -7
  11. package/skills/adia-author/references/api-contract.md +9 -9
  12. package/skills/adia-author/references/authoring-cycle.md +6 -6
  13. package/skills/adia-author/references/code-style.md +4 -4
  14. package/skills/adia-author/references/lifecycle-patterns.md +8 -8
  15. package/skills/adia-author/references/token-contract.md +5 -5
  16. package/skills/adia-author/references/worked-example.md +16 -16
  17. package/skills/adia-author/references/yaml-contract.md +1 -1
  18. package/skills/adia-deploy/SKILL.md +44 -0
  19. package/skills/adia-deploy/evals/routing-corpus.json +146 -0
  20. package/skills/adia-deploy/references/deploy-playbooks.md +1 -1
  21. package/skills/adia-dogfood/SKILL.md +64 -11
  22. package/skills/adia-dogfood/evals/routing-corpus.json +146 -0
  23. package/skills/adia-gen-review/SKILL.md +55 -0
  24. package/skills/adia-gen-review/evals/routing-corpus.json +146 -0
  25. package/skills/adia-llm-internals/evals/routing-corpus.json +146 -0
  26. package/skills/adia-llm-internals/references/adapter-contract.md +1 -1
  27. package/skills/adia-llm-internals/references/add-a-provider.md +1 -0
  28. package/skills/adia-llm-internals/references/bridge-facade.md +1 -1
  29. package/skills/adia-llm-internals/references/browser-proxy-boundary.md +8 -6
  30. package/skills/adia-llm-internals/references/model-registry.md +1 -1
  31. package/skills/adia-release/SKILL.md +12 -5
  32. package/skills/adia-release/evals/evals.json +24 -0
  33. package/skills/adia-release/references/changelog-discipline.md +4 -4
  34. package/skills/adia-release/references/cut-procedure.md +45 -11
  35. package/skills/adia-release/references/gates-catalog.md +3 -3
  36. package/skills/adia-release/references/recovery-paths.md +4 -2
  37. package/skills/adia-release/scripts/bump.mjs +32 -1
  38. package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
  39. package/skills/adia-release/scripts/gate-roster.mjs +71 -0
  40. package/skills/adia-release/scripts/insert-stub.mjs +35 -1
  41. package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
  42. package/skills/adia-release/scripts/release-pack.mjs +284 -53
  43. package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
  44. package/skills/adia-ssr/SKILL.md +110 -0
  45. package/skills/adia-ssr/evals/audit-report.md +63 -0
  46. package/skills/adia-ssr/evals/routing-corpus.json +138 -0
  47. package/skills/adia-ssr/references/consumer-workarounds.md +63 -0
  48. package/skills/adia-ssr/references/failure-shapes.md +122 -0
  49. package/skills/adia-ssr/references/guard-patterns.md +142 -0
  50. package/skills/adia-ssr/references/status-ledger.md +35 -0
  51. package/skills/adia-ssr/references/test-without-linkedom.md +113 -0
@@ -0,0 +1,63 @@
1
+ # adia-ssr skill audit (FLOOR)
2
+
3
+ Skill: `packages/plugins/adia-ui-forge/skills/adia-ssr/` · Standards: skill-authoring-standards · Lint: clean
4
+ Verdict: **PASS**
5
+
6
+ Species: knowledge (`disable-model-invocation: false`, `user-invocable: false`) — sibling in
7
+ shape to `packages/plugins/adia-ui-factory/skills/adia-tokens/`, confirmed.
8
+
9
+ | ID | Verdict | Severity | Evidence (file:line) | Fix |
10
+ |----|---------|----------|----------------------|-----|
11
+ | R1 Behavior delta | PASS | — | `SKILL.md:67-73` (caveat: don't invent a component-local workaround for gh#284 — deleting it removes the one line steering Claude away from a plausible wrong answer); `SKILL.md:36-38` (gh#288 dependency on gh#284 — deleting it, Claude could treat #288 as independently actionable); `SKILL.md:51` (test-without-linkedom consult row — deleting it loses the exact routing signal to the one file that explains why the repo's default test env can't reproduce SSR bugs) | none |
12
+ | R2 Trigger fidelity | PASS | — | Description (`SKILL.md:3-10`) phrasings "does X work under SSR", "why does X crash/disappear when server-rendered", "is this connect-time read safe" are the literal `should_route_to_ssr: true` phrases in `evals/routing-corpus.json:20,35,90`; fences ("NOT for implementing a fix (adia-author) or consumer host/hydration wiring (adia-host...)") correctly repel `ssr-adversarial-01/02/05` (routing-corpus.json:104-107, 110-114, 130-135) | none |
13
+ | R3 Species/dial agreement | PASS | — | `SKILL.md:11-12` both dials explicit and correct for knowledge species; name `adia-ssr` follows the established local `adia-*` topic-noun convention (matches sibling `adia-tokens`, `adia-a2ui`) rather than the generic `x-patterns` grammar — acceptable under the project's own precedent | none |
14
+ | R4 Register | PASS | — | Load-bearing lines cite exact file:line, PR, and issue numbers rather than describing generically, e.g. `SKILL.md:63-66` ("`UIElement.connectedCallback`... calls `stamp(result, this)`... currently line 194") — verified accurate, see Notes | none |
15
+ | R5 No restatement | PASS | — | No drift-pair found against `component-token-contract.md` / `component-implementation-patterns.md` / AGENTS.md — none cover SSR. Pack explicitly fences out generic web-components-SSR theory (`SKILL.md:97-99`), so it doesn't restate model-general knowledge either | none |
16
+ | R6 Position | MINOR | minor | Body is only ~108 lines (~2,000 tokens, well inside the 5,000-token survival window, so no practical compaction risk) — but structurally, "Deviation doctrine" and "Boundaries" (`SKILL.md:75-99`, gate-shaped content) sit *after* the "Worked example" (`SKILL.md:55-74`), inverted from the "contracts/gates first, examples last" rule | Move Deviation doctrine + Boundaries above the Worked example on next semantic edit; not urgent given total size |
17
+ | R8 Quantities | PASS | — | `127+ primitives`, `24 component/trait/module files`, `12 files` calling `this.internals.*`, `~110-line shim`, dated `2026-07-17`, gh#284-288 — all load-bearing counts are numeric, none vague | none |
18
+
19
+ R7 (Contracts): N/A — knowledge species, not procedural/command.
20
+
21
+ ## Notes — factual grounding (beyond the R1-R8 checklist, but relevant to a knowledge pack's core promise)
22
+
23
+ Spot-checked every concrete claim against the actual repo state rather than trusting the pack's
24
+ own self-description:
25
+
26
+ - `packages/web-components/core/template.js:194` — `export function stamp(result, container)` is
27
+ exactly where `failure-shapes.md` and the Worked example say it is.
28
+ - `packages/web-components/core/element.js` — `attachInternals` shim (`this.internals = typeof
29
+ this.attachInternals === 'function' ? this.attachInternals() : NOOP_INTERNALS`),
30
+ `adoptedStyleSheets` guard (`if (!('adoptedStyleSheets' in document)) return;`), and the
31
+ `connectedCallback` → `stamp()` call all match `guard-patterns.md` and `failure-shapes.md`
32
+ verbatim.
33
+ - `packages/web-modules/shell/admin-sidebar/admin-sidebar.js` — `#syncCollapsedFromWidth` (zero-rect
34
+ `return` before the decision) and `#setupChildResizeObserver` (`typeof ResizeObserver ===
35
+ 'undefined'` guard, deferred correction) match `guard-patterns.md` §3's cited code exactly,
36
+ including `SNAP_THRESHOLD`.
37
+ - `packages/web-components/core/element.test.js:259` — the `describe('UIElement — SSR
38
+ browser-API absence (gh#285)'` block matches `test-without-linkedom.md`'s cited delete/try/finally
39
+ pattern.
40
+ - `git log`: PR #292 (gh#285) and PR #290 (gh#286) both merged 2026-07-17, matching every date
41
+ cited in `status-ledger.md` and `consumer-workarounds.md`.
42
+ - No `linkedom` in any `package.json` in the tree — confirms `test-without-linkedom.md`'s central
43
+ claim.
44
+ - Live `gh issue view` for 284-288 today: #284 OPEN, #285 CLOSED, #286 CLOSED, #287 OPEN, #288
45
+ OPEN — matches `status-ledger.md`'s table exactly, row for row, including the "#287 is
46
+ unrelated" framing.
47
+
48
+ Every citable fact in this pack checked out. This is unusually high grounding for a hand-authored
49
+ knowledge pack — worth naming as the standard other `adia-*` knowledge skills should be held to.
50
+
51
+ ## Top 3
52
+
53
+ 1. **Ship as-is.** Zero blocking or major findings; lint clean; every code/issue citation
54
+ independently verified against the live repo and `gh issue view` — the pack's core promise
55
+ ("cited to the actual shipped/open code") is actually true today, not just claimed.
56
+ 2. **Routing corpus is well-formed and matches its own spec** — 12 trigger / 5 adversarial phrases
57
+ (29.4% adversarial, spec says ~29%), all 5 reference files covered by `expected_shape`, and the
58
+ 3 adversarial near-misses (gh#287 bundler bug, a2ui pipeline, generic hydration theory) are
59
+ genuinely hard boundary cases, not straw men.
60
+ 3. **One minor, non-blocking structural nit (R6):** the gate-shaped "Deviation doctrine" and
61
+ "Boundaries" sections trail the "Worked example" instead of leading it. Harmless at this body
62
+ size (~2K tokens, nowhere near the 5K compaction cutoff) — fix opportunistically on the next
63
+ semantic edit to this file, not worth a dedicated pass.
@@ -0,0 +1,138 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "name": "adia-ssr routing accuracy corpus",
4
+ "version": "1.0.0",
5
+ "purpose": "Routing-eval corpus for adia-ssr (adia-forge plugin). Each phrase declares whether adia-ssr SHOULD be the routing target. Scored by the estate's TF-IDF routing-eval runner against the skill's description (heuristic token overlap).",
6
+ "scoring_notes": "Heuristic signal, not ground truth. Treat misroutes as a prompt to tighten the skill description, never as a reason to keyword-stuff it. Real harness routing is LLM-driven.",
7
+ "license": "internal",
8
+ "scope": "adia-ssr routing — does this phrase activate adia-ssr?",
9
+
10
+ "minimums_per_spec": {
11
+ "trigger_phrases": 12,
12
+ "adversarial_phrases": 5,
13
+ "task_shapes_covered": "failure-shapes, guard-patterns, status-ledger, test-without-linkedom, consumer-workarounds",
14
+ "adversarial_fraction": "~29%"
15
+ },
16
+
17
+ "phrases": [
18
+ {
19
+ "id": "ssr-trigger-01",
20
+ "phrase": "why does text-ui render empty when we server-render it with Astro",
21
+ "should_route_to_ssr": true,
22
+ "expected_shape": "failure-shapes",
23
+ "rationale": "Canonical shape-2 symptom (projected-text content loss) — the worked example in SKILL.md."
24
+ },
25
+ {
26
+ "id": "ssr-trigger-02",
27
+ "phrase": "our SSR pass crashes on attachInternals not being a function",
28
+ "should_route_to_ssr": true,
29
+ "expected_shape": "guard-patterns",
30
+ "rationale": "Directly names the highest-leverage shape-1 crash site the pack documents."
31
+ },
32
+ {
33
+ "id": "ssr-trigger-03",
34
+ "phrase": "does select-ui work under linkedom",
35
+ "should_route_to_ssr": true,
36
+ "expected_shape": "failure-shapes",
37
+ "rationale": "The pack's core orienting question — 'does X work under SSR'."
38
+ },
39
+ {
40
+ "id": "ssr-trigger-04",
41
+ "phrase": "admin-sidebar snaps collapsed on every server-rendered page load",
42
+ "should_route_to_ssr": true,
43
+ "expected_shape": "failure-shapes",
44
+ "rationale": "Shape-3 symptom (connect-time measurement treated as confirmed) — the gh#286 precedent."
45
+ },
46
+ {
47
+ "id": "ssr-trigger-05",
48
+ "phrase": "is gh#284 fixed yet, can we drop the attribute-only SSR registration restriction",
49
+ "should_route_to_ssr": true,
50
+ "expected_shape": "status-ledger",
51
+ "rationale": "Direct status-ledger ask, names the issue and the exact consumer workaround."
52
+ },
53
+ {
54
+ "id": "ssr-trigger-06",
55
+ "phrase": "how do we test that a new ResizeObserver guard actually works without installing linkedom",
56
+ "should_route_to_ssr": true,
57
+ "expected_shape": "test-without-linkedom",
58
+ "rationale": "Canonical test-methodology ask this pack's most concrete axis exists for."
59
+ },
60
+ {
61
+ "id": "ssr-trigger-07",
62
+ "phrase": "new ResizeObserver() throws under our DOM-shim SSR environment",
63
+ "should_route_to_ssr": true,
64
+ "expected_shape": "guard-patterns",
65
+ "rationale": "Shape-1 crash, observer-construction variant — guard-patterns §1 shape (a)/(b)/(c)."
66
+ },
67
+ {
68
+ "id": "ssr-trigger-08",
69
+ "phrase": "table-ui and chart-ui always render empty in our server-rendered HTML even though the data is set",
70
+ "should_route_to_ssr": true,
71
+ "expected_shape": "failure-shapes",
72
+ "rationale": "Shape-4 (property-only components, gh#288), explicitly named as blocked on shape 2."
73
+ },
74
+ {
75
+ "id": "ssr-trigger-09",
76
+ "phrase": "what's the actual root cause of the SSR content-loss bug, container CEs losing their nested children",
77
+ "should_route_to_ssr": true,
78
+ "expected_shape": "failure-shapes",
79
+ "rationale": "Shape-2 container-CE symptom, the other half of the worked example's shape."
80
+ },
81
+ {
82
+ "id": "ssr-trigger-10",
83
+ "phrase": "can our app finally delete the 110-line browser-API shim now that 0.8.5 is out",
84
+ "should_route_to_ssr": true,
85
+ "expected_shape": "consumer-workarounds",
86
+ "rationale": "Direct ask about a named consumer workaround's deletability."
87
+ },
88
+ {
89
+ "id": "ssr-trigger-11",
90
+ "phrase": "is it safe to call getBoundingClientRect() synchronously in connectedCallback",
91
+ "should_route_to_ssr": true,
92
+ "expected_shape": "guard-patterns",
93
+ "rationale": "Shape-3 general-principle ask ('unknown vs confirmed'), independent of any specific component."
94
+ },
95
+ {
96
+ "id": "ssr-trigger-12",
97
+ "phrase": "document.adoptedStyleSheets is undefined and our component library crashes on import",
98
+ "should_route_to_ssr": true,
99
+ "expected_shape": "guard-patterns",
100
+ "rationale": "Shape-1 crash, the adoptStyles() call site specifically."
101
+ },
102
+ {
103
+ "id": "ssr-adversarial-01",
104
+ "phrase": "add hydration-aware stamping so connectedCallback stops overwriting existing children",
105
+ "should_route_to_ssr": false,
106
+ "expected_alternative": "adia-author",
107
+ "rationale": "This is IMPLEMENTING the gh#284 fix, not asking whether/why something fails — routes to the builder, not this answers-only pack."
108
+ },
109
+ {
110
+ "id": "ssr-adversarial-02",
111
+ "phrase": "set up Astro's rendering mode and hydration boundaries for our admin app",
112
+ "should_route_to_ssr": false,
113
+ "expected_alternative": "adia-host",
114
+ "rationale": "Consumer-side host/hydration wiring — explicitly fenced to adia-host in the boundaries section."
115
+ },
116
+ {
117
+ "id": "ssr-adversarial-03",
118
+ "phrase": "explain how custom element hydration works in general, independent of any specific framework",
119
+ "should_route_to_ssr": false,
120
+ "expected_alternative": "general knowledge, no pack",
121
+ "rationale": "Generic web-components/SSR theory not grounded in THIS repo's own code/issues — explicitly out of scope per the boundaries section."
122
+ },
123
+ {
124
+ "id": "ssr-adversarial-04",
125
+ "phrase": "the icon registry comes back empty because of a workspace-hoisting import.meta.glob path bug",
126
+ "should_route_to_ssr": false,
127
+ "expected_alternative": "adia-author (gh#287, a bundler/dep-optimizer bug, not one of the three SSR lifecycle shapes)",
128
+ "rationale": "gh#287 is explicitly named in status-ledger.md as unrelated to this pack's failure taxonomy — a near-miss adversarial by design."
129
+ },
130
+ {
131
+ "id": "ssr-adversarial-05",
132
+ "phrase": "wire up the a2ui generation pipeline to run inside a server-rendered page",
133
+ "should_route_to_ssr": false,
134
+ "expected_alternative": "adia-a2ui",
135
+ "rationale": "A2UI runtime/pipeline concern, not the UIElement lifecycle this pack is scoped to — explicitly fenced in the boundaries section."
136
+ }
137
+ ]
138
+ }
@@ -0,0 +1,63 @@
1
+ # The consumer's current workarounds — what they look like, why they're fragile
2
+
3
+ Two real consumers — adiav2's `admin-portal-fe` and `factory-dashboard` — SSR AdiaUI
4
+ via Astro 5 + `custom-elements-ssr` (linkedom). Each open issue names a workaround
5
+ they currently maintain in their OWN app code (not in this framework). Knowing the
6
+ shape of each matters for two reasons: (1) it tells you what "done" looks like — the
7
+ workaround should become deletable once the underlying gh issue closes; (2) it's the
8
+ concrete evidence for WHY each open issue matters, beyond the abstract description.
9
+
10
+ ## The ~110-line browser-API shim (gh#285 — now deletable, unconfirmed)
11
+
12
+ Before every component module import, the consumer's SSR entry point ran a
13
+ prototype-patching shim: stubbed `attachInternals`, defined four no-op Observer
14
+ classes (`ResizeObserver`/`IntersectionObserver`/`MutationObserver`/`PerformanceObserver`),
15
+ and force-assigned `document.adoptedStyleSheets = []`. Two named fragilities:
16
+ - **Fragile import order** — the shim MUST execute before any component module
17
+ loads, or the "deep-render pass" crashes anyway. This already broke once in a
18
+ production bundle (per the issue) — a bundler/tree-shaking change that reordered
19
+ imports silently reintroduced the crash.
20
+ - **A global patch, not scoped** — patching `HTMLElement.prototype` /
21
+ `globalThis.ResizeObserver` affects EVERY component and every other library in the
22
+ same process, not just AdiaUI's.
23
+
24
+ **Status as of gh#285's fix (PR #292, merged 2026-07-17): this shim should no longer
25
+ be necessary** — the framework guards its own call sites now. This pack's own routing
26
+ corpus / the issue thread is where confirmation from the consumer would land; don't
27
+ assume it's been removed without checking the issue's comment thread for that
28
+ confirmation, since a consumer removing a workaround is THEIR change, not something
29
+ this fix does automatically.
30
+
31
+ ## Attribute-only SSR registration restriction (gh#284 — likely no longer necessary, unconfirmed)
32
+
33
+ The consumer restricts server-side custom-element registration to components whose
34
+ visible content derives ENTIRELY from attributes — `button-ui text="…"`, `icon-ui
35
+ name="…"`. Every container (`admin-shell`, `admin-sidebar`, `nav-ui`) and every
36
+ projected-text component (`text-ui`, `avatar-ui`, `badge-ui`, and by extension
37
+ anything using `<component>slotted text</component>` markup) must register
38
+ CLIENT-side only, upgrading after first paint.
39
+
40
+ **Status as of gh#284's narrowing (2026-07-17): this restriction may no longer be
41
+ needed** — every component it names (and every other shipped component with real
42
+ content) currently derives its visible content from properties/attributes only,
43
+ never from light-DOM children, so `stamp()`'s destructive replace never fires
44
+ against real content for any of them (see [`failure-shapes.md`](failure-shapes.md)
45
+ §2 for the empirical survey). This did NOT ship as a `stamp()` fix — it's a
46
+ narrowing based on the CURRENT shape of the component set, backed by a forward
47
+ static audit (`scripts/dev/audit-template-child-conflict.mjs`) that would catch a
48
+ future regression. As with gh#285's shim: don't assume the consumer has actually
49
+ relaxed this restriction without checking the issue thread for their confirmation
50
+ — a consumer removing their own workaround is their change, not something this
51
+ narrowing does automatically. If the consumer's linkedom/`custom-elements-ssr`
52
+ setup surfaces something this repo's own repro didn't (a real second-connect
53
+ timing difference, an app-level component not covered by this framework), that's
54
+ new information worth feeding back into the issue, not something to assume away.
55
+
56
+ ## No workaround exists yet for gh#288 (property-only components)
57
+
58
+ The consumer's current behavior for `table-ui`/`chart-ui`/`select-ui` with
59
+ programmatic content is simply: render empty in the SSR response, then a per-page
60
+ wiring script does `customElements.whenDefined(...) → getElementById → assign
61
+ properties` after hydration. This is boilerplate the consumer repeats per page, not a
62
+ shim with a specific fragility of its own beyond "structurally excluded from SSR" —
63
+ see [`failure-shapes.md`](failure-shapes.md) §4.
@@ -0,0 +1,122 @@
1
+ # SSR failure shapes — symptom → root cause → status
2
+
3
+ Three distinct root-cause classes have surfaced from real SSR consumers (adiav2's
4
+ `admin-portal-fe` and `factory-dashboard`, server-rendering AdiaUI via Astro 5 +
5
+ `custom-elements-ssr`, which runs on linkedom — a DOM shim with no layout engine and
6
+ missing many browser APIs). A new SSR bug report almost always maps onto one of
7
+ these three; misclassifying it (e.g. treating a measurement-timing bug as a missing-API
8
+ bug) sends the fix to the wrong place. Check symptom against this table first.
9
+
10
+ ## 1 · Browser-only API called unconditionally → crash
11
+
12
+ **Symptom:** the SSR pass throws — often at construction, before any component-specific
13
+ code runs. Stack trace points at `attachInternals`, `new ResizeObserver(...)`, `new
14
+ IntersectionObserver(...)`, `new MutationObserver(...)`, `new PerformanceObserver(...)`,
15
+ or a write to `document.adoptedStyleSheets`.
16
+
17
+ **Root cause:** the call site assumes the API exists. linkedom implements none of these —
18
+ not a partial/quirky implementation, an absence. Any unconditional call throws
19
+ `TypeError` or `ReferenceError` (undefined global) immediately.
20
+
21
+ **Status: FIXED** (gh#285, PR #292, merged 2026-07-17). `UIElement`'s constructor
22
+ (`packages/web-components/core/element.js`) and 24 component/trait/module files had
23
+ this shape. The fix pattern (feature-detect + fallback matched to how the reference is
24
+ used downstream) is [`guard-patterns.md`](guard-patterns.md) — apply that pattern to
25
+ any NEW file that construct-calls one of these APIs; don't re-derive the shape from
26
+ scratch.
27
+
28
+ ## 2 · `connectedCallback` destructively re-stamps existing DOM → content loss
29
+
30
+ **Symptom (as originally reported):** the component renders in the SSR HTML response
31
+ as an EMPTY or STRUCTURALLY WRONG tag — not a crash, a silent loss. Two named
32
+ variants:
33
+ - A **container** (`admin-shell`, `admin-sidebar`, `nav-ui`) loses its nested custom-element
34
+ children — `<nav-item-ui>` inside `<nav-ui>` simply isn't in the response.
35
+ - A **projected-text** leaf (`<text-ui>Adia Admin</text-ui>`, `<avatar-ui>A</avatar-ui>`,
36
+ `<badge-ui>warning</badge-ui>`) goes out as an empty tag — the authored text is gone.
37
+
38
+ **Root cause (still real):** `UIElement.connectedCallback` (`packages/web-components/core/element.js`)
39
+ runs `stamp(result, this)` (the `stamp` function in `packages/web-components/core/template.js`,
40
+ currently at line 194) with the OWN template's output — this OVERWRITES the element's
41
+ existing children rather than adopting/patching them. In a real browser this is
42
+ correct and invisible: nothing exists inside the element yet at first connect. Under
43
+ SSR, `connectedCallback` runs a SECOND TIME against the server-parsed DOM (which
44
+ already has real, meaningful children from the HTML response) — the stamp silently
45
+ replaces them.
46
+
47
+ **Status: NARROWED, downgraded 2026-07-17** (gh#284). Verified against 0.8.4 — the
48
+ mechanism is real (a synthetic repro, a component with a non-null template AND
49
+ projected text, does lose it), but **every named example in the issue currently
50
+ has `static template = () => null`**: `admin-shell`, `admin-sidebar`, `nav-ui`,
51
+ `text-ui`, `badge-ui`, `avatar-ui` all skip `stamp()` entirely (`if (result)
52
+ stamp(result, this)` — `null` never enters the branch). A framework-wide survey
53
+ (150 components) found every component with a NON-null template derives its
54
+ visible content from properties/attributes only (`check-ui`'s `label=`,
55
+ `switch-ui`'s `label=`/`hint=`, `skip-nav`'s `text=`) — never from light-DOM
56
+ children — so the destructive replace, where it does fire, only ever regenerates
57
+ identical, template-owned content. Zero shipped components are exposed to the
58
+ originally-reported symptom right now.
59
+
60
+ **What shipped instead of a `stamp()` fix:** the two candidate directions the issue
61
+ named (adopt-and-patch, or skip-on-marker) were explicitly rejected as
62
+ disproportionate — "changes the render lifecycle of every primitive in the
63
+ framework (127+), not a mechanical guard sweep" — for a risk with zero current
64
+ instances. Shipped a forward-looking STATIC AUDIT instead:
65
+ `scripts/dev/audit-template-child-conflict.mjs` — flags (critical) any future
66
+ component pairing a non-null `static template` with a yaml `slots.default` entry
67
+ (the container shape), and (advisory) a non-null template paired with a body-text
68
+ usage example (the projected-text shape, no mechanical yaml signal to check). Wired
69
+ into `adia-author`'s structural-gate sequence. This is an operator ruling (not a
70
+ unilateral call) — see gh#284's comment thread for the full reasoning and the
71
+ empirical survey it's based on.
72
+
73
+ **Current consumer workaround still exists but may be over-conservative:**
74
+ `adiav2`'s SSR component registration is restricted to attribute-only-content
75
+ components (`button-ui text="…"`, `icon-ui name="…"`) — per this narrowing, EVERY
76
+ currently-shipped component (container or leaf) with real content already
77
+ qualifies as attribute/property-driven, so the restriction may no longer be
78
+ necessary for 0.8.4+. Don't assume this without the consumer confirming it via the
79
+ issue thread — see [`consumer-workarounds.md`](consumer-workarounds.md).
80
+
81
+ ## 3 · A connect-time layout MEASUREMENT is meaningless before real layout exists
82
+
83
+ **Symptom:** a component makes a decision (a boolean state, a mode, a snapped value)
84
+ by synchronously reading `getBoundingClientRect()` (or similar) inside `connected()`,
85
+ and that decision comes out WRONG — not crashed, not empty, just incorrect — in any
86
+ environment where real layout hasn't happened yet. This includes linkedom (no layout
87
+ engine at all — always returns a 0×0 rect) but ALSO a real browser connecting an
88
+ element before its first layout pass (inside a `display:none` ancestor, for instance) —
89
+ SSR is the environment that surfaces it reliably, but the bug is not SSR-specific.
90
+
91
+ **Root cause:** treating "the rect read 0" as equivalent to "the rect really is 0" —
92
+ they are NOT the same fact. A zero rect from a shim/pre-layout read means "unknown,"
93
+ and a decision derived from "unknown" as if it were "confirmed small" is a category
94
+ error, not a rendering gap.
95
+
96
+ **Status: FIXED for `admin-sidebar`'s specific instance** (gh#286, PR #290, merged
97
+ 2026-07-17) — see [`guard-patterns.md`](guard-patterns.md) §3 for the fix shape
98
+ (treat a zero read as unknown, defer to the component's own `ResizeObserver`'s first
99
+ real tick). **The general pattern is NOT swept framework-wide** — any OTHER component
100
+ that derives a persistent decision from a synchronous connect-time measurement
101
+ carries the same latent bug, undiscovered until someone hits it. If you're
102
+ investigating a "wrong initial state under SSR" report that ISN'T a missing-API crash
103
+ (shape 1) or a content-loss (shape 2), check whether the component reads a rect/size
104
+ synchronously at connect and treat that as the working hypothesis first.
105
+
106
+ ## 4 · Feature gap, not a bug — property-only components can't seed from SSR HTML
107
+
108
+ **Symptom:** not a bug report at all — a `table-ui`/`chart-ui`/`select-ui` with
109
+ programmatic-only content (`.columns`, `.data`, `.options` set as JS properties)
110
+ renders correctly in the browser but is STRUCTURALLY ABSENT from the SSR HTML response,
111
+ because JS property assignments don't serialize into server-rendered markup. The
112
+ component pops in empty and fills in after a post-hydration wiring script runs.
113
+
114
+ **Root cause:** no declarative (HTML-serializable) form of the data these components
115
+ need exists yet — property-only content is a deliberate API shape for CONSUMERS with a
116
+ live client, not a gap in any individual component's code.
117
+
118
+ **Status: OPEN, blocked on shape 2** (gh#288). A JSON-script-child or attribute form
119
+ is proposed but explicitly depends on #284's resolution — reading a declarative payload
120
+ at connect time is pointless if the very next thing `connectedCallback` does is
121
+ overwrite whatever DOM it finds (including a `<script type="application/json">` child)
122
+ with the stamp. Don't attempt #288 in isolation.
@@ -0,0 +1,142 @@
1
+ # Guard patterns — the established fix shape per failure shape
2
+
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.
5
+
6
+ ## §1 · A browser-only API constructor/method call
7
+
8
+ **Shape:** `typeof <Global> !== 'undefined'` (for globals: `ResizeObserver`,
9
+ `IntersectionObserver`, `MutationObserver`, `PerformanceObserver`) or `typeof
10
+ this.<method> === 'function'` (for instance methods: `attachInternals`) or `'<prop>'
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
13
+ no one boilerplate to paste everywhere.
14
+
15
+ **Three concrete shapes, all live in the repo:**
16
+
17
+ **(a) Field assigned, used later with `?.` on teardown** — the majority case. Guard the
18
+ construction; every later reference to the field must already (or now needs to) use
19
+ optional chaining, since the field can legitimately stay unset:
20
+ ```js
21
+ // packages/web-modules/shell/admin-sidebar/admin-sidebar.js — #setupChildResizeObserver
22
+ #setupChildResizeObserver() {
23
+ this.#childRO = new ResizeObserver((entries) => { ... });
24
+ this.#childRO.observe(this);
25
+ }
26
+ // guarded:
27
+ if (typeof ResizeObserver !== 'undefined') {
28
+ this.#childRO = new ResizeObserver((entries) => { ... });
29
+ this.#childRO.observe(this);
30
+ }
31
+ // disconnectedCallback: this.#childRO?.disconnect(); ← already needs the `?.`
32
+ ```
33
+
34
+ **(b) Early-return no-op** — when the entire trait/method's purpose IS the observer;
35
+ nothing else in the function is worth running without it:
36
+ ```js
37
+ // packages/web-components/traits/resize-observer/resize-observer.js
38
+ setup({ host }) {
39
+ if (typeof ResizeObserver === 'undefined') return () => {};
40
+ // ... the rest of the trait, only reached when the API exists
41
+ }
42
+ ```
43
+
44
+ **(c) Module-level `const`, ternary at declaration** — when the observer is a
45
+ singleton constructed at import time, not per-instance:
46
+ ```js
47
+ // packages/web-components/core/data-stream.js
48
+ const observer = typeof MutationObserver !== 'undefined'
49
+ ? new MutationObserver((mutations) => { ... })
50
+ : null;
51
+ // every later use: observer?.observe(...) / bootstrap() no-ops via the same check
52
+ ```
53
+
54
+ **The `this.internals` special case — a shim, not `undefined`.** `UIElement`'s
55
+ constructor (`packages/web-components/core/element.js`) needed a DIFFERENT answer
56
+ than "guard and leave unset," because ~12 files across the framework call
57
+ `this.internals.setValidity(...)` / `.setFormValue(...)` unconditionally (form
58
+ participation is a routine, expected thing for `UIFormElement` subclasses to do).
59
+ Leaving `this.internals` as `undefined` when `attachInternals` doesn't exist would
60
+ just relocate the crash one level deeper, into every one of those 12 consumers' first
61
+ validation call. The actual fix is a frozen no-op object matching the EXACT surface
62
+ those consumers call (grepped, not guessed: `setFormValue`, `setValidity`,
63
+ `checkValidity`, `reportValidity`, `form`, `labels`, `validity`, `validationMessage`,
64
+ `willValidate`):
65
+ ```js
66
+ const NOOP_INTERNALS = Object.freeze({
67
+ setFormValue() {}, setValidity() {},
68
+ checkValidity() { return true; }, reportValidity() { return true; },
69
+ get form() { return null; }, get labels() { return []; },
70
+ get validity() { return {}; }, get validationMessage() { return ''; },
71
+ get willValidate() { return false; },
72
+ });
73
+ // constructor:
74
+ this.internals = typeof this.attachInternals === 'function'
75
+ ? this.attachInternals()
76
+ : NOOP_INTERNALS;
77
+ ```
78
+ **When a NEW browser-only API needs the same treatment:** ask "do consumers assume the
79
+ returned object/value is always truthy and call methods on it unconditionally?" If
80
+ yes, a no-op shim matching the ACTUAL consumed surface (grep for it, don't guess) is
81
+ the right answer, not leaving the field `undefined`. If the API is purely
82
+ fire-and-forget infrastructure with no return value anyone depends on (most
83
+ Observers), a plain construction guard (shapes a/b/c above) is sufficient — no shim
84
+ needed.
85
+
86
+ ## §2 · Destructive `stamp()` on connect (gh#284 — narrowed to a static audit, not a stamp() patch)
87
+
88
+ There is no `stamp()`/`connectedCallback` fix, and per the 2026-07-17 narrowing
89
+ there isn't meant to be one — the mechanism is real but zero shipped components
90
+ are exposed (see [`failure-shapes.md`](failure-shapes.md) §2 for the full survey).
91
+ The shipped guard is `scripts/dev/audit-template-child-conflict.mjs`: it flags a
92
+ NEW component that pairs a non-null `static template` with a yaml `slots.default`
93
+ entry (critical) or a body-text usage example (advisory) — i.e. it prevents the
94
+ conflict shape from being reintroduced, rather than patching the render lifecycle
95
+ every one of the 150 components goes through. **A component author who hits this
96
+ audit's finding fixes it by making the template `() => null`** (the pattern every
97
+ current children-accepting component already uses — compose via CSS + `render()`'s
98
+ own surgical DOM manipulation, matching `avatar-ui`'s `#imgEl`/`#initialsEl`
99
+ pattern) — NOT by inventing a component-local stamp-skip flag or a marker
100
+ attribute; either of those drifts from the established convention and isn't
101
+ covered by this audit's "safe" classification. If a genuinely new component NEEDS
102
+ both a non-null template AND real consumer children (unlike anything shipped
103
+ today), route through `adia-author` before authoring it — that's a real design
104
+ question, not a mechanical fix.
105
+
106
+ ## §3 · A connect-time measurement is "unknown," not "confirmed"
107
+
108
+ **Shape:** treat a zero (or otherwise clearly-bogus) synchronous read as "no signal
109
+ yet" and DEFER the decision to whatever async correction mechanism the component
110
+ already has — don't add a new one if an existing `ResizeObserver`/similar already
111
+ watches the same dimension.
112
+ ```js
113
+ // packages/web-modules/shell/admin-sidebar/admin-sidebar.js — #syncCollapsedFromWidth
114
+ #syncCollapsedFromWidth() {
115
+ const w = this.getBoundingClientRect().width;
116
+ if (w === 0) return; // ← unknown, not "confirmed collapsed" —
117
+ this.collapsed = w <= SNAP_THRESHOLD; // leaves whatever connected() already
118
+ } // restored (persisted state, or default)
119
+
120
+ // the component's EXISTING ResizeObserver callback (already watching this.host for
121
+ // an unrelated reason — flipping select-ui placement) gains ONE more line, so its
122
+ // first REAL tick (post-hydration, or after the display:none ancestor is revealed)
123
+ // resolves what connect-time couldn't:
124
+ #setupChildResizeObserver() {
125
+ this.#childRO = new ResizeObserver((entries) => {
126
+ for (const entry of entries) {
127
+ const w = entry.contentBoxSize[0].inlineSize;
128
+ if (w > 0) this.collapsed = w <= SNAP_THRESHOLD; // ← the deferred correction
129
+ // ...existing unrelated logic continues unchanged...
130
+ }
131
+ });
132
+ }
133
+ ```
134
+ The key move: reuse the observer that's ALREADY there rather than adding a
135
+ purpose-built one — a second observer watching the same element for two unrelated
136
+ reasons is a maintenance smell, and per §1 above, a NEW observer needs its own
137
+ construction guard anyway.
138
+
139
+ ## Verify targets for a new guard
140
+
141
+ See [`test-without-linkedom.md`](test-without-linkedom.md) for how to prove a new
142
+ guard actually works — deleting the real API in a test, not mocking it.
@@ -0,0 +1,35 @@
1
+ # SSR support — status ledger
2
+
3
+ **This file drifts fast — re-verify against the live issue tracker
4
+ (`gh issue view <n>`) before trusting it for anything beyond a quick orientation.**
5
+ Every row was true as of 2026-07-17; a closed issue below may have re-opened, a PR
6
+ number may have been superseded by a follow-up fix, etc. This ledger is a map, not
7
+ the territory — the territory is `gh issue list` / `gh issue view`.
8
+
9
+ | Issue | Failure shape | Status | Shipped in | Notes |
10
+ |---|---|---|---|---|
11
+ | [#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 |
12
+ | [#286](https://github.com/adiahealth/gen-ui-kit/issues/286) | §3 — connect-time measurement treated as confirmed | **CLOSED** (for `admin-sidebar` specifically) | PR #290 (merged 2026-07-17) | The GENERAL pattern (any other component reading a rect/size synchronously at connect) is NOT swept — only this one instance is fixed |
13
+ | [#284](https://github.com/adiahealth/gen-ui-kit/issues/284) | §2 — destructive `stamp()` on connect | **NARROWED, downgraded 2026-07-17** | `scripts/dev/audit-template-child-conflict.mjs` | Verified against 0.8.4: EVERY component that accepts light-DOM children (nav-ui, admin-shell, admin-sidebar, text-ui, badge-ui, avatar-ui — every example the issue names) has `static template = () => null`, so `stamp()` never runs for them; every component with a non-null template derives content from properties only. The mechanism is still real (a synthetic repro confirms it), but zero shipped components are exposed. Fixing `stamp()`/`connectedCallback` itself was rejected as disproportionate (touches every primitive's render lifecycle for a currently-zero-instance risk) in favor of a static audit gate that catches a FUTURE component reintroducing the conflict shape. Operator ruling, not a unilateral call — see gh#284's comment thread. |
14
+ | [#288](https://github.com/adiahealth/gen-ui-kit/issues/288) | §4 — property-only components can't seed from SSR HTML | **OPEN, blocked on #284's ORIGINAL scope** | — | #284's narrowing doesn't resolve this — #288 is about JS-property-only content (table-ui `.columns`, select-ui `.options`), a different gap from destructive stamping. Still needs its own declarative-data-channel design; don't attempt in isolation. |
15
+ | [#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. |
16
+
17
+ ## What "done" looks like for the open items
18
+
19
+ - **#284's original scope is closed as a non-issue against 0.8.4** — see the ledger
20
+ row above. Re-open only if a FUTURE component's yaml/class.js trips
21
+ `audit-template-child-conflict.mjs`, or if `adiav2`'s production workaround
22
+ (`consumer-workarounds.md`) turns out to be catching something this repo's
23
+ synthetic repro didn't (confirm via the issue thread before assuming so).
24
+ - **#288 done** = a declarative data channel exists. It no longer strictly depends
25
+ on #284 shipping a `stamp()`-level fix (there isn't one), but a real design still
26
+ needs to answer "what happens if a future property-only component's data arrives
27
+ after a template that already stamped" — don't hand-wave that gap away just
28
+ because #284 narrowed.
29
+
30
+ ## Re-syncing this ledger
31
+
32
+ When any row's status changes, update the table AND re-check whether
33
+ [`consumer-workarounds.md`](consumer-workarounds.md)'s claims about what's "now
34
+ deletable" are still accurate — a workaround section that says "should no longer be
35
+ necessary" for a shape that's since re-opened is a false manifest, not a stale detail.