@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,603 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dogfooding analyzer — walks every `/site/components/*` demo page in a
4
+ * headless browser and runs visual-correctness probes that type-checks
5
+ * and tests don't catch.
6
+ *
7
+ * Probes:
8
+ * 1. Zero-area — `*-ui` elements that render at 0×0 (e.g. parent
9
+ * `display:none`, children moved into an overflow
10
+ * popover, contents collapsed).
11
+ * 2. Transparent — swatches/badges/indicators with known
12
+ * "should-be-colored" semantics whose computed
13
+ * background is `rgba(0,0,0,0)` (chart-legend bug
14
+ * class — a fallback token that doesn't resolve).
15
+ * 3. Empty-control — form controls (`input-ui`, `search-ui`, etc.)
16
+ * whose `connected()` should have stamped an
17
+ * `<input>` or `[contenteditable]` but didn't.
18
+ * 4. Synonym-attr — known synonym-attribute / synonym-slot drift
19
+ * classes documented in
20
+ * `docs/conventions/attribute-api-migration.md`.
21
+ * 5. Console — errors + deprecation warnings during page
22
+ * load + 800ms settling.
23
+ *
24
+ * Output:
25
+ * docs/reports/dogfooding-YYYY-MM-DD.md (or path from --out)
26
+ * Severity levels: critical (page broken), warning (likely-broken),
27
+ * info (drift, deprecation, non-blocking).
28
+ *
29
+ * Usage (run from the framework monorepo checkout):
30
+ * node "$SCRIPT" --filter chart-legend # one page
31
+ * node "$SCRIPT" --port 5173 # custom port
32
+ * node "$SCRIPT" --out /tmp/report.md # custom path
33
+ * node "$SCRIPT" --quiet # report only
34
+ * where SCRIPT is this file's path
35
+ * (`${CLAUDE_PLUGIN_ROOT}/skills/adia-ui-dogfood/scripts/analyze.mjs`).
36
+ *
37
+ * Repo root: the monorepo whose `site/sitemap.json` is swept. Resolved from
38
+ * `$ADIA_REPO_ROOT` if set, else the current working directory — so the
39
+ * script targets the user's checkout, not the plugin install location.
40
+ *
41
+ * Assumes the dev server is already running. Spin one up first:
42
+ * npm run dev # in a separate terminal
43
+ */
44
+
45
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
46
+ import { join, dirname } from 'node:path';
47
+ import { chromium } from 'playwright';
48
+
49
+ // The framework monorepo whose demo pages are swept. The script ships inside
50
+ // the plugin but operates on the user's checkout, so the repo root is the
51
+ // invocation cwd (or $ADIA_REPO_ROOT) — never a path relative to this file.
52
+ const REPO_ROOT = process.env.ADIA_REPO_ROOT || process.cwd();
53
+
54
+ // ── Args ──────────────────────────────────────────────────────────────────
55
+
56
+ const args = process.argv.slice(2);
57
+ const argVal = (flag) => {
58
+ const i = args.indexOf(flag);
59
+ return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : null;
60
+ };
61
+ const FILTER = argVal('--filter');
62
+ const PORT = parseInt(argVal('--port') || '5173', 10);
63
+ const QUIET = args.includes('--quiet');
64
+ const today = new Date().toISOString().slice(0, 10);
65
+ const OUT = argVal('--out') || join(REPO_ROOT, 'docs/reports', `dogfooding-${today}.md`);
66
+
67
+ const log = (...m) => { if (!QUIET) console.error(...m); };
68
+
69
+ // ── Component contracts (what each component MUST stamp at render) ────────
70
+ //
71
+ // Narrow on purpose: most AdiaUI components are *the* interactive surface
72
+ // (role="button" + tabindex + own listeners), not host-of-native-control.
73
+ // Listing them here as "must have a <button> child" produces 300+ false
74
+ // positives. The genuinely-stamping components are input-ui (creates an
75
+ // `<input>` or `<span contenteditable>`) and search-ui (wraps input-ui).
76
+
77
+ const STAMP_CONTRACTS = {
78
+ 'input-ui': {
79
+ reason: 'connected() should stamp <input slot="text"> or <span slot="text" contenteditable>',
80
+ },
81
+ 'search-ui': {
82
+ reason: 'connected() should stamp <input-ui type="search"> internally',
83
+ },
84
+ };
85
+
86
+ // ── Selectors that should resolve to a non-transparent background ─────────
87
+ //
88
+ // `[data-swatch]`, semantic-variant pills/buttons, and chart indicators.
89
+ // rgba(0,0,0,0) on these = bug (the chart-legend class).
90
+
91
+ const COLORED_SELECTORS = [
92
+ // chart-legend / chart-tooltip swatches
93
+ '[data-swatch]',
94
+ '[data-indicator]',
95
+ // semantic variants on tag-ui / button-ui / alert-ui
96
+ 'tag-ui[variant=primary]',
97
+ 'tag-ui[variant=success]',
98
+ 'tag-ui[variant=warning]',
99
+ 'tag-ui[variant=danger]',
100
+ 'tag-ui[variant=info]',
101
+ 'button-ui[variant=primary]:not([disabled])',
102
+ 'button-ui[color=danger]:not([disabled])',
103
+ ];
104
+
105
+ // ── Synonym-attribute / synonym-slot drift markers ────────────────────────
106
+ //
107
+ // Mirrors `docs/conventions/attribute-api-migration.md`. These run via
108
+ // querySelectorAll; matching elements are flagged as info-level.
109
+
110
+ const DRIFT_MARKERS = [
111
+ { selector: 'avatar-ui[name]', message: '<avatar-ui name=…> is deprecated; use text=…' },
112
+ { selector: 'grid-ui[cols]', message: '<grid-ui cols=…> is the synonym; canonical is columns=…' },
113
+ { selector: 'stepper-ui[current]', message: '<stepper-ui current=…> is the synonym; canonical is step=…' },
114
+ { selector: 'stepper-item-ui[state]', message: '<stepper-item-ui state=…> is the synonym; parent <stepper-ui step> drives it' },
115
+ { selector: 'card-ui [slot="meta"]', message: 'card-ui has no slot="meta" rule — tag falls into bare grid cell and stretches' },
116
+ ];
117
+
118
+ // ── Probes (all run in-page via page.evaluate) ────────────────────────────
119
+
120
+ async function runProbes(page) {
121
+ return await page.evaluate(({ COLORED_SELECTORS, DRIFT_MARKERS }) => {
122
+ const findings = [];
123
+ const push = (severity, kind, detail) => findings.push({ severity, kind, ...detail });
124
+
125
+ // 1. Zero-area
126
+ // Walk artifact items; skip elements that are intentionally hidden
127
+ // (any display:none / visibility:hidden / [hidden] ancestor — e.g.
128
+ // avatar-group's overflow children, popover panels, drawer contents)
129
+ // or that are "rendered-on-demand" components which only paint when
130
+ // opened (drawers, modals, popovers, tooltips, toasts).
131
+ const ON_DEMAND_TAGS = new Set([
132
+ 'drawer-ui', 'modal-ui', 'popover-ui', 'tooltip-ui', 'toast-ui',
133
+ 'context-menu-ui', 'sheet-ui', 'dialog-ui',
134
+ ]);
135
+ const isIntentionallyHidden = (el) => {
136
+ let p = el;
137
+ while (p && p.nodeType === 1) {
138
+ if (p.hasAttribute?.('hidden')) return true;
139
+ if (p.hasAttribute?.('popover')) return true; // popover API closed
140
+ if (p.tagName === 'DIALOG' && !p.open) return true;
141
+ if (ON_DEMAND_TAGS.has(p.tagName?.toLowerCase())) return true;
142
+ const cs = getComputedStyle(p);
143
+ if (cs.display === 'none' || cs.visibility === 'hidden') return true;
144
+ // Stop walking at the artifact-item boundary.
145
+ if (p.matches?.('[data-artifact-item]')) return false;
146
+ p = p.parentElement;
147
+ }
148
+ return false;
149
+ };
150
+ document.querySelectorAll('[data-artifact-item] *').forEach((el) => {
151
+ if (!el.tagName.endsWith('-UI')) return;
152
+ if (ON_DEMAND_TAGS.has(el.tagName.toLowerCase())) return;
153
+ if (isIntentionallyHidden(el)) return;
154
+ const r = el.getBoundingClientRect();
155
+ // Only flag elements that should have rendered something — skip those
156
+ // with no children, no text, and no [icon]/[text] attrs.
157
+ const hasIntent =
158
+ el.children.length > 0 ||
159
+ (el.textContent || '').trim().length > 0 ||
160
+ el.hasAttribute('icon') ||
161
+ el.hasAttribute('text') ||
162
+ el.hasAttribute('label');
163
+ if (hasIntent && (r.width === 0 || r.height === 0)) {
164
+ push('critical', 'zero-area', {
165
+ tag: el.tagName.toLowerCase(),
166
+ label: el.closest('[data-artifact-label]')?.dataset.artifactLabel || null,
167
+ rect: { w: Math.round(r.width), h: Math.round(r.height) },
168
+ hint: el.parentElement?.tagName === 'TOOLBAR-UI'
169
+ ? 'parent toolbar-ui — likely overflow="menu" spilled it into the popover'
170
+ : null,
171
+ });
172
+ }
173
+ });
174
+
175
+ // 2. Transparent-where-colored
176
+ for (const selector of COLORED_SELECTORS) {
177
+ document.querySelectorAll(selector).forEach((el) => {
178
+ const r = el.getBoundingClientRect();
179
+ if (r.width === 0 || r.height === 0) return; // already flagged or hidden
180
+ const cs = getComputedStyle(el);
181
+ const bg = cs.backgroundColor;
182
+ const borderTop = cs.borderTopColor;
183
+ const isTransparent = bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent';
184
+ const borderTransparent = !borderTop || borderTop === 'rgba(0, 0, 0, 0)';
185
+ // [data-swatch] for shape="dashed" intentionally has transparent bg
186
+ // and paints via border-top instead. Test that directly: any
187
+ // colored top border with non-zero width is "by design".
188
+ const paintsViaBorder =
189
+ el.matches('[data-swatch]') &&
190
+ parseFloat(cs.borderTopWidth) > 0 &&
191
+ !borderTransparent;
192
+ if (paintsViaBorder) return;
193
+ if (isTransparent) {
194
+ push('critical', 'transparent-fill', {
195
+ selector,
196
+ tag: el.tagName.toLowerCase(),
197
+ label: el.closest('[data-artifact-label]')?.dataset.artifactLabel || null,
198
+ inlineSwatchVar: el.style.getPropertyValue('--swatch-color') || null,
199
+ });
200
+ }
201
+ });
202
+ }
203
+
204
+ // 3. Synonym-attr / synonym-slot drift
205
+ for (const { selector, message } of DRIFT_MARKERS) {
206
+ document.querySelectorAll(selector).forEach((el) => {
207
+ push('info', 'drift', {
208
+ selector,
209
+ message,
210
+ tag: el.tagName.toLowerCase(),
211
+ label: el.closest('[data-artifact-label]')?.dataset.artifactLabel || null,
212
+ });
213
+ });
214
+ }
215
+
216
+ // 4. Multi-child rich content inside alert-ui (no col-ui wrap)
217
+ document.querySelectorAll('alert-ui').forEach((alert) => {
218
+ const direct = Array.from(alert.children).filter(
219
+ (c) => !c.hasAttribute('slot'),
220
+ );
221
+ if (direct.length >= 2 && direct.every((c) => c.tagName === 'TEXT-UI')) {
222
+ push('warning', 'alert-flex-row', {
223
+ tag: 'alert-ui',
224
+ label: alert.closest('[data-artifact-label]')?.dataset.artifactLabel || null,
225
+ message: 'multiple bare <text-ui> inside alert-ui lay out as flex-row — wrap in <col-ui slot="content">',
226
+ });
227
+ }
228
+ });
229
+
230
+ // 5. Missing component CSS — for each unique *-ui tag rendered on
231
+ // the page, verify a stylesheet matching `/components/{prefix}/{prefix}.css`
232
+ // is loaded. Catches the "swap to <select-ui> but forget to <link>
233
+ // select.css" class — markup looks right, JS registers the element,
234
+ // every other probe passes, but the listbox renders unstyled.
235
+ //
236
+ // Demo pages under /site/components/<X>/ usually embed all sibling
237
+ // components transitively (their docs router shell loads tokens.css
238
+ // + a barrel, so most components resolve). When this probe fires
239
+ // on a demo page, it's signaling a real authoring miss — either
240
+ // a link tag was dropped or a new component on the page wasn't
241
+ // added to the shell loader.
242
+ // Components / infra elements that legitimately ship without a sibling
243
+ // {prefix}/{prefix}.css under packages/web-components/components/. Two
244
+ // groups:
245
+ //
246
+ // 1. CSS-only components (yaml + a2ui.json only, styled by parent slot
247
+ // rules — per `feedback_css_only_components`). Source of truth:
248
+ // `for d in components/*/; do n="${d%/}"; [ ! -f "$d$n.css" ] &&
249
+ // echo "$n"; done` enumerates them.
250
+ //
251
+ // 2. Infrastructure elements registered outside components/ (e.g.
252
+ // `router-ui` from core/provider.js). They have inline / shell
253
+ // stylesheets that don't match the path pattern.
254
+ //
255
+ // When this list grows: re-run the source-of-truth grep above + audit
256
+ // `customElements.define(...)` calls under packages/web-components/core/.
257
+ const COMPONENTS_WITHOUT_OWN_CSS = new Set([
258
+ // CSS-only components (no .css sibling) — styled by parent slot rules
259
+ 'aside-ui', 'footer-ui', 'header-ui', 'section-ui', 'stat-ui',
260
+ // Co-located siblings (CSS lives in the parent component's directory)
261
+ 'tab-ui', // css + js in components/tabs/, not components/tab/
262
+ // Module-tier composites with no own CSS (styled by shell/parent)
263
+ 'chat-thread-ui',
264
+ // Infrastructure elements (registered outside components/)
265
+ 'router-ui',
266
+ ]);
267
+ const allCustomTags = new Set();
268
+ document.querySelectorAll('*').forEach((el) => {
269
+ if (el.tagName.endsWith('-UI') && !ON_DEMAND_TAGS.has(el.tagName.toLowerCase())) {
270
+ allCustomTags.add(el.tagName.toLowerCase());
271
+ }
272
+ });
273
+ const loadedHrefs = Array.from(document.styleSheets)
274
+ .map((s) => { try { return s.href || ''; } catch { return ''; } })
275
+ .filter(Boolean);
276
+ for (const tag of allCustomTags) {
277
+ if (COMPONENTS_WITHOUT_OWN_CSS.has(tag)) continue;
278
+ const prefix = tag.replace(/-ui$/, '');
279
+ const expected = `/components/${prefix}/${prefix}.css`;
280
+ const linked = loadedHrefs.some((h) => h.includes(expected));
281
+ if (linked) continue;
282
+ // Heuristic fallback: if at least one conventional component-prefixed
283
+ // CSS custom property resolves on a rendered instance, the stylesheet
284
+ // probably loaded under a different URL pattern (bundled / aliased).
285
+ const inst = document.querySelector(tag);
286
+ let resolved = false;
287
+ if (inst) {
288
+ const cs = getComputedStyle(inst);
289
+ // Primary probe: standard token naming convention
290
+ const probeNames = [
291
+ `--${prefix}-bg`, `--${prefix}-fg`, `--${prefix}-pad`,
292
+ `--${prefix}-radius`, `--${prefix}-size`, `--${prefix}-gap`,
293
+ `--${prefix}-border`,
294
+ ];
295
+ resolved = probeNames.some((n) => cs.getPropertyValue(n).trim().length > 0);
296
+ // Broader fallback: scan all custom properties for any --{prefix}-* token.
297
+ // Catches components that use sub-namespaced tokens (e.g. --menu-popover-bg,
298
+ // --toggle-option-gap) rather than the flat --{prefix}-bg convention.
299
+ if (!resolved) {
300
+ const propPrefix = `--${prefix}-`;
301
+ for (const prop of cs) {
302
+ if (prop.startsWith(propPrefix) && cs.getPropertyValue(prop).trim().length > 0) {
303
+ resolved = true;
304
+ break;
305
+ }
306
+ }
307
+ }
308
+ }
309
+ if (!resolved) {
310
+ push('critical', 'missing-component-css', {
311
+ tag,
312
+ label: inst?.closest('[data-artifact-label]')?.dataset.artifactLabel || null,
313
+ message: `<${tag}> rendered but no stylesheet matching ${expected} loaded — controls render unstyled (popover bg transparent, listbox = raw text). Add the <link rel="stylesheet"> in the page or shell.`,
314
+ expectedHref: expected,
315
+ });
316
+ }
317
+ }
318
+
319
+ // 6. Unstyled popover — runtime check for [popover]:popover-open
320
+ // whose computed background is transparent + zero padding. This
321
+ // is the visual symptom of missing-component-css for a popover-
322
+ // bearing primitive (select-ui listbox, tooltip-ui, etc.). Catches
323
+ // the case from a different angle in case the URL heuristic above
324
+ // didn't match a particular bundling scheme.
325
+ // Popovers whose host element is intentionally transparent — visual
326
+ // background comes from child elements, not the host itself.
327
+ const TRANSPARENT_HOST_POPOVERS = new Set([
328
+ 'feed-ui', // toast feed: host is transparent; feed-item-ui children carry bg
329
+ ]);
330
+ document.querySelectorAll('[popover]').forEach((el) => {
331
+ if (!el.matches?.(':popover-open')) return;
332
+ if (TRANSPARENT_HOST_POPOVERS.has(el.tagName.toLowerCase())) return;
333
+ const cs = getComputedStyle(el);
334
+ const transparent = cs.backgroundColor === 'rgba(0, 0, 0, 0)' || cs.backgroundColor === 'transparent';
335
+ const noPad = parseFloat(cs.paddingTop) === 0 && parseFloat(cs.paddingLeft) === 0;
336
+ if (transparent && noPad) {
337
+ push('critical', 'unstyled-popover', {
338
+ tag: el.tagName.toLowerCase(),
339
+ host: el.parentElement?.tagName?.toLowerCase() || null,
340
+ label: el.closest('[data-artifact-label]')?.dataset.artifactLabel || null,
341
+ message: 'open popover has transparent bg + zero padding — host component CSS likely missing',
342
+ });
343
+ }
344
+ });
345
+
346
+ return findings;
347
+ }, { COLORED_SELECTORS, DRIFT_MARKERS });
348
+ }
349
+
350
+ async function runStampContractProbe(page) {
351
+ // Functions don't survive page.evaluate serialization — pass tag+reason
352
+ // pairs only and re-implement the predicate in-page below.
353
+ const reasons = Object.fromEntries(
354
+ Object.entries(STAMP_CONTRACTS).map(([k, v]) => [k, v.reason]),
355
+ );
356
+ return await page.evaluate((contracts) => {
357
+ const findings = [];
358
+ for (const [tag, reason] of Object.entries(contracts)) {
359
+ document.querySelectorAll(`${tag}`).forEach((el) => {
360
+ // Skip elements clearly outside artifact frames (icons in nav, etc.).
361
+ if (!el.closest('[data-artifact-item]')) return;
362
+ const r = el.getBoundingClientRect();
363
+ if (r.width === 0 || r.height === 0) return;
364
+ // Re-implement contracts in-page (functions don't survive serialization).
365
+ let ok = true;
366
+ if (tag === 'input-ui') ok = !!el.querySelector('[slot="text"]');
367
+ else if (tag === 'search-ui') ok = !!el.querySelector('input-ui');
368
+ if (!ok) {
369
+ findings.push({
370
+ severity: 'critical',
371
+ kind: 'empty-control',
372
+ tag,
373
+ label: el.closest('[data-artifact-label]')?.dataset.artifactLabel || null,
374
+ reason,
375
+ });
376
+ }
377
+ });
378
+ }
379
+ return findings;
380
+ }, reasons);
381
+ }
382
+
383
+ // ── Sweep ─────────────────────────────────────────────────────────────────
384
+
385
+ async function loadComponentRoutes() {
386
+ const sitemap = JSON.parse(await readFile(join(REPO_ROOT, 'site/sitemap.json'), 'utf8'));
387
+ const routes = [];
388
+ // Sitemap shape varies by section; recurse over every key generically
389
+ // and pick up nodes that have a `/site/components/*` path + content.
390
+ const walk = (node, depth = 0) => {
391
+ if (!node || depth > 12) return;
392
+ if (Array.isArray(node)) return node.forEach((n) => walk(n, depth));
393
+ if (typeof node !== 'object') return;
394
+ if (node.path?.startsWith('/site/components/') && node.content) {
395
+ routes.push({ path: node.path, content: node.content, name: node.path.split('/').pop() });
396
+ }
397
+ for (const v of Object.values(node)) walk(v, depth + 1);
398
+ };
399
+ walk(sitemap);
400
+ return FILTER ? routes.filter((r) => r.name.includes(FILTER)) : routes;
401
+ }
402
+
403
+ async function probePage(browser, base, route) {
404
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
405
+ const page = await ctx.newPage();
406
+ const consoleEntries = [];
407
+ page.on('console', (msg) => {
408
+ const type = msg.type();
409
+ if (type === 'error' || type === 'warning') {
410
+ consoleEntries.push({ type, text: msg.text().slice(0, 240) });
411
+ }
412
+ });
413
+ page.on('pageerror', (err) => consoleEntries.push({ type: 'error', text: err.message.slice(0, 240) }));
414
+
415
+ // Hit the route path (not the raw `content` HTML) — the docs shell is
416
+ // what loads the component bundle. Visiting the raw HTML directly leaves
417
+ // every `*-ui` element inert and makes every probe report 0×0.
418
+ const url = `${base}${route.path}`;
419
+ try {
420
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
421
+ } catch (err) {
422
+ await ctx.close();
423
+ return {
424
+ route: route.path,
425
+ error: `navigation failed: ${err.message}`,
426
+ findings: [],
427
+ consoleEntries: [],
428
+ };
429
+ }
430
+ // Settle: components often stamp on connected() + microtasks.
431
+ await page.waitForTimeout(800);
432
+
433
+ const probeFindings = await runProbes(page);
434
+ const stampFindings = await runStampContractProbe(page);
435
+ const findings = [...probeFindings, ...stampFindings];
436
+
437
+ await ctx.close();
438
+ return { route: route.path, findings, consoleEntries };
439
+ }
440
+
441
+ // ── Report ────────────────────────────────────────────────────────────────
442
+
443
+ function severityOrder(s) {
444
+ return { critical: 0, warning: 1, info: 2 }[s] ?? 3;
445
+ }
446
+
447
+ function renderReport(results, base) {
448
+ const all = results.flatMap((r) =>
449
+ r.findings.map((f) => ({ ...f, route: r.route })),
450
+ );
451
+ const errors = results.filter((r) => r.error);
452
+ const consoleErrors = results.flatMap((r) =>
453
+ r.consoleEntries
454
+ .filter((e) => e.type === 'error')
455
+ .map((e) => ({ ...e, route: r.route })),
456
+ );
457
+ const consoleWarnings = results.flatMap((r) =>
458
+ r.consoleEntries
459
+ .filter((e) => e.type === 'warning')
460
+ .map((e) => ({ ...e, route: r.route })),
461
+ );
462
+
463
+ const bySeverity = (sev) =>
464
+ all.filter((f) => f.severity === sev).sort((a, b) => a.route.localeCompare(b.route));
465
+ const critical = bySeverity('critical');
466
+ const warning = bySeverity('warning');
467
+ const info = bySeverity('info');
468
+
469
+ const renderFinding = (f) => {
470
+ const head = `- **${f.kind}** on \`${f.route}\``;
471
+ const labelBit = f.label ? ` (${f.label})` : '';
472
+ const detailBits = [];
473
+ if (f.tag) detailBits.push(`tag=\`${f.tag}\``);
474
+ if (f.selector) detailBits.push(`selector=\`${f.selector}\``);
475
+ if (f.rect) detailBits.push(`rect=${f.rect.w}×${f.rect.h}`);
476
+ if (f.inlineSwatchVar) detailBits.push(`swatch=\`${f.inlineSwatchVar}\``);
477
+ if (f.hint) detailBits.push(f.hint);
478
+ if (f.reason) detailBits.push(f.reason);
479
+ if (f.message) detailBits.push(f.message);
480
+ return `${head}${labelBit} — ${detailBits.join(' · ')}`;
481
+ };
482
+
483
+ const lines = [];
484
+ lines.push(`# Dogfooding report — ${today}`);
485
+ lines.push('');
486
+ lines.push(`Sweep of every \`/site/components/*\` demo page against ${base}.`);
487
+ lines.push('Generated by the `adia-ui-dogfood` skill analyzer (`scripts/analyze.mjs`).');
488
+ lines.push('');
489
+ lines.push('## Summary');
490
+ lines.push('');
491
+ lines.push(`| Severity | Count |`);
492
+ lines.push(`|----------|------:|`);
493
+ lines.push(`| critical | ${critical.length} |`);
494
+ lines.push(`| warning | ${warning.length} |`);
495
+ lines.push(`| info | ${info.length} |`);
496
+ lines.push(`| console errors | ${consoleErrors.length} |`);
497
+ lines.push(`| console warnings | ${consoleWarnings.length} |`);
498
+ lines.push(`| pages scanned | ${results.length} |`);
499
+ lines.push(`| navigation errors | ${errors.length} |`);
500
+ lines.push('');
501
+
502
+ if (critical.length) {
503
+ lines.push(`## Critical (${critical.length})`);
504
+ lines.push('');
505
+ lines.push('Page is visibly broken — element collapsed, swatch transparent, control un-stamped.');
506
+ lines.push('');
507
+ critical.forEach((f) => lines.push(renderFinding(f)));
508
+ lines.push('');
509
+ }
510
+
511
+ if (warning.length) {
512
+ lines.push(`## Warning (${warning.length})`);
513
+ lines.push('');
514
+ lines.push('Likely-broken composition the layout will silently mis-render.');
515
+ lines.push('');
516
+ warning.forEach((f) => lines.push(renderFinding(f)));
517
+ lines.push('');
518
+ }
519
+
520
+ if (info.length) {
521
+ lines.push(`## Info (${info.length})`);
522
+ lines.push('');
523
+ lines.push('Synonym-attribute / deprecation drift. Not breaking; sweep when convenient.');
524
+ lines.push('');
525
+ info.forEach((f) => lines.push(renderFinding(f)));
526
+ lines.push('');
527
+ }
528
+
529
+ if (consoleErrors.length) {
530
+ lines.push(`## Console errors (${consoleErrors.length})`);
531
+ lines.push('');
532
+ consoleErrors.slice(0, 50).forEach((e) =>
533
+ lines.push(`- \`${e.route}\` — ${e.text}`),
534
+ );
535
+ if (consoleErrors.length > 50) {
536
+ lines.push(`- … ${consoleErrors.length - 50} more truncated`);
537
+ }
538
+ lines.push('');
539
+ }
540
+
541
+ if (errors.length) {
542
+ lines.push(`## Navigation errors (${errors.length})`);
543
+ lines.push('');
544
+ errors.forEach((r) => lines.push(`- \`${r.route}\` — ${r.error}`));
545
+ lines.push('');
546
+ }
547
+
548
+ lines.push('---');
549
+ lines.push('');
550
+ lines.push('## Probe coverage');
551
+ lines.push('');
552
+ lines.push('1. **Zero-area** — `*-ui` element with intent (children, text, icon attr) but `getBoundingClientRect()` width or height = 0.');
553
+ lines.push('2. **Transparent fill** — `[data-swatch]`, semantic-variant pills/buttons, chart indicators with `background-color: rgba(0,0,0,0)`. Bug class: chart-legend `--chart-N` fallback that doesn\'t resolve standalone.');
554
+ lines.push('3. **Empty control** — `input-ui`, `search-ui`, `button-ui`, `select-ui`, `tag-ui` whose `connected()` should have stamped internals but didn\'t.');
555
+ lines.push('4. **Synonym-attr / synonym-slot drift** — markers documented in `docs/conventions/attribute-api-migration.md`.');
556
+ lines.push('5. **Alert flex-row** — `alert-ui` with multiple bare `<text-ui>` children (need `<col-ui slot="content">` wrap).');
557
+ lines.push('6. **Console** — every `console.error` and `console.warn` during page load + 800ms settling.');
558
+ lines.push('');
559
+
560
+ return lines.join('\n');
561
+ }
562
+
563
+ // ── Main ──────────────────────────────────────────────────────────────────
564
+
565
+ async function main() {
566
+ const base = `http://localhost:${PORT}`;
567
+ const routes = await loadComponentRoutes();
568
+ log(`▶ Dogfooding sweep — ${routes.length} component pages against ${base}`);
569
+
570
+ const browser = await chromium.launch({ headless: true });
571
+ const results = [];
572
+ let i = 0;
573
+ for (const route of routes) {
574
+ i += 1;
575
+ log(` [${i}/${routes.length}] ${route.path}`);
576
+ try {
577
+ const r = await probePage(browser, base, route);
578
+ results.push(r);
579
+ } catch (err) {
580
+ results.push({ route: route.path, error: err.message, findings: [], consoleEntries: [] });
581
+ }
582
+ }
583
+ await browser.close();
584
+
585
+ const report = renderReport(results, base);
586
+ await mkdir(dirname(OUT), { recursive: true });
587
+ await writeFile(OUT, report, 'utf8');
588
+
589
+ const total = results.reduce((acc, r) => acc + r.findings.length, 0);
590
+ const critical = results.reduce(
591
+ (acc, r) => acc + r.findings.filter((f) => f.severity === 'critical').length,
592
+ 0,
593
+ );
594
+ log(`✔ ${total} findings (${critical} critical) → ${OUT}`);
595
+
596
+ // Exit code: 1 if any critical, 0 otherwise. CI-friendly.
597
+ process.exit(critical > 0 ? 1 : 0);
598
+ }
599
+
600
+ main().catch((err) => {
601
+ console.error('dogfood analyzer crashed:', err);
602
+ process.exit(2);
603
+ });
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "adia-ui-dogfood",
3
+ "version": "0.1.0",
4
+ "description": "Six-mode static + visual sweep across site/components/*, apps/, playgrounds/, catalog/ in a framework-monorepo checkout. Use whenever the user wants to FIND VISUAL BUGS / AUDIT NATIVE PRIMITIVE LEAKS / SCAN ADMIN-SHELL COMPOSITION / SWEEP HTML TYPOS / AUDIT COMPONENT ANATOMY / RUN APP-SHELL QA. Each mode names a real verify target (re-run the audit; check the exit code; commit the report). Triggers on \"run dogfood sweep\", \"scan component pages\", \"find broken demos\", \"visual-QA all components\", \"audit native primitive leaks\", \"find raw <button> usage\", \"audit admin-shell composition\", \"shell composition audit\", \"sweep html typos\", \"audit card header anatomy\", or scheduled dogfood-* CRON routines. Does NOT trigger for: composing UI screens (consumer factory), authoring primitives (use adia-ui-authoring), cutting releases (use adia-ui-release).",
5
+ "status": "stable",
6
+ "authors": ["kim.granlund"],
7
+ "tags": [
8
+ "dogfood",
9
+ "audit",
10
+ "visual-qa",
11
+ "static-analysis",
12
+ "headless-chromium",
13
+ "native-primitive-leak",
14
+ "admin-shell",
15
+ "html-typo",
16
+ "anatomy-sweep"
17
+ ],
18
+ "peer_skills": [
19
+ "adia-ui-authoring",
20
+ "adia-ui-release"
21
+ ],
22
+ "absorbs": [],
23
+ "environment": {
24
+ "portable": false,
25
+ "requires": [
26
+ "framework monorepo (@adia-ai or a fork) with site/components/, apps/, playgrounds/, catalog/, packages/web-components/components/*/",
27
+ "Node 18+",
28
+ "Playwright (for mode 1)",
29
+ "a running dev server (for mode 1)"
30
+ ],
31
+ "rationale": "Probes assume adia-ui primitives and the monorepo's HTML/package conventions; modes 2/4/5 drive repo-local audit scripts that live in the monorepo."
32
+ },
33
+ "files": [
34
+ "SKILL.md",
35
+ "skill.json",
36
+ "CHANGELOG.md",
37
+ "README.md",
38
+ "scripts/analyze.mjs"
39
+ ]
40
+ }