@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.
- package/.claude-plugin/plugin.json +23 -0
- package/CHANGELOG.md +26 -0
- package/README.md +54 -0
- package/bin/forge-lint +263 -0
- package/bin/lib/audit-axes.mjs +555 -0
- package/bin/lib/dry-run-irreversible.mjs +236 -0
- package/bin/lib/run-skill-evals.mjs +487 -0
- package/bin/lib/teach-router.mjs +250 -0
- package/commands/adia-forge-a2ui.md +10 -0
- package/commands/adia-forge-author.md +10 -0
- package/commands/adia-forge-dogfood.md +8 -0
- package/commands/adia-forge-llm.md +8 -0
- package/commands/adia-forge-orient.md +10 -0
- package/commands/adia-forge-release.md +8 -0
- package/commands/adia-forge-review.md +10 -0
- package/hooks/hooks.json +15 -0
- package/package.json +41 -0
- package/references/shared/content-trust.md +76 -0
- package/references/shared/pev-rationale.md +64 -0
- package/references/shared/skill-conventions.md +133 -0
- package/skills/adia-ui-a2ui/CHANGELOG.md +32 -0
- package/skills/adia-ui-a2ui/SKILL.md +243 -0
- package/skills/adia-ui-a2ui/evals/adversarial-corpus.json +75 -0
- package/skills/adia-ui-a2ui/evals/routing-corpus.json +31 -0
- package/skills/adia-ui-a2ui/evals/teach-routing-cases.json +100 -0
- package/skills/adia-ui-a2ui/references/anti-patterns.md +24 -0
- package/skills/adia-ui-a2ui/references/chunk-authoring.md +88 -0
- package/skills/adia-ui-a2ui/references/corpus-discipline.md +56 -0
- package/skills/adia-ui-a2ui/references/eval-diagnostics.md +127 -0
- package/skills/adia-ui-a2ui/references/fragment-graph.md +91 -0
- package/skills/adia-ui-a2ui/references/mcp-pipeline-ops.md +106 -0
- package/skills/adia-ui-a2ui/references/mcp-tool-reference.md +398 -0
- package/skills/adia-ui-a2ui/references/pipeline-overview.md +175 -0
- package/skills/adia-ui-a2ui/references/semantic-fail-lifting.md +120 -0
- package/skills/adia-ui-a2ui/references/strategy-engines.md +111 -0
- package/skills/adia-ui-a2ui/references/teach-protocol.md +220 -0
- package/skills/adia-ui-a2ui/references/zettel-calibration.md +93 -0
- package/skills/adia-ui-a2ui/scripts/audit-a2ui-roster.mjs +96 -0
- package/skills/adia-ui-a2ui/scripts/teach-route.mjs +157 -0
- package/skills/adia-ui-a2ui/skill.json +38 -0
- package/skills/adia-ui-authoring/CHANGELOG.md +32 -0
- package/skills/adia-ui-authoring/SKILL.md +256 -0
- package/skills/adia-ui-authoring/assets/case-studies/admin-shell-decomposition.md +101 -0
- package/skills/adia-ui-authoring/assets/case-studies/maxtokens-32768-discovery.md +109 -0
- package/skills/adia-ui-authoring/assets/case-studies/theme-panel-promotion.md +113 -0
- package/skills/adia-ui-authoring/evals/adversarial-design-plan-gates.json +138 -0
- package/skills/adia-ui-authoring/evals/routing-corpus.json +245 -0
- package/skills/adia-ui-authoring/references/anti-patterns.md +606 -0
- package/skills/adia-ui-authoring/references/api-contract.md +205 -0
- package/skills/adia-ui-authoring/references/authoring-cycle.md +211 -0
- package/skills/adia-ui-authoring/references/canonical-pattern-index.md +179 -0
- package/skills/adia-ui-authoring/references/code-style.md +329 -0
- package/skills/adia-ui-authoring/references/common-gotchas.md +93 -0
- package/skills/adia-ui-authoring/references/composite-demo-protocol.md +1084 -0
- package/skills/adia-ui-authoring/references/css-patterns.md +364 -0
- package/skills/adia-ui-authoring/references/lifecycle-patterns.md +302 -0
- package/skills/adia-ui-authoring/references/llm-bridge.md +254 -0
- package/skills/adia-ui-authoring/references/module-promotion.md +289 -0
- package/skills/adia-ui-authoring/references/primitive-audit.md +123 -0
- package/skills/adia-ui-authoring/references/shell-patterns.md +561 -0
- package/skills/adia-ui-authoring/references/teach-protocol.md +428 -0
- package/skills/adia-ui-authoring/references/token-contract.md +120 -0
- package/skills/adia-ui-authoring/references/worked-example.md +351 -0
- package/skills/adia-ui-authoring/references/yaml-contract.md +224 -0
- package/skills/adia-ui-authoring/scripts/audit-authoring-roster.mjs +148 -0
- package/skills/adia-ui-authoring/scripts/build-canonical-pattern-index.mjs +199 -0
- package/skills/adia-ui-authoring/skill.json +45 -0
- package/skills/adia-ui-dogfood/CHANGELOG.md +17 -0
- package/skills/adia-ui-dogfood/README.md +62 -0
- package/skills/adia-ui-dogfood/SKILL.md +866 -0
- package/skills/adia-ui-dogfood/scripts/analyze.mjs +603 -0
- package/skills/adia-ui-dogfood/skill.json +40 -0
- package/skills/adia-ui-forge/SKILL.md +88 -0
- package/skills/adia-ui-forge/evals/routing-corpus.json +30 -0
- package/skills/adia-ui-gen-review/CHANGELOG.md +108 -0
- package/skills/adia-ui-gen-review/SKILL.md +266 -0
- package/skills/adia-ui-gen-review/references/loop-protocol.md +712 -0
- package/skills/adia-ui-gen-review/references/rubric-cosmetic.md +144 -0
- package/skills/adia-ui-gen-review/references/rubric-decompose.md +117 -0
- package/skills/adia-ui-gen-review/references/rubric-score.md +249 -0
- package/skills/adia-ui-gen-review/references/scores.schema.json +125 -0
- package/skills/adia-ui-gen-review/references/teach-protocol.md +214 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-coverage-audit.mjs +127 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs +569 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-status.mjs +176 -0
- package/skills/adia-ui-gen-review/scripts/validate-cycle-scores.mjs +198 -0
- package/skills/adia-ui-gen-review/skill.json +22 -0
- package/skills/adia-ui-llm/CHANGELOG.md +25 -0
- package/skills/adia-ui-llm/SKILL.md +165 -0
- package/skills/adia-ui-llm/evals/adversarial-corpus.json +75 -0
- package/skills/adia-ui-llm/evals/routing-corpus.json +30 -0
- package/skills/adia-ui-llm/evals/teach-routing-cases.json +73 -0
- package/skills/adia-ui-llm/references/adapter-contract.md +99 -0
- package/skills/adia-ui-llm/references/add-a-provider.md +90 -0
- package/skills/adia-ui-llm/references/bridge-facade.md +94 -0
- package/skills/adia-ui-llm/references/browser-proxy-boundary.md +89 -0
- package/skills/adia-ui-llm/references/model-registry.md +75 -0
- package/skills/adia-ui-llm/references/streaming-sse.md +95 -0
- package/skills/adia-ui-llm/references/teach-protocol.md +78 -0
- package/skills/adia-ui-llm/scripts/audit-llm-roster.mjs +93 -0
- package/skills/adia-ui-llm/scripts/teach-route.mjs +119 -0
- package/skills/adia-ui-llm/skill.json +33 -0
- package/skills/adia-ui-release/CHANGELOG.md +23 -0
- package/skills/adia-ui-release/SKILL.md +295 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-batch-push-v0.6.14-v0.6.15.md +144 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-corpus-drift-remediation-v0.6.15.md +155 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-version-skip-correction-v0.6.12.md +114 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-author-from-scratch-v0.6.18.md +139 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-feedback37-retraction-v0.6.21.md +124 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-fn1-enrichment-pass-v0.6.19.md +125 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-stale-test-detection-v0.6.20.md +142 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-23-freshness-gate-recovery-v0.6.32.md +97 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-26-catalog-drift-recurring-v0.6.40.md +147 -0
- package/skills/adia-ui-release/assets/templates/stub-changelog.template.md +22 -0
- package/skills/adia-ui-release/evals/evals.json +164 -0
- package/skills/adia-ui-release/references/changelog-discipline.md +250 -0
- package/skills/adia-ui-release/references/cycle-happy-path.md +520 -0
- package/skills/adia-ui-release/references/exe-deploy.md +149 -0
- package/skills/adia-ui-release/references/gates-catalog.md +778 -0
- package/skills/adia-ui-release/references/ledger-discipline.md +232 -0
- package/skills/adia-ui-release/references/migration-guide-authoring.md +174 -0
- package/skills/adia-ui-release/references/multi-agent-baseline.md +207 -0
- package/skills/adia-ui-release/references/notes-authoring.md +212 -0
- package/skills/adia-ui-release/references/recovery-paths.md +215 -0
- package/skills/adia-ui-release/references/rollup-notes.md +208 -0
- package/skills/adia-ui-release/references/teach-protocol.md +468 -0
- package/skills/adia-ui-release/scripts/assert-monorepo-root.mjs +49 -0
- package/skills/adia-ui-release/scripts/audit-gate-roster.mjs +196 -0
- package/skills/adia-ui-release/scripts/bump.mjs +118 -0
- package/skills/adia-ui-release/scripts/dispatch-publish.mjs +151 -0
- package/skills/adia-ui-release/scripts/insert-stub.mjs +127 -0
- package/skills/adia-ui-release/scripts/make-ledger.mjs +179 -0
- package/skills/adia-ui-release/scripts/promote-unreleased.mjs +88 -0
- package/skills/adia-ui-release/scripts/release-pack.mjs +350 -0
- package/skills/adia-ui-release/scripts/tag-lockstep.mjs +110 -0
- 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
|
+
}
|