@adia-ai/adia-ui-forge 0.1.2

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 (136) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +54 -0
  4. package/bin/forge-lint +263 -0
  5. package/bin/lib/audit-axes.mjs +555 -0
  6. package/bin/lib/dry-run-irreversible.mjs +236 -0
  7. package/bin/lib/run-skill-evals.mjs +487 -0
  8. package/bin/lib/teach-router.mjs +250 -0
  9. package/commands/adia-forge-a2ui.md +10 -0
  10. package/commands/adia-forge-author.md +10 -0
  11. package/commands/adia-forge-dogfood.md +8 -0
  12. package/commands/adia-forge-llm.md +8 -0
  13. package/commands/adia-forge-orient.md +10 -0
  14. package/commands/adia-forge-release.md +8 -0
  15. package/commands/adia-forge-review.md +10 -0
  16. package/hooks/hooks.json +15 -0
  17. package/package.json +41 -0
  18. package/references/shared/content-trust.md +76 -0
  19. package/references/shared/pev-rationale.md +64 -0
  20. package/references/shared/skill-conventions.md +133 -0
  21. package/skills/adia-ui-a2ui/CHANGELOG.md +32 -0
  22. package/skills/adia-ui-a2ui/SKILL.md +243 -0
  23. package/skills/adia-ui-a2ui/evals/adversarial-corpus.json +75 -0
  24. package/skills/adia-ui-a2ui/evals/routing-corpus.json +31 -0
  25. package/skills/adia-ui-a2ui/evals/teach-routing-cases.json +100 -0
  26. package/skills/adia-ui-a2ui/references/anti-patterns.md +24 -0
  27. package/skills/adia-ui-a2ui/references/chunk-authoring.md +88 -0
  28. package/skills/adia-ui-a2ui/references/corpus-discipline.md +56 -0
  29. package/skills/adia-ui-a2ui/references/eval-diagnostics.md +127 -0
  30. package/skills/adia-ui-a2ui/references/fragment-graph.md +91 -0
  31. package/skills/adia-ui-a2ui/references/mcp-pipeline-ops.md +106 -0
  32. package/skills/adia-ui-a2ui/references/mcp-tool-reference.md +398 -0
  33. package/skills/adia-ui-a2ui/references/pipeline-overview.md +175 -0
  34. package/skills/adia-ui-a2ui/references/semantic-fail-lifting.md +120 -0
  35. package/skills/adia-ui-a2ui/references/strategy-engines.md +111 -0
  36. package/skills/adia-ui-a2ui/references/teach-protocol.md +220 -0
  37. package/skills/adia-ui-a2ui/references/zettel-calibration.md +93 -0
  38. package/skills/adia-ui-a2ui/scripts/audit-a2ui-roster.mjs +96 -0
  39. package/skills/adia-ui-a2ui/scripts/teach-route.mjs +157 -0
  40. package/skills/adia-ui-a2ui/skill.json +38 -0
  41. package/skills/adia-ui-authoring/CHANGELOG.md +32 -0
  42. package/skills/adia-ui-authoring/SKILL.md +256 -0
  43. package/skills/adia-ui-authoring/assets/case-studies/admin-shell-decomposition.md +101 -0
  44. package/skills/adia-ui-authoring/assets/case-studies/maxtokens-32768-discovery.md +109 -0
  45. package/skills/adia-ui-authoring/assets/case-studies/theme-panel-promotion.md +113 -0
  46. package/skills/adia-ui-authoring/evals/adversarial-design-plan-gates.json +138 -0
  47. package/skills/adia-ui-authoring/evals/routing-corpus.json +245 -0
  48. package/skills/adia-ui-authoring/references/anti-patterns.md +606 -0
  49. package/skills/adia-ui-authoring/references/api-contract.md +205 -0
  50. package/skills/adia-ui-authoring/references/authoring-cycle.md +211 -0
  51. package/skills/adia-ui-authoring/references/canonical-pattern-index.md +179 -0
  52. package/skills/adia-ui-authoring/references/code-style.md +329 -0
  53. package/skills/adia-ui-authoring/references/common-gotchas.md +93 -0
  54. package/skills/adia-ui-authoring/references/composite-demo-protocol.md +1084 -0
  55. package/skills/adia-ui-authoring/references/css-patterns.md +364 -0
  56. package/skills/adia-ui-authoring/references/lifecycle-patterns.md +302 -0
  57. package/skills/adia-ui-authoring/references/llm-bridge.md +254 -0
  58. package/skills/adia-ui-authoring/references/module-promotion.md +289 -0
  59. package/skills/adia-ui-authoring/references/primitive-audit.md +123 -0
  60. package/skills/adia-ui-authoring/references/shell-patterns.md +561 -0
  61. package/skills/adia-ui-authoring/references/teach-protocol.md +428 -0
  62. package/skills/adia-ui-authoring/references/token-contract.md +120 -0
  63. package/skills/adia-ui-authoring/references/worked-example.md +351 -0
  64. package/skills/adia-ui-authoring/references/yaml-contract.md +224 -0
  65. package/skills/adia-ui-authoring/scripts/audit-authoring-roster.mjs +148 -0
  66. package/skills/adia-ui-authoring/scripts/build-canonical-pattern-index.mjs +199 -0
  67. package/skills/adia-ui-authoring/skill.json +45 -0
  68. package/skills/adia-ui-dogfood/CHANGELOG.md +17 -0
  69. package/skills/adia-ui-dogfood/README.md +62 -0
  70. package/skills/adia-ui-dogfood/SKILL.md +866 -0
  71. package/skills/adia-ui-dogfood/scripts/analyze.mjs +603 -0
  72. package/skills/adia-ui-dogfood/skill.json +40 -0
  73. package/skills/adia-ui-forge/SKILL.md +88 -0
  74. package/skills/adia-ui-forge/evals/routing-corpus.json +30 -0
  75. package/skills/adia-ui-gen-review/CHANGELOG.md +108 -0
  76. package/skills/adia-ui-gen-review/SKILL.md +266 -0
  77. package/skills/adia-ui-gen-review/references/loop-protocol.md +712 -0
  78. package/skills/adia-ui-gen-review/references/rubric-cosmetic.md +144 -0
  79. package/skills/adia-ui-gen-review/references/rubric-decompose.md +117 -0
  80. package/skills/adia-ui-gen-review/references/rubric-score.md +249 -0
  81. package/skills/adia-ui-gen-review/references/scores.schema.json +125 -0
  82. package/skills/adia-ui-gen-review/references/teach-protocol.md +214 -0
  83. package/skills/adia-ui-gen-review/scripts/gen-review-coverage-audit.mjs +127 -0
  84. package/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs +569 -0
  85. package/skills/adia-ui-gen-review/scripts/gen-review-status.mjs +176 -0
  86. package/skills/adia-ui-gen-review/scripts/validate-cycle-scores.mjs +198 -0
  87. package/skills/adia-ui-gen-review/skill.json +22 -0
  88. package/skills/adia-ui-llm/CHANGELOG.md +25 -0
  89. package/skills/adia-ui-llm/SKILL.md +165 -0
  90. package/skills/adia-ui-llm/evals/adversarial-corpus.json +75 -0
  91. package/skills/adia-ui-llm/evals/routing-corpus.json +30 -0
  92. package/skills/adia-ui-llm/evals/teach-routing-cases.json +73 -0
  93. package/skills/adia-ui-llm/references/adapter-contract.md +99 -0
  94. package/skills/adia-ui-llm/references/add-a-provider.md +90 -0
  95. package/skills/adia-ui-llm/references/bridge-facade.md +94 -0
  96. package/skills/adia-ui-llm/references/browser-proxy-boundary.md +89 -0
  97. package/skills/adia-ui-llm/references/model-registry.md +75 -0
  98. package/skills/adia-ui-llm/references/streaming-sse.md +95 -0
  99. package/skills/adia-ui-llm/references/teach-protocol.md +78 -0
  100. package/skills/adia-ui-llm/scripts/audit-llm-roster.mjs +93 -0
  101. package/skills/adia-ui-llm/scripts/teach-route.mjs +119 -0
  102. package/skills/adia-ui-llm/skill.json +33 -0
  103. package/skills/adia-ui-release/CHANGELOG.md +23 -0
  104. package/skills/adia-ui-release/SKILL.md +295 -0
  105. package/skills/adia-ui-release/assets/case-studies/2026-05-20-batch-push-v0.6.14-v0.6.15.md +144 -0
  106. package/skills/adia-ui-release/assets/case-studies/2026-05-20-corpus-drift-remediation-v0.6.15.md +155 -0
  107. package/skills/adia-ui-release/assets/case-studies/2026-05-20-version-skip-correction-v0.6.12.md +114 -0
  108. package/skills/adia-ui-release/assets/case-studies/2026-05-21-author-from-scratch-v0.6.18.md +139 -0
  109. package/skills/adia-ui-release/assets/case-studies/2026-05-21-feedback37-retraction-v0.6.21.md +124 -0
  110. package/skills/adia-ui-release/assets/case-studies/2026-05-21-fn1-enrichment-pass-v0.6.19.md +125 -0
  111. package/skills/adia-ui-release/assets/case-studies/2026-05-21-stale-test-detection-v0.6.20.md +142 -0
  112. package/skills/adia-ui-release/assets/case-studies/2026-05-23-freshness-gate-recovery-v0.6.32.md +97 -0
  113. package/skills/adia-ui-release/assets/case-studies/2026-05-26-catalog-drift-recurring-v0.6.40.md +147 -0
  114. package/skills/adia-ui-release/assets/templates/stub-changelog.template.md +22 -0
  115. package/skills/adia-ui-release/evals/evals.json +164 -0
  116. package/skills/adia-ui-release/references/changelog-discipline.md +250 -0
  117. package/skills/adia-ui-release/references/cycle-happy-path.md +520 -0
  118. package/skills/adia-ui-release/references/exe-deploy.md +149 -0
  119. package/skills/adia-ui-release/references/gates-catalog.md +778 -0
  120. package/skills/adia-ui-release/references/ledger-discipline.md +232 -0
  121. package/skills/adia-ui-release/references/migration-guide-authoring.md +174 -0
  122. package/skills/adia-ui-release/references/multi-agent-baseline.md +207 -0
  123. package/skills/adia-ui-release/references/notes-authoring.md +212 -0
  124. package/skills/adia-ui-release/references/recovery-paths.md +215 -0
  125. package/skills/adia-ui-release/references/rollup-notes.md +208 -0
  126. package/skills/adia-ui-release/references/teach-protocol.md +468 -0
  127. package/skills/adia-ui-release/scripts/assert-monorepo-root.mjs +49 -0
  128. package/skills/adia-ui-release/scripts/audit-gate-roster.mjs +196 -0
  129. package/skills/adia-ui-release/scripts/bump.mjs +118 -0
  130. package/skills/adia-ui-release/scripts/dispatch-publish.mjs +151 -0
  131. package/skills/adia-ui-release/scripts/insert-stub.mjs +127 -0
  132. package/skills/adia-ui-release/scripts/make-ledger.mjs +179 -0
  133. package/skills/adia-ui-release/scripts/promote-unreleased.mjs +88 -0
  134. package/skills/adia-ui-release/scripts/release-pack.mjs +350 -0
  135. package/skills/adia-ui-release/scripts/tag-lockstep.mjs +110 -0
  136. package/skills/adia-ui-release/skill.json +75 -0
