@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,569 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gen-review-decompose.mjs — Phase 2 automation for adia-ui-gen-review.
4
+ *
5
+ * For each prompt in gallery-latest.json:
6
+ * 1. Navigate to the gallery page and locate the canvas-ui for that prompt.
7
+ * 2. Wait 2.5s for canvas-ui.processAll() to settle.
8
+ * 3. Screenshot the .gallery-canvas-wrap element.
9
+ * 4. Walk the canvas DOM tree and save raw JSON.
10
+ * 5. Apply the primitive lookup table → sanitized decomposed.json.
11
+ *
12
+ * Trust boundary: this script produces the Phase 2→5 intermediate files.
13
+ * decomposed.json contains ONLY allowlisted attributes (no data-*, aria-*
14
+ * content). Phase 5 (fix plan writing) reads only decomposed.json.
15
+ *
16
+ * This script is skill-owned but operates on the @adia-ai monorepo: run it
17
+ * from the monorepo root (it reads apps/genui/.../gallery-latest.json and
18
+ * writes the review/cycle-N tree there). Requires `playwright` in the
19
+ * monorepo's node_modules.
20
+ *
21
+ * Usage (from the monorepo root):
22
+ * node ${CLAUDE_PLUGIN_ROOT}/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs --cycle 1
23
+ * node ${CLAUDE_PLUGIN_ROOT}/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs --cycle 2 --group auth
24
+ * node ${CLAUDE_PLUGIN_ROOT}/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs --cycle 1 --prompt login-form
25
+ * node ${CLAUDE_PLUGIN_ROOT}/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs --cycle 1 --port 5174
26
+ * node ${CLAUDE_PLUGIN_ROOT}/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs --dry-run (validate inputs, no browser)
27
+ *
28
+ * Output (per prompt):
29
+ * review/cycle-{N}/screenshots/{slug}.png
30
+ * review/cycle-{N}/raw-dom/{slug}.json
31
+ * review/cycle-{N}/decomposed/{slug}.json ← the trust-boundary file
32
+ *
33
+ * Exits 0 on success, 1 if any prompt has a RENDER_FAILURE.
34
+ */
35
+
36
+ import { chromium } from 'playwright';
37
+ import { mkdir, writeFile, readFile } from 'node:fs/promises';
38
+ import { existsSync } from 'node:fs';
39
+ import { join } from 'node:path';
40
+
41
+ // The script lives inside the plugin but runs against the monorepo. All
42
+ // monorepo paths are resolved from the working directory (the monorepo root),
43
+ // not from the script location.
44
+ const REPO_ROOT = process.cwd();
45
+
46
+ // ── CLI ──────────────────────────────────────────────────────────────────────
47
+
48
+ const args = process.argv.slice(2);
49
+ const flag = k => args.includes(`--${k}`);
50
+ const opt = k => { const i = args.indexOf(`--${k}`); return i >= 0 ? args[i + 1] : undefined; };
51
+
52
+ const cycleNum = parseInt(opt('cycle') ?? '1', 10);
53
+ const groupFilter = opt('group') ?? null;
54
+ const promptFilter = opt('prompt') ?? null;
55
+ const port = opt('port') ?? '5300';
56
+ const dryRun = flag('dry-run');
57
+ const settleMs = parseInt(opt('settle') ?? '2500', 10);
58
+
59
+ if (isNaN(cycleNum) || cycleNum < 1) {
60
+ console.error('[decompose] --cycle must be a positive integer'); process.exit(1);
61
+ }
62
+
63
+ // ── Primitive lookup table (from loop-protocol.md §Phase 2) ─────────────────
64
+
65
+ const TAG_TO_COMPONENT = {
66
+ 'card-ui': 'Card',
67
+ 'col-ui': 'Column',
68
+ 'row-ui': 'Row',
69
+ 'grid-ui': 'Grid',
70
+ 'field-ui': 'Field',
71
+ 'input-ui': 'Input',
72
+ 'textarea-ui': 'Textarea',
73
+ 'select-ui': 'Select',
74
+ 'button-ui': 'Button',
75
+ 'badge-ui': 'Badge',
76
+ 'tag-ui': 'Tag',
77
+ 'text-ui': 'Text',
78
+ 'icon-ui': 'Icon',
79
+ 'stat-ui': 'Stat',
80
+ 'avatar-ui': 'Avatar',
81
+ 'list-ui': 'List',
82
+ 'list-item-ui': 'ListItem',
83
+ 'tabs-ui': 'Tabs',
84
+ 'tab-ui': 'Tab',
85
+ 'nav-ui': 'Nav',
86
+ 'nav-item-ui': 'NavItem',
87
+ 'nav-group-ui': 'NavGroup',
88
+ 'check-ui': 'Checkbox',
89
+ 'switch-ui': 'Switch',
90
+ 'slider-ui': 'Slider',
91
+ 'progress-ui': 'Progress',
92
+ 'progress-row-ui': 'ProgressRow',
93
+ 'skeleton-ui': 'Skeleton',
94
+ 'alert-ui': 'Alert',
95
+ 'modal-ui': 'Modal',
96
+ 'drawer-ui': 'Drawer',
97
+ 'popover-ui': 'Popover',
98
+ 'tooltip-ui': 'Tooltip',
99
+ 'divider-ui': 'Divider',
100
+ 'breadcrumb-ui': 'Breadcrumb',
101
+ 'pagination-ui': 'Pagination',
102
+ 'accordion-ui': 'Accordion',
103
+ 'timeline-ui': 'Timeline',
104
+ 'timeline-item-ui':'TimelineItem',
105
+ 'table-ui': 'Table',
106
+ 'feed-ui': 'Feed',
107
+ 'feed-item-ui': 'FeedItem',
108
+ 'empty-state-ui': 'EmptyState',
109
+ 'step-progress-ui':'StepProgress',
110
+ 'onboarding-checklist-ui': 'OnboardingChecklist',
111
+ 'mark-ui': 'Mark',
112
+ 'inline-edit-ui': 'InlineEdit',
113
+ 'tour-ui': 'Tour',
114
+ 'link-ui': 'Link',
115
+ 'code-ui': 'Code',
116
+ 'rating-ui': 'Rating',
117
+ 'image-ui': 'Image',
118
+ 'embed-ui': 'Embed',
119
+ 'search-ui': 'Search',
120
+ 'combobox-ui': 'Combobox',
121
+ 'tags-input-ui': 'TagsInput',
122
+ 'heatmap-ui': 'Heatmap',
123
+ 'chart-ui': 'Chart',
124
+ 'upload-ui': 'Upload',
125
+ 'visually-hidden-ui': 'VisuallyHidden',
126
+ // Additional primitives
127
+ 'accordion-item-ui': 'AccordionItem',
128
+ 'action-item-ui': 'ActionItem',
129
+ 'action-list-ui': 'ActionList',
130
+ 'agent-artifact-ui': 'AgentArtifact',
131
+ 'agent-feedback-bar-ui': 'AgentFeedbackBar',
132
+ 'agent-questions-ui': 'AgentQuestions',
133
+ 'agent-reasoning-ui': 'AgentReasoning',
134
+ 'agent-suggestions-ui': 'AgentSuggestions',
135
+ 'agent-trace-ui': 'AgentTrace',
136
+ 'aside-ui': 'Aside',
137
+ 'avatar-group-ui': 'AvatarGroup',
138
+ 'block-ui': 'Block',
139
+ 'blockquote-ui': 'Blockquote',
140
+ 'calendar-grid-ui': 'CalendarGrid',
141
+ 'calendar-picker-ui': 'CalendarPicker',
142
+ 'chart-legend-ui': 'ChartLegend',
143
+ 'chat-input-ui': 'ChatInput',
144
+ 'chat-thread-ui': 'ChatThread',
145
+ 'color-input-ui': 'ColorInput',
146
+ 'color-picker-ui': 'ColorPicker',
147
+ 'command-ui': 'Command',
148
+ 'context-menu-ui': 'ContextMenu',
149
+ 'date-range-picker-ui': 'DateRangePicker',
150
+ 'datetime-picker-ui': 'DatetimePicker',
151
+ 'demo-toggle-ui': 'DemoToggle',
152
+ 'description-list-ui': 'DescriptionList',
153
+ 'fields-ui': 'Fields',
154
+ 'footer-ui': 'Footer',
155
+ 'header-ui': 'Header',
156
+ 'inline-message-ui': 'InlineMessage',
157
+ 'inspector-ui': 'Inspector',
158
+ 'integration-card-ui': 'IntegrationCard',
159
+ 'kbd-ui': 'Kbd',
160
+ 'list-window-ui': 'ListWindow',
161
+ 'loading-overlay-ui': 'LoadingOverlay',
162
+ 'menu-divider-ui': 'MenuDivider',
163
+ 'menu-item-ui': 'MenuItem',
164
+ 'menu-ui': 'Menu',
165
+ 'noodles-ui': 'Noodles',
166
+ 'number-format-ui': 'NumberFormat',
167
+ 'option-card-ui': 'OptionCard',
168
+ 'otp-input-ui': 'OtpInput',
169
+ 'page-ui': 'Page',
170
+ 'pane-ui': 'Pane',
171
+ 'password-strength-ui': 'PasswordStrength',
172
+ 'pipeline-status-ui': 'PipelineStatus',
173
+ 'qr-code-ui': 'QRCode',
174
+ 'radio-ui': 'Radio',
175
+ 'range-ui': 'Range',
176
+ 'relative-time-ui': 'RelativeTime',
177
+ 'richtext-ui': 'RichText',
178
+ 'section-ui': 'Section',
179
+ 'segment-ui': 'Segment',
180
+ 'segmented-ui': 'Segmented',
181
+ 'skip-nav-ui': 'SkipNav',
182
+ 'spinner-ui': 'Spinner',
183
+ 'stack-ui': 'Stack',
184
+ 'stepper-item-ui': 'StepperItem',
185
+ 'stepper-ui': 'Stepper',
186
+ 'stream-ui': 'Stream',
187
+ 'swatch-ui': 'Swatch',
188
+ 'swiper-ui': 'Swiper',
189
+ 'table-toolbar-ui': 'TableToolbar',
190
+ 'time-picker-ui': 'TimePicker',
191
+ 'toast-ui': 'Toast',
192
+ 'toc-ui': 'TableOfContents',
193
+ 'toggle-group-ui': 'ToggleGroup',
194
+ 'toggle-option-ui': 'ToggleOption',
195
+ 'toggle-scheme-ui': 'ToggleScheme',
196
+ 'toolbar-group-ui': 'ToolbarGroup',
197
+ 'toolbar-ui': 'Toolbar',
198
+ 'tour-step-ui': 'TourStep',
199
+ 'tree-item-ui': 'TreeItem',
200
+ 'tree-ui': 'Tree',
201
+ // Native structural elements inside component light DOM — flag as native
202
+ 'header': 'NativeHeader', // should be card-ui slot, not bare <header>
203
+ 'section': 'NativeSection',
204
+ 'footer': 'NativeFooter',
205
+ 'form': 'NativeForm', // should be wrapped in a primitive
206
+ 'a': 'NativeLink', // should be link-ui
207
+ 'ul': 'NativeList',
208
+ 'li': 'NativeListItem',
209
+ 'p': 'NativeParagraph',
210
+ 'span': 'NativeSpan',
211
+ 'img': 'NativeImage', // should be image-ui
212
+ 'h1': 'NativeH1', 'h2': 'NativeH2', 'h3': 'NativeH3',
213
+ 'h4': 'NativeH4', 'h5': 'NativeH5', 'h6': 'NativeH6',
214
+ // Shell tags — skip but descend
215
+ 'canvas-ui': null,
216
+ 'a2ui-root': null,
217
+ };
218
+
219
+ // Allowlisted attribute names (values from these are safe to include)
220
+ const ATTR_ALLOWLIST = new Set([
221
+ 'class', 'id', 'slot', 'text', 'label', 'value', 'variant', 'icon',
222
+ 'size', 'gap', 'columns', 'type', 'checked', 'disabled', 'readonly',
223
+ 'placeholder', 'heading', 'description', 'trend', 'change',
224
+ ]);
225
+
226
+ // ── Load gallery data ────────────────────────────────────────────────────────
227
+
228
+ const galleryPath = join(REPO_ROOT, 'apps/genui/app/gen-ui-gallery/outputs/gallery-latest.json');
229
+ if (!existsSync(galleryPath)) {
230
+ console.error('[decompose] gallery-latest.json not found. Run npm run gallery:generate first.');
231
+ process.exit(1);
232
+ }
233
+
234
+ const gallery = JSON.parse(await readFile(galleryPath, 'utf8'));
235
+
236
+ // Validate required keys
237
+ for (const key of ['version', 'generatedAt', 'engines', 'groups']) {
238
+ if (!(key in gallery)) {
239
+ console.error(`[decompose] gallery-latest.json missing required key: "${key}"`);
240
+ process.exit(1);
241
+ }
242
+ }
243
+
244
+ // Build flat prompt list with optional filters
245
+ const allPrompts = [];
246
+ for (const group of gallery.groups) {
247
+ if (groupFilter && group.slug !== groupFilter) continue;
248
+ for (const prompt of group.prompts) {
249
+ if (promptFilter && prompt.slug !== promptFilter) continue;
250
+ for (const engineKey of Object.keys(prompt.engines ?? {})) {
251
+ const engineData = prompt.engines[engineKey];
252
+ if (!engineData || engineData.dryRun) continue;
253
+ allPrompts.push({ group: group.slug, prompt: prompt.slug, engineKey, engineData });
254
+ }
255
+ }
256
+ }
257
+
258
+ if (allPrompts.length === 0) {
259
+ console.error('[decompose] No prompts matched the specified filters.');
260
+ process.exit(1);
261
+ }
262
+
263
+ // ── Output directories ───────────────────────────────────────────────────────
264
+
265
+ const outBase = join(REPO_ROOT, 'apps/genui/app/gen-ui-gallery/review', `cycle-${cycleNum}`);
266
+ const screenshotDir = join(outBase, 'screenshots');
267
+ const rawDomDir = join(outBase, 'raw-dom');
268
+ const decomposedDir = join(outBase, 'decomposed');
269
+
270
+ if (!dryRun) {
271
+ await mkdir(screenshotDir, { recursive: true });
272
+ await mkdir(rawDomDir, { recursive: true });
273
+ await mkdir(decomposedDir, { recursive: true });
274
+ }
275
+
276
+ const galleryVersion = gallery.version ?? 'unknown';
277
+ const galleryGeneratedAt = gallery.generatedAt ?? null;
278
+
279
+ console.log(`\n[decompose] cycle-${cycleNum} | gallery-v${galleryVersion} | ${allPrompts.length} prompts | port ${port}${dryRun ? ' | DRY RUN' : ''}`);
280
+ if (dryRun) { console.log('[decompose] Dry run complete — no browser launched.'); process.exit(0); }
281
+
282
+ // Write cycle-manifest.json for provenance before any screenshots are taken
283
+ if (!dryRun) {
284
+ const manifestPath = join(outBase, 'cycle-manifest.json');
285
+ await writeFile(manifestPath, JSON.stringify({
286
+ cycleNumber: cycleNum,
287
+ galleryVersion,
288
+ galleryGeneratedAt,
289
+ decomposedAt: new Date().toISOString(),
290
+ promptCount: allPrompts.length,
291
+ port,
292
+ }, null, 2));
293
+ }
294
+
295
+ // ── Playwright run ───────────────────────────────────────────────────────────
296
+
297
+ const galleryUrl = `http://localhost:${port}/apps/genui/app/gen-ui-gallery/gen-ui-gallery.html`;
298
+ const browser = await chromium.launch();
299
+ const page = await browser.newPage();
300
+ await page.setViewportSize({ width: 1280, height: 900 });
301
+
302
+ // Verify gallery is reachable
303
+ const res = await page.goto(galleryUrl, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => null);
304
+ if (!res || res.status() >= 400) {
305
+ console.error(`[decompose] Cannot reach gallery at ${galleryUrl}. Is the dev server running on port ${port}?`);
306
+ await browser.close();
307
+ process.exit(1);
308
+ }
309
+ await page.waitForTimeout(1500); // initial render settle
310
+
311
+ const results = [];
312
+ let renderFailureCount = 0;
313
+ let idx = 0;
314
+
315
+ for (const { group, prompt: promptSlug, engineKey, engineData } of allPrompts) {
316
+ idx++;
317
+ const slug = `${group}-${promptSlug}-${engineKey}`;
318
+ process.stdout.write(` [${idx}/${allPrompts.length}] ${slug.padEnd(50)} `);
319
+
320
+ // ── Locate the canvas-ui for this prompt ────────────────────────────────
321
+ // The gallery renders prompts as .gallery-prompt sections with h3 headings.
322
+ // Each has a .gallery-canvas-wrap containing a canvas-ui.
323
+ // We identify by matching the group anchor + prompt h3 text.
324
+
325
+ const promptLabel = promptSlug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
326
+
327
+ // Scroll to the prompt section and wait for canvas settle
328
+ await page.evaluate(({ groupSlug, label }) => {
329
+ const section = document.getElementById(`group-${groupSlug}`);
330
+ if (!section) return;
331
+ const h3s = [...section.querySelectorAll('.gallery-prompt-heading')];
332
+ const target = h3s.find(h => h.textContent.trim().toLowerCase() === label.toLowerCase());
333
+ target?.closest('.gallery-prompt')?.scrollIntoView({ behavior: 'instant', block: 'center' });
334
+ }, { groupSlug: group, label: promptLabel });
335
+
336
+ await page.waitForTimeout(settleMs);
337
+
338
+ // ── Screenshot ───────────────────────────────────────────────────────────
339
+
340
+ const canvasWrapHandle = await page.evaluateHandle(({ groupSlug, label }) => {
341
+ const section = document.getElementById(`group-${groupSlug}`);
342
+ if (!section) return null;
343
+ const h3s = [...section.querySelectorAll('.gallery-prompt-heading')];
344
+ const h3 = h3s.find(h => h.textContent.trim().toLowerCase() === label.toLowerCase());
345
+ return h3?.closest('.gallery-prompt')?.querySelector('.gallery-canvas-wrap') ?? null;
346
+ }, { groupSlug: group, label: promptLabel });
347
+
348
+ const canvasWrapEl = canvasWrapHandle.asElement();
349
+ let renderFailure = false;
350
+ let screenshotPath = null;
351
+
352
+ if (canvasWrapEl) {
353
+ const box = await canvasWrapEl.boundingBox();
354
+ renderFailure = !box || box.height < 50;
355
+ if (!renderFailure) {
356
+ screenshotPath = join(screenshotDir, `${slug}.png`);
357
+ await page.screenshot({ path: screenshotPath, clip: box });
358
+ }
359
+ } else {
360
+ renderFailure = true;
361
+ }
362
+
363
+ // ── Raw DOM walk ─────────────────────────────────────────────────────────
364
+
365
+ const rawDom = await page.evaluate(({ groupSlug, label }) => {
366
+ const section = document.getElementById(`group-${groupSlug}`);
367
+ if (!section) return null;
368
+ const h3s = [...section.querySelectorAll('.gallery-prompt-heading')];
369
+ const h3 = h3s.find(h => h.textContent.trim().toLowerCase() === label.toLowerCase());
370
+ const wrap = h3?.closest('.gallery-prompt')?.querySelector('.gallery-canvas-wrap');
371
+ if (!wrap) return null;
372
+
373
+ function walk(node, depth) {
374
+ if (depth > 8) return null;
375
+ const tag = node.tagName?.toLowerCase() ?? '';
376
+ if (!tag) return null;
377
+ const attrs = {};
378
+ for (const a of (node.attributes || [])) {
379
+ attrs[a.name] = a.value;
380
+ }
381
+ const children = [...node.children]
382
+ .map(c => walk(c, depth + 1))
383
+ .filter(Boolean);
384
+ return { tag, attrs, children };
385
+ }
386
+
387
+ // Walk inside canvas-ui (the a2ui-root subtree)
388
+ const canvasUi = wrap.querySelector('canvas-ui');
389
+ if (!canvasUi) return null;
390
+ return walk(canvasUi, 0);
391
+ }, { groupSlug: group, label: promptLabel });
392
+
393
+ const rawDomPath = join(rawDomDir, `${slug}.json`);
394
+ await writeFile(rawDomPath, JSON.stringify(rawDom, null, 2));
395
+
396
+ // ── Overflow / clip detection (visual gate) ───────────────────────────────
397
+ // Mechanically detects text truncation and element clipping within the
398
+ // canvas. Runs only when the canvas rendered (renderFailure = false).
399
+ // Results land in decomposed.json as `overflowElements` — a non-empty array
400
+ // is treated as P1 in Phase 4 regardless of the Phase 3 structural score.
401
+
402
+ const OVERFLOW_TAGS = new Set([
403
+ 'text-ui', 'stat-ui', 'badge-ui', 'field-ui', 'button-ui',
404
+ 'label', 'span', 'p', 'h1', 'h2', 'h3', 'h4',
405
+ ]);
406
+
407
+ let overflowElements = [];
408
+
409
+ if (!renderFailure) {
410
+ overflowElements = await page.evaluate(({ groupSlug, label, overflowTags }) => {
411
+ const section = document.getElementById(`group-${groupSlug}`);
412
+ if (!section) return [];
413
+ const h3s = [...section.querySelectorAll('.gallery-prompt-heading')];
414
+ const h3 = h3s.find(h => h.textContent.trim().toLowerCase() === label.toLowerCase());
415
+ const wrap = h3?.closest('.gallery-prompt')?.querySelector('.gallery-canvas-wrap');
416
+ if (!wrap) return [];
417
+
418
+ const found = [];
419
+ function walk(el) {
420
+ const tag = el.tagName?.toLowerCase() ?? '';
421
+ if (overflowTags.includes(tag)) {
422
+ const style = getComputedStyle(el);
423
+ const hasHidden = style.overflow === 'hidden' || style.overflowX === 'hidden';
424
+ if (hasHidden && el.scrollWidth > el.clientWidth + 2) {
425
+ found.push({ tag, clippedWidth: true });
426
+ }
427
+ if (hasHidden && el.scrollHeight > el.clientHeight + 2) {
428
+ found.push({ tag, clippedHeight: true });
429
+ }
430
+ }
431
+ for (const child of el.children) walk(child);
432
+ }
433
+ walk(wrap);
434
+ return found;
435
+ }, { groupSlug: group, label: promptLabel, overflowTags: [...OVERFLOW_TAGS] });
436
+ }
437
+
438
+ // ── Primitive lookup + sanitize ──────────────────────────────────────────
439
+
440
+ const unknownElements = [];
441
+
442
+ function lookupAndSanitize(node) {
443
+ if (!node) return null;
444
+ const tag = node.tag?.toLowerCase() ?? '';
445
+ const component = TAG_TO_COMPONENT[tag];
446
+
447
+ // Shell tags (canvas-ui, a2ui-root, plain div wrappers): skip the node
448
+ // itself but descend into children to find the real component tree.
449
+ if (component === null || tag === 'div') {
450
+ const childResults = (node.children ?? []).map(lookupAndSanitize).filter(Boolean);
451
+ if (childResults.length === 1) return childResults[0];
452
+ if (childResults.length > 1) {
453
+ // Multiple siblings at this level — return a synthetic wrapper
454
+ return { component: 'Fragment', attrs: {}, children: childResults };
455
+ }
456
+ return null;
457
+ }
458
+
459
+ if (component === undefined) {
460
+ if (tag && !tag.startsWith('#')) unknownElements.push(tag);
461
+ }
462
+
463
+ // Allowlisted attrs only — strip data-*, aria-*, and any non-allowlisted
464
+ const safeAttrs = {};
465
+ for (const [name, value] of Object.entries(node.attrs ?? {})) {
466
+ if (ATTR_ALLOWLIST.has(name) && value && value.trim()) {
467
+ safeAttrs[name] = value.trim().slice(0, 200); // cap length
468
+ }
469
+ }
470
+
471
+ const children = (node.children ?? [])
472
+ .map(lookupAndSanitize)
473
+ .filter(Boolean);
474
+
475
+ return { component: component ?? `UnknownElement:${tag}`, attrs: safeAttrs, children };
476
+ }
477
+
478
+ const sanitized = rawDom ? lookupAndSanitize(rawDom) : null;
479
+
480
+ // Flatten to component list for decomposed.json
481
+ function flatten(node, list = []) {
482
+ if (!node) return list;
483
+ list.push(node.component);
484
+ for (const child of node.children ?? []) flatten(child, list);
485
+ return list;
486
+ }
487
+ const componentList = sanitized ? flatten(sanitized) : [];
488
+
489
+ // Detect root + layout primitive
490
+ const rootComponent = sanitized?.component ?? null;
491
+ const layoutPrimitive = componentList.find(c => ['Column', 'Row', 'Grid'].includes(c)) ?? null;
492
+
493
+ // Extract slot positions (one level deep)
494
+ const slotPositions = [];
495
+ function extractSlots(node, parentComponent) {
496
+ if (!node) return;
497
+ const slot = node.attrs?.slot;
498
+ if (slot && parentComponent) {
499
+ slotPositions.push({ parent: parentComponent, slot, child: node.component });
500
+ }
501
+ for (const child of node.children ?? []) extractSlots(child, node.component);
502
+ }
503
+ if (sanitized) extractSlots(sanitized, null);
504
+
505
+ // Key attr summary (button text, field labels, etc.)
506
+ const keyAttrs = {};
507
+ function extractKeyAttrs(node) {
508
+ if (!node) return;
509
+ const c = node.component;
510
+ if (!keyAttrs[c] && Object.keys(node.attrs).length) keyAttrs[c] = node.attrs;
511
+ for (const child of node.children ?? []) extractKeyAttrs(child);
512
+ }
513
+ if (sanitized) extractKeyAttrs(sanitized);
514
+
515
+ // Write decomposed.json (the trust-boundary file)
516
+ const decomposed = {
517
+ promptSlug,
518
+ groupSlug: group,
519
+ engineKey,
520
+ renderFailure,
521
+ rootComponent,
522
+ layoutPrimitive,
523
+ components: componentList,
524
+ slotPositions,
525
+ attrs: keyAttrs,
526
+ unknownElements: [...new Set(unknownElements)],
527
+ overflowElements,
528
+ screenshotPath: screenshotPath ? screenshotPath.replace(REPO_ROOT + '/', '') : null,
529
+ rawDomPath: rawDomPath.replace(REPO_ROOT + '/', ''),
530
+ generatedAt: new Date().toISOString(),
531
+ };
532
+
533
+ const decomposedPath = join(decomposedDir, `${slug}.json`);
534
+ await writeFile(decomposedPath, JSON.stringify(decomposed, null, 2));
535
+
536
+ if (renderFailure) {
537
+ renderFailureCount++;
538
+ process.stdout.write(`RENDER_FAILURE\n`);
539
+ } else {
540
+ const overflowNote = overflowElements.length > 0 ? ` | ⚠ ${overflowElements.length} overflow` : '';
541
+ process.stdout.write(`✓ ${componentList.length} components | root: ${rootComponent ?? '?'}${overflowNote}\n`);
542
+ }
543
+
544
+ results.push({ slug, renderFailure, componentCount: componentList.length, rootComponent, overflowCount: overflowElements.length });
545
+ }
546
+
547
+ await browser.close();
548
+
549
+ // ── Summary ──────────────────────────────────────────────────────────────────
550
+
551
+ const passed = results.filter(r => !r.renderFailure).length;
552
+ const failed = results.filter(r => r.renderFailure).length;
553
+
554
+ const overflowPrompts = results.filter(r => r.overflowCount > 0);
555
+ console.log(`\n[decompose] Done — ${passed} ok, ${failed} RENDER_FAILURE (gallery-v${galleryVersion})`);
556
+ if (overflowPrompts.length > 0) {
557
+ console.log(` ⚠ ${overflowPrompts.length} prompts with overflow/clip (auto-P1 in Phase 4):`);
558
+ overflowPrompts.forEach(r => console.log(` ${r.slug} (${r.overflowCount} elements)`));
559
+ }
560
+ console.log(` Screenshots → review/cycle-${cycleNum}/screenshots/`);
561
+ console.log(` Raw DOM → review/cycle-${cycleNum}/raw-dom/`);
562
+ console.log(` Decomposed → review/cycle-${cycleNum}/decomposed/`);
563
+ console.log(` Manifest → review/cycle-${cycleNum}/cycle-manifest.json`);
564
+
565
+ if (failed > 0) {
566
+ console.log('\nRENDER_FAILURE prompts:');
567
+ results.filter(r => r.renderFailure).forEach(r => console.log(' ', r.slug));
568
+ process.exit(1);
569
+ }