@@ -0,0 +1,606 @@
1
+ # Anti-Pattern Catalogue
2
+
3
+ Every entry here is a real bug caught in the framework's 5-iteration coherence audit. They're named and cited so future authors recognize the _shape_ of the mistake before repeating it.
4
+
5
+ Organized by axis.
6
+
7
+ ---
8
+
9
+ ## API / Attributes
10
+
11
+ ### AP-01 · `default: true` on a Boolean prop
12
+
13
+ ```javascript
14
+ // WRONG
15
+ closable: { type: Boolean, default: true, reflect: true }
16
+ ```
17
+
18
+ **What went wrong:** the component did something by default, and the prop named the default behavior. Users couldn't opt out by just setting the attribute — they had to pass `closable="false"` which doesn't even work for Boolean attributes (presence = true; absent = false is the HTML model).
19
+
20
+ **Fix:** flip the name. Default `true` behavior = name the prop for the opt-out.
21
+
22
+ | Before | After |
23
+ | --- | --- |
24
+ | `modal-ui[closable]` (default `true`) | `modal-ui[permanent]` (default `false`) |
25
+ | `skeleton-ui[animate]` | `skeleton-ui[static]` |
26
+ | `stream-ui[cursor]` | `stream-ui[hideCursor]` |
27
+ | `chart-ui[average]` | `chart-ui[hideAverage]` |
28
+ | `toggle-group-ui[multiple]` | `toggle-group-ui[single]` |
29
+ | `swiper-ui[pause-on-hover]` | `swiper-ui[noPauseOnHover]` |
30
+
31
+ **Recognition:** if you find yourself writing `default: true` in a `static properties` block, stop — the name is wrong.
32
+
33
+ ### AP-02 · Magic-value sentinel instead of `null`
34
+
35
+ ```javascript
36
+ // WRONG
37
+ value: { type: Number, default: -1, reflect: true } // -1 means "indeterminate"
38
+ ```
39
+
40
+ **What went wrong:** `-1` is a magic number. Consumers branch on `this.value !== -1`, which breaks the day someone assigns `-1` to mean something else, or decides `-0.5` is also indeterminate.
41
+
42
+ **Fix:** `null` for indeterminate. Check `value == null`.
43
+
44
+ ```javascript
45
+ value: { type: Number, default: null, reflect: true }
46
+ ```
47
+
48
+ **Real fix:** `progress-ui.value` carried `-1` for months; fixed with a back-compat coercion (`if (v === -1) v = null`) to avoid breaking existing consumers.
49
+
50
+ ### AP-03 · `attr:` silent typo
51
+
52
+ ```javascript
53
+ // WRONG — silently ignored
54
+ allowHalf: { type: Boolean, default: false, reflect: true, attr: 'allow-half' }
55
+ ```
56
+
57
+ **What went wrong:** the mapper looks for `attribute:`, not `attr:`. The typo is silently dropped. The kebab-case HTML attribute _happens_ to auto- convert to `allow-half` so the bug hides — until someone renames `allowHalf` and the attribute never wires.
58
+
59
+ **Fix:** `attribute:` always.
60
+
61
+ **Real fix:** `rating-ui` had this typo for several commits. Found by grep in axis-1 audit.
62
+
63
+ ### AP-04 · `title` as a prop name
64
+
65
+ ```html
66
+ <cot-ui title="Analyzing intent"></cot-ui>
67
+ ```
68
+
69
+ **What went wrong:** HTML's global `title` attribute is the browser-native tooltip. A custom element with a `title` attribute gets a tooltip on hover by default, which is almost never intended.
70
+
71
+ **Fix:** rename to `heading` (or something domain-specific that doesn't collide).
72
+
73
+ ### AP-05 · `active` on a parent component
74
+
75
+ ```html
76
+ <timeline-ui active="3"> <!-- parent taking an index -->
77
+ <timeline-item-ui active> <!-- child per-item state -->
78
+ ```
79
+
80
+ **What went wrong:** `active` on the parent carries a non-boolean payload (an index or a selection). The name `active` implies a Boolean.
81
+
82
+ **Fix:** rename by intent:
83
+
84
+ - Parent holds a current index → `step` (e.g. `timeline-ui[step="3"]`)
85
+ - Parent holds a selection → `value` (e.g. `inspector-ui[value="item-42"]`)
86
+ - Child Boolean state stays as `active` — that's fine.
87
+
88
+ ### AP-06 · `error` as a variant
89
+
90
+ ```html
91
+ <tag-ui variant="error">Failed</tag-ui>
92
+ ```
93
+
94
+ **What went wrong:** `error` is the validation-state family (`[error]` on inputs; `--a-error-*` tokens). A variant named `error` conflates visual variant with validation state.
95
+
96
+ **Fix:** use the semantic-family name for the variant: `danger` (matches `--a-danger-*` tokens). Reserve `error` for validation state.
97
+
98
+ ### AP-07 · `disabled` on a non-form component
99
+
100
+ ```html
101
+ <noodles-ui disabled>...</noodles-ui> <!-- diagram, not form-participating -->
102
+ ```
103
+
104
+ **What went wrong:** `disabled` has specific form semantics (removed from tab order, blocks submission). On a non-form component, the implicit meaning is "read-only interaction."
105
+
106
+ **Fix:** `readonly` — matches the actual intent.
107
+
108
+ ### AP-08 · `multiple` with exclusion semantics
109
+
110
+ ```html
111
+ <toggle-group-ui multiple> <!-- default: multi-select -->
112
+ ```
113
+
114
+ **What went wrong:** implies multi-select is the default (matches HTML's `<select multiple>`). But then the prop violates the Boolean-default-false rule (AP-01).
115
+
116
+ **Fix:** negate to `single` so absent-attribute = multi-select default = Boolean false.
117
+
118
+ ```html
119
+ <toggle-group-ui> <!-- default: multi-select -->
120
+ <toggle-group-ui single> <!-- opt-in to single-select -->
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Tokens
126
+
127
+ ### AP-T1 · Variant body consumes L2 directly
128
+
129
+ ```css
130
+ /* WRONG — button.css */
131
+ :scope[variant="danger"]:not([disabled]):hover {
132
+ --button-fg: var(--a-danger); /* L2 */
133
+ --button-border: var(--a-danger); /* L2 */
134
+ }
135
+ ```
136
+
137
+ **What went wrong:** the variant bypassed the L3 state matrix. When a user enables high-contrast mode, the contrast-mode overrides on `--a-danger-fg-hover` don't reach this component — it's reading the flat L2 token instead of the stateful L3 one.
138
+
139
+ **Fix:** alias from the L3 matrix:
140
+
141
+ ```css
142
+ :scope[variant="danger"]:not([disabled]):hover {
143
+ --button-fg: var(--a-danger-fg-hover);
144
+ --button-border: var(--a-danger-border-hover);
145
+ }
146
+ ```
147
+
148
+ Or, if the component already defines state-aware tokens in `:where(:scope)`, reuse those:
149
+
150
+ ```css
151
+ :scope[variant="danger"]:not([disabled]):hover {
152
+ --button-fg: var(--button-fg-hover);
153
+ --button-border: var(--button-border-hover);
154
+ }
155
+ ```
156
+
157
+ ### AP-T2 · Raw hex / rgb / oklch in component CSS
158
+
159
+ ```css
160
+ /* WRONG */
161
+ :scope {
162
+ background: #f4f4f5;
163
+ color: oklch(0.2 0.01 264);
164
+ }
165
+ ```
166
+
167
+ **What went wrong:** theme/scheme/contrast switching can't reach a hardcoded color. The component breaks silently in dark mode.
168
+
169
+ **Fix:** consume tokens. Always.
170
+
171
+ ### AP-T3 · Raw px ≥ 3 without justification
172
+
173
+ ```css
174
+ /* WRONG — 12px is neither a stroke nor intrinsic */
175
+ :scope { padding: 12px; }
176
+ ```
177
+
178
+ **Fix:** use the spacing scale.
179
+
180
+ ```css
181
+ :scope { padding: var(--a-space-3); }
182
+ ```
183
+
184
+ **Allowed carve-out** (with comment):
185
+
186
+ ```css
187
+ :where(:scope) {
188
+ /* Component-intrinsic visual constant; no --a-space-* equivalent */
189
+ --noodles-port-size: 10px;
190
+ }
191
+ ```
192
+
193
+ ### AP-T4 · Component token name mismatches scope
194
+
195
+ ```css
196
+ /* WRONG — button.css */
197
+ @scope (button-ui) {
198
+ :where(:scope) {
199
+ --btn-bg: var(--a-accent-bg); /* ← stem mismatch */
200
+ }
201
+ }
202
+ ```
203
+
204
+ **Fix:** stem matches the scope's tag-stem exactly.
205
+
206
+ ```css
207
+ @scope (button-ui) {
208
+ :where(:scope) {
209
+ --button-bg: var(--a-accent-bg);
210
+ }
211
+ }
212
+ ```
213
+
214
+ ---
215
+
216
+ ## CSS Patterns
217
+
218
+ ### AP-S1 · Variant body sets layout properties
219
+
220
+ ```css
221
+ /* WRONG — pagination.css (pre-fix) */
222
+ :scope[variant="button"] [slot="nav"] button {
223
+ width: var(--pagination-button-size);
224
+ height: var(--pagination-button-size);
225
+ padding: 0;
226
+ border-radius: var(--pagination-radius);
227
+ }
228
+ ```
229
+
230
+ **What went wrong:** `variant="button"` changes the layout (square buttons with specific dimensions) — that's a mode, not a variant.
231
+
232
+ **Fix:** add `pagination-ui[variant="button"]` to the Sanctioned Mode Attributes table in `docs/specs/component-token-contract.md`. Document the layout change as intentional.
233
+
234
+ Alternative: refactor to make the attribute cosmetic — if possible.
235
+
236
+ ### AP-S2 · Single-block `:scope` (tokens and styles interleaved)
237
+
238
+ ```css
239
+ /* WRONG — missing the :where(:scope) token block */
240
+ @scope (button-ui) {
241
+ :scope {
242
+ --button-bg: var(--a-accent-bg);
243
+ background: var(--button-bg);
244
+ display: inline-flex;
245
+ }
246
+ }
247
+ ```
248
+
249
+ **What went wrong:** tokens inherit `:scope`'s `(0,1,0)` specificity. Parent overrides fail because they can't out-specify.
250
+
251
+ **Fix:** two blocks.
252
+
253
+ ### AP-S3 · `::slotted()` in light-DOM component
254
+
255
+ ```css
256
+ /* WRONG — shadow DOM syntax */
257
+ ::slotted([slot="icon"]) { margin-inline-end: var(--gap); }
258
+ ```
259
+
260
+ **Fix:** attribute selector on child.
261
+
262
+ ```css
263
+ :scope > [slot="icon"] { margin-inline-end: var(--component-gap); }
264
+ ```
265
+
266
+ ### AP-S4 · `!important`
267
+
268
+ Ever. If you need it, specificity is wrong somewhere. Fix the layering.
269
+
270
+ ### AP-S5 · Inline `grid-template-columns` for asymmetric splits
271
+
272
+ ```html
273
+ <!-- WRONG — escape hatch that bypasses the grid-ui vocabulary -->
274
+ <grid-ui gap="4" style="grid-template-columns: 2fr 1fr">
275
+ <card-ui>Chart</card-ui>
276
+ <card-ui>Sidebar</card-ui>
277
+ </grid-ui>
278
+ ```
279
+
280
+ **What went wrong:** the inline style works visually but opts out of the component's attribute API. It doesn't reflect to `[style]`-based selectors, doesn't compose with theme overrides, and reads as a local hack when agents scan the page for grid conventions. Every inline style is a precedent the next author cargo-cults.
281
+
282
+ **Fix:** use `grid-ui[columns="N"]` plus `[span="M"]` on the children. The component ships column variants 1-6, auto-fill, auto-fit, and a per-child `span="2|3|4|5|6|full"` attribute. Compose a 2:1 split as `columns="3"` + the wide child gets `span="2"`; compose a 3:2 split as `columns="5"` + `span="3"` + `span="2"`.
283
+
284
+ ```html
285
+ <!-- RIGHT — 2:1 via columns=3 and a span=2 child -->
286
+ <grid-ui columns="3" gap="4">
287
+ <card-ui span="2">Chart</card-ui>
288
+ <card-ui>Sidebar</card-ui>
289
+ </grid-ui>
290
+
291
+ <!-- RIGHT — 3:2 via columns=5 and span=3 + span=2 -->
292
+ <grid-ui columns="5" gap="4">
293
+ <card-ui span="3">Overview</card-ui>
294
+ <card-ui span="2">Recent Sales</card-ui>
295
+ </grid-ui>
296
+ ```
297
+
298
+ **Recognition:** `style="grid-template-columns: …"` anywhere in a page is a signal that either (a) the author doesn't know the `span` attribute exists, or (b) the ratio truly can't be expressed in columns 1-6. The second case is rare — most asymmetric splits approximate cleanly to `3:1`, `2:1`, `3:2`, or `4:1`. If the ratio genuinely can't be expressed, it probably should become a separate layout primitive with a named attribute rather than live as an inline style.
299
+
300
+ **Real fix:** admin-dashboard had `style="grid-template-columns: 7fr 5fr"` and `"2fr 1fr"`, analytics-dashboard had one `"2fr 1fr"`. Replaced with `columns="5" span="3"+"2"` and `columns="3" span="2"+default` respectively; the 7:5 ratio shifted to 3:2 (60:40 vs 58:42) — imperceptible visually, a win structurally.
301
+
302
+ ### AP-S6 · `:scope >` child combinator on a conditionally-rendered part
303
+
304
+ ```css
305
+ /* WRONG — [data-part="empty"] is rendered behind a `${isEmpty ? … : null}` branch */
306
+ :scope > [data-part="empty"] { display: grid; place-items: center; }
307
+ ```
308
+
309
+ **What went wrong:** the template engine wraps every conditional render branch — `${cond ? … : null}` (also `?` / `.map()`) — in a `<span style="display:contents">`. That span is a real DOM child, so the conditionally-rendered element is a _grandchild_ of `:scope`, not a direct child. `display:contents` removes the span from layout but NOT from selector matching, so `:scope > [data-part="empty"]` matches nothing and the rule silently no-ops — no error, passes `components --verify`, looks fine in happy-dom. Only a live render shows the un-styled block.
310
+
311
+ **Fix:** use a **descendant** combinator for any conditionally-rendered part; keep `:scope >` only for parts that render unconditionally (those stay direct children).
312
+
313
+ ```css
314
+ /* RIGHT — descendant combinator survives the display:contents wrapper */
315
+ :scope [data-part="empty"] { display: grid; place-items: center; }
316
+ :scope > [data-part="header"] { … } /* static part — child combinator OK */
317
+ ```
318
+
319
+ **Recognition:** a `[data-part]` rule that "doesn't apply" though the markup looks right, on a part rendered inside a `${cond ? … : null}` / `?` / `.map()` branch. Inspect the live DOM — a `<span style="display:contents">` wrapping the part confirms it. **Recurring class:** integrations-page empty-state, onboarding-checklist complete-CTA, and earlier sightings (bug-51, bug-53). Positive-guidance version: [css-patterns.md](css-patterns.md) §"Conditional-render parts defeat `:scope >`".
320
+
321
+ **Real fix:** integrations-page.css + onboarding-checklist.css had `:scope > [data-part]` on conditionally-rendered parts (empty-state / complete CTA); the layout grid and the separating margin silently dropped. Converted those rules to descendant combinators.
322
+
323
+ ### AP-S7 · `align-self: stretch` to vertically center a fixed-height flex child
324
+
325
+ ```css
326
+ /* WRONG — stretch can't size a child that carries a definite height */
327
+ [slot="field"] > [slot="trailing"] { align-self: stretch; } /* a 20px <kbd-ui> pins to the TOP */
328
+ ```
329
+
330
+ **What went wrong:** a flex item with `align-self: stretch` **and** a definite cross-axis size (`height` / `block-size`) does not stretch — the explicit size wins and the item positions at **flex-start** (top), not center. A `<kbd-ui>` (`height: 1.25rem`) in a 30px chrome sat ~4px high. `stretch` only sizes auto-height items; for a fixed-height child it silently degrades to top-alignment. A sibling `<button-ui>` looked fine only because it had no blocking height and genuinely filled the chrome (center vs. stretch identical for it).
331
+
332
+ **Fix:** to vertically center a row of mixed-height affordances, use `align-self: center` and let each child keep its own token height.
333
+
334
+ ```css
335
+ /* RIGHT — center keeps each child's intrinsic height on the vertical center */
336
+ [slot="field"] > [slot="trailing"] { align-self: center; }
337
+ ```
338
+
339
+ **Recognition:** a fixed-height inline affordance (kbd / icon / badge) hugging the top of a taller flex container while a full-height sibling looks correct — suspect `align-self: stretch` on a mixed-height row.
340
+
341
+ **Real fix:** input-ui's leading/trailing affordance slots used `align-self: stretch`; the ⌘K `<kbd-ui>` hint sat 4px high. `center` fixed it and let `<button-ui>` children honor their own `--button-height` token (bug-60).
342
+
343
+ ---
344
+
345
+ ## JS Lifecycle
346
+
347
+ ### AP-L1 · Inline arrow passed to `addEventListener`
348
+
349
+ ```javascript
350
+ // WRONG — cot.js (pre-fix)
351
+ this.#summaryEl.addEventListener('click', () => {
352
+ this.collapsed = !this.collapsed;
353
+ });
354
+ ```
355
+
356
+ **What went wrong:** `removeEventListener(type, anInlineArrow)` can't match — new function instance each time. The listener stays bound. If the component is re-attached, a SECOND listener binds too. Accumulates.
357
+
358
+ **Fix:** stable `#field` arrow.
359
+
360
+ ```javascript
361
+ #onSummaryClick = () => {
362
+ this.collapsed = !this.collapsed;
363
+ };
364
+
365
+ connected() {
366
+ this.#summaryEl.addEventListener('click', this.#onSummaryClick);
367
+ }
368
+
369
+ disconnected() {
370
+ this.#summaryEl?.removeEventListener('click', this.#onSummaryClick);
371
+ this.#summaryEl = null;
372
+ }
373
+ ```
374
+
375
+ ### AP-L2 · Duplicate `disconnected()` method
376
+
377
+ ```javascript
378
+ // WRONG — chart.js (pre-fix)
379
+ class AdiaChart extends AdiaElement {
380
+ connected() { /* ... */ this.#ro = new ResizeObserver(...); }
381
+
382
+ disconnected() {
383
+ this.#ro?.disconnect(); // ← this one gets SHADOWED
384
+ }
385
+
386
+ // ... 100 lines later ...
387
+
388
+ disconnected() {
389
+ // second declaration silently wins; ResizeObserver never disconnected
390
+ this.#tooltipEl?.remove();
391
+ }
392
+ }
393
+ ```
394
+
395
+ **What went wrong:** JavaScript silently takes the last method definition. The first `disconnected()` (with the observer cleanup) is discarded. No error, no warning.
396
+
397
+ **Fix:** merge into one method. Always.
398
+
399
+ ```javascript
400
+ disconnected() {
401
+ this.#ro?.disconnect();
402
+ this.#ro = null;
403
+ this.#tooltipEl?.remove();
404
+ this.#tooltipEl = null;
405
+ }
406
+ ```
407
+
408
+ ### AP-L3 · Missing `super.connected()` / `super.disconnected()`
409
+
410
+ ```javascript
411
+ // WRONG — AdiaFormElement subclass missing super call
412
+ class AdiaInput extends AdiaFormElement {
413
+ connected() {
414
+ // super.connected() NOT called
415
+ this.#inputEl = this.querySelector('input');
416
+ }
417
+ }
418
+ ```
419
+
420
+ **What went wrong:** `ElementInternals` registration skipped. The component doesn't participate in forms — no value submission, no validation, no form-reset handling. Silent.
421
+
422
+ **Fix:** `super.connected()` first line of the method. `super.disconnected()` first line of its method too.
423
+
424
+ ### AP-L4 · Listener added to global `document` without cleanup
425
+
426
+ ```javascript
427
+ // WRONG
428
+ connected() {
429
+ document.addEventListener('click', this.#onDocumentClick);
430
+ // disconnected() doesn't removeEventListener
431
+ }
432
+ ```
433
+
434
+ **What went wrong:** the listener survives the component. Every time the component mounts, another listener is added. Over an SPA's lifetime, dozens accumulate. Each fires on every document click.
435
+
436
+ **Fix:** mirror the addListener with a removeListener in `disconnected()`.
437
+
438
+ ### AP-L5 · Timer not cleared
439
+
440
+ ```javascript
441
+ // WRONG
442
+ connected() {
443
+ this.#timer = setInterval(() => this.tick(), 1000);
444
+ }
445
+ // disconnected() doesn't clearInterval
446
+ ```
447
+
448
+ **What went wrong:** timer keeps firing on a removed component. Can cause null-dereferences (DOM refs are nulled), phantom renders, or memory leaks.
449
+
450
+ **Fix:** `clearInterval(this.#timer)` in `disconnected()`.
451
+
452
+ ### AP-L6 · Observer recreated without disconnecting old one
453
+
454
+ ```javascript
455
+ // WRONG
456
+ connected() {
457
+ this.#ro = new ResizeObserver(...);
458
+ this.#ro.observe(this);
459
+ }
460
+
461
+ // Later — new options, recreated
462
+ updateConfig(opts) {
463
+ this.#ro = new ResizeObserver(...); // ← old one leaks
464
+ this.#ro.observe(this);
465
+ }
466
+ ```
467
+
468
+ **Fix:** disconnect first, then recreate.
469
+
470
+ ```javascript
471
+ updateConfig(opts) {
472
+ this.#ro?.disconnect();
473
+ this.#ro = new ResizeObserver(...);
474
+ this.#ro.observe(this);
475
+ }
476
+ ```
477
+
478
+ ### AP-L7 · Empty template wipes authored light-DOM children
479
+
480
+ ```javascript
481
+ // WRONG — drawer.js (pre-fix)
482
+ class AdiaDrawer extends AdiaElement {
483
+ static template = () => html``; // ← looks harmless. It isn't.
484
+
485
+ connected() {
486
+ this.addEventListener('press', this.#onPress);
487
+ // ... intent: migrate authored [slot="header|body|footer"] into the panel
488
+ }
489
+ }
490
+ ```
491
+
492
+ **What went wrong:** `AdiaElement`'s base `connectedCallback` runs an effect that calls `stamp(template(this), this)`. `stamp` calls `mount`, which calls `container.replaceChildren(fragment)`. Even an "empty" `html\`\``produces a non-null result, so`replaceChildren`runs and clears every authored child (header/section/footer) out of the host element. The component's own`render()` then tries to migrate children that no longer exist.
493
+
494
+ Symptom: drawers render with a close button in an empty panel; all authored content is gone. Hard to debug because the children WERE in the DOM at `connected()` time — they get wiped milliseconds later when the effect runs.
495
+
496
+ **Fix:** drop the override entirely. The base class already defines `static template = () => null;`, and `stamp()` is skipped when the result is null:
497
+
498
+ ```javascript
499
+ // RIGHT — no template override
500
+ class AdiaDrawer extends AdiaElement {
501
+ // base AdiaElement.template returns null → stamp() is skipped →
502
+ // authored light-DOM children survive through render().
503
+
504
+ connected() { /* ... */ }
505
+ render() { /* migrate children into the panel */ }
506
+ }
507
+ ```
508
+
509
+ **Recognition:** light-DOM components that compose authored children (card-ui, drawer-ui, list-item-ui, menu-ui, anything with slots) must NOT declare any template — not even an empty one. If the component needs to stamp structural parts (dialog, panel, scrim), use `static parts = {...}` and `this.ensure('name')` instead; parts append once and don't wipe siblings.
510
+
511
+ ### AP-L8 · `setAttribute('value', …)` on `input-ui` after first render
512
+
513
+ ```javascript
514
+ // WRONG — expected to update the visible text
515
+ input.setAttribute('value', 'Liam Johnson');
516
+ // …user still sees an empty field
517
+ ```
518
+
519
+ **What went wrong:** `input-ui.connected()` copies `this.value` to `#textEl.textContent` exactly once. `render()` does NOT re-sync when the `value` attribute changes externally — this is intentional, to prevent external re-renders from clobbering in-progress user input. But it means declarative value updates after mount have no visible effect.
520
+
521
+ **Fix:** set the attribute AND mirror the text slot when you need to update the displayed value from outside:
522
+
523
+ ```javascript
524
+ input.setAttribute('value', 'Liam Johnson');
525
+ const textSlot = input.querySelector('[slot="text"]');
526
+ if (textSlot) textSlot.textContent = 'Liam Johnson';
527
+ ```
528
+
529
+ **Real fix:** drawer-populating setup code (admin-dashboard transaction drawer, report drawer) hit this when filling input-ui fields from a clicked row. Worked around with an explicit text-slot mirror; a proper fix would be for `input-ui.render()` to re-sync `#textEl.textContent` when the `value` property changes while the field isn't focused.
530
+
531
+ **Recognition:** if you're setting an `input-ui` / `textarea-ui` `value` from JavaScript and the field looks empty, you've hit this. `<stat-ui>` and `<select-ui>` do sync on attribute change — the gap is only in the contenteditable-backed text field.
532
+
533
+ ### AP-L9 · Radio cards without a `role="radiogroup"` parent
534
+
535
+ ```html
536
+ <!-- WRONG — radios never cross-deselect -->
537
+ <list-ui>
538
+ <list-item-ui>
539
+ <card-ui>
540
+ <header>
541
+ <radio-ui slot="icon" name="plan" value="pro" checked></radio-ui>
542
+ <span slot="heading">Pro</span>
543
+ </header>
544
+ </card-ui>
545
+ </list-item-ui>
546
+ <!-- …more radio-ui name="plan"… -->
547
+ </list-ui>
548
+ ```
549
+
550
+ **What went wrong:** `radio-ui.#select` does a sibling lookup via `this.closest('fieldset, [role="radiogroup"]') || this.parentElement`. When each radio sits inside its own `<span slot="icon">`, the "parent" contains only that one radio — no siblings to deselect. Clicking one doesn't uncheck the others, and the initial `checked` state may or may not render visually depending on mount order.
551
+
552
+ **Fix:** wrap the whole group in an element `role="radiogroup"`:
553
+
554
+ ```html
555
+ <div role="radiogroup" aria-label="Plan" data-plan-picker
556
+ onclick="const r = event.target.closest('card-ui')?.querySelector('radio-ui'); r && r.click()">
557
+ <list-ui divider>
558
+ <list-item-ui>
559
+ <card-ui>
560
+ <header>
561
+ <radio-ui slot="icon" name="plan" value="pro" checked></radio-ui>
562
+ <span slot="heading">Pro · $49/mo</span>
563
+ <span slot="description">…</span>
564
+ </header>
565
+ </card-ui>
566
+ </list-item-ui>
567
+ <!-- additional options -->
568
+ </list-ui>
569
+ </div>
570
+ ```
571
+
572
+ The inline delegate on the group lets users click anywhere on a card to toggle its radio; the radio's own click handler no-ops on a card-click when it's already checked.
573
+
574
+ **Recognition:** any time `radio-ui`s with the same `name` are separated by container elements (cards, list-items, grid cells), they need a `role="radiogroup"` ancestor or they won't behave as a group.
575
+
576
+ ### AP-L10 · Popover / tooltip not removed from `document.body`
577
+
578
+ ```javascript
579
+ // WRONG
580
+ showTooltip() {
581
+ this.#tip = document.createElement('div');
582
+ document.body.appendChild(this.#tip);
583
+ // disconnected() doesn't remove #tip
584
+ }
585
+ ```
586
+
587
+ **What went wrong:** tip stays in `<body>` after the component is destroyed. Visible zombie element.
588
+
589
+ **Fix:**
590
+
591
+ ```javascript
592
+ disconnected() {
593
+ this.#tip?.remove();
594
+ this.#tip = null;
595
+ }
596
+ ```
597
+
598
+ ---
599
+
600
+ ## Meta-pattern: the "just this once" trap
601
+
602
+ All of these bugs started as "just this once" exceptions — a component author thought "I'll fix it later," or "this component is special," or "the linter isn't catching it so it's fine." Each "just this once" became a template for the next author.
603
+
604
+ The audit found 100 findings in iteration 1. Fifteen were novel; eighty-five were different authors cargo-culting the same five shapes.
605
+
606
+ **Convention is an exponential.** Breaking it once multiplies; enforcing it once multiplies in the other direction. Treat every authoring decision as setting a precedent, because it is.