vibepro 0.1.0-alpha.0

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 (89) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.ja.md +448 -0
  4. package/README.md +520 -0
  5. package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
  6. package/bin/vibepro.js +9 -0
  7. package/docs/assets/vibepro-header.png +0 -0
  8. package/package.json +51 -0
  9. package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
  10. package/skills/vibepro-human-review/SKILL.md +73 -0
  11. package/skills/vibepro-story-refactor/SKILL.md +89 -0
  12. package/skills/vibepro-workflow/SKILL.md +139 -0
  13. package/src/agent-harness-map.js +230 -0
  14. package/src/agent-harness-scanner.js +337 -0
  15. package/src/agent-review.js +2180 -0
  16. package/src/api-boundary-scanner.js +452 -0
  17. package/src/architecture-profiler.js +423 -0
  18. package/src/authorization-scoring.js +149 -0
  19. package/src/brainbase-importer.js +534 -0
  20. package/src/change-risk-classifier.js +195 -0
  21. package/src/check-packs.js +605 -0
  22. package/src/checkpoint-manager.js +233 -0
  23. package/src/cli.js +2213 -0
  24. package/src/code-quality-scanner.js +310 -0
  25. package/src/codex-manager.js +143 -0
  26. package/src/component-style-scanner.js +336 -0
  27. package/src/coverage-report.js +99 -0
  28. package/src/database-access-scanner.js +163 -0
  29. package/src/decision-records.js +315 -0
  30. package/src/design-modernize.js +1435 -0
  31. package/src/design-system.js +1732 -0
  32. package/src/diagnostic-engine.js +1945 -0
  33. package/src/diagram-requirement-resolver.js +194 -0
  34. package/src/doctor.js +677 -0
  35. package/src/environment-graph.js +424 -0
  36. package/src/execution-state.js +849 -0
  37. package/src/explore-evidence.js +425 -0
  38. package/src/flow-design-scanner.js +896 -0
  39. package/src/flow-verifier.js +887 -0
  40. package/src/gesture-interaction-scanner.js +330 -0
  41. package/src/graph-context.js +263 -0
  42. package/src/graphify-adapter.js +189 -0
  43. package/src/html-report.js +1035 -0
  44. package/src/journey-map.js +1299 -0
  45. package/src/language.js +48 -0
  46. package/src/lazy-pattern-detector.js +182 -0
  47. package/src/local-dev-scanner.js +135 -0
  48. package/src/managed-worktree-gate.js +187 -0
  49. package/src/managed-worktree.js +766 -0
  50. package/src/merge-manager.js +501 -0
  51. package/src/network-contract-scanner.js +442 -0
  52. package/src/nocodb-story-sync.js +386 -0
  53. package/src/oss-readiness-scanner.js +417 -0
  54. package/src/performance-evidence.js +756 -0
  55. package/src/performance-measurer.js +591 -0
  56. package/src/pr-manager.js +8220 -0
  57. package/src/presets.js +682 -0
  58. package/src/public-discovery-scanner.js +519 -0
  59. package/src/refactoring-delta-reporter.js +367 -0
  60. package/src/refactoring-opportunity-generator.js +797 -0
  61. package/src/regression-risk-scanner.js +146 -0
  62. package/src/repo-status.js +266 -0
  63. package/src/report-fingerprint.js +188 -0
  64. package/src/report-pr-body-prompt-template.md +108 -0
  65. package/src/report-pr-body-schema.json +95 -0
  66. package/src/report-store.js +135 -0
  67. package/src/report-validator.js +192 -0
  68. package/src/requirement-consistency.js +1066 -0
  69. package/src/runtime-info.js +134 -0
  70. package/src/self-dogfood-scanner.js +476 -0
  71. package/src/session-learning.js +164 -0
  72. package/src/skills-manager.js +157 -0
  73. package/src/spec-drift.js +378 -0
  74. package/src/spec-fingerprint.js +445 -0
  75. package/src/spec-prompt-template.md +155 -0
  76. package/src/spec-schema.json +219 -0
  77. package/src/spec-store.js +258 -0
  78. package/src/spec-validator.js +459 -0
  79. package/src/static-site-scanner.js +316 -0
  80. package/src/story-candidate-generator.js +85 -0
  81. package/src/story-catalog-generator.js +2813 -0
  82. package/src/story-html.js +156 -0
  83. package/src/story-manager.js +2144 -0
  84. package/src/story-task-generator.js +522 -0
  85. package/src/task-manager.js +1029 -0
  86. package/src/terminal-link-scanner.js +238 -0
  87. package/src/usage-report.js +417 -0
  88. package/src/verification-evidence.js +284 -0
  89. package/src/workspace.js +126 -0
@@ -0,0 +1,1435 @@
1
+ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+
5
+ const DEFAULT_SCREEN_ROUTES = ['/'];
6
+ const UI_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
7
+ const IGNORED_DIRS = new Set(['.git', '.next', '.vibepro', 'coverage', 'dist', 'node_modules']);
8
+
9
+ export async function createDesignModernizePlan(repoRoot, options = {}) {
10
+ const root = path.resolve(repoRoot);
11
+ const storyId = options.storyId ?? 'design-modernize';
12
+ const product = options.product ?? inferProductName(root);
13
+ const routes = await resolveDesignRoutes(root, options.routes);
14
+ const bundle = await readDesignSystemBundle(root, options.designSystemBundle);
15
+ const designSystem = normalizeDesignSystemBundle(bundle, {
16
+ designSystemId: options.designSystemId,
17
+ title: options.designSystemTitle ?? product
18
+ });
19
+ const screens = await collectScreens(root, routes, { product, designSystem, baseUrl: options.baseUrl });
20
+ const productSemanticModel = buildProductSemanticModel({
21
+ product,
22
+ brief: options.brief,
23
+ routes,
24
+ screens
25
+ });
26
+ const derivedDesignSystem = buildDerivedDesignSystem({
27
+ product,
28
+ semanticModel: productSemanticModel,
29
+ screens,
30
+ referenceDesignSystem: designSystem
31
+ });
32
+ const designConstraintGraph = buildDesignConstraintGraph(designSystem, screens, derivedDesignSystem);
33
+ const visualHypothesis = buildVisualHypothesisPlan({ storyId, product, screens, designConstraintGraph });
34
+ const plan = {
35
+ schema_version: '0.1.0',
36
+ workflow: 'design-quality-dag',
37
+ story_id: storyId,
38
+ product,
39
+ generated_at: new Date().toISOString(),
40
+ design_intelligence: {
41
+ model: 'vibepro_internal_design_quality_dag',
42
+ reference_sources: [
43
+ 'current_ui_code',
44
+ 'current_screen_capture',
45
+ 'product_information_architecture',
46
+ 'optional_brand_or_design_system_bundle'
47
+ ],
48
+ external_generator_required: false,
49
+ optional_reference: {
50
+ source: options.designSystemId || options.sceneId ? 'external_design_reference_export' : null,
51
+ design_system_id: options.designSystemId ?? designSystem.id ?? null,
52
+ scene_id: options.sceneId ?? null,
53
+ status: options.optionalReferenceStatus ?? 'not_checked',
54
+ note: options.optionalReferenceNote ?? null
55
+ }
56
+ },
57
+ reference_design_system: designSystem,
58
+ visual_foundations_reference: designSystem.visual_foundations ? {
59
+ source: designSystem.visual_foundations.source,
60
+ authority: designSystem.visual_foundations.authority,
61
+ artifact: '.vibepro/design-modernize/<story-id>/visual-foundations-reference.json'
62
+ } : null,
63
+ product_semantic_model: productSemanticModel,
64
+ derived_design_system: derivedDesignSystem,
65
+ component_role_map: derivedDesignSystem.component_role_map,
66
+ composition_guidelines: derivedDesignSystem.composition_guidelines,
67
+ design_constraint_graph: designConstraintGraph,
68
+ visual_hypothesis: visualHypothesis,
69
+ design_quality_dag: buildDesignQualityDag({ storyId, product, screens }),
70
+ screens,
71
+ implementation_plan: buildImplementationPlan(screens),
72
+ spec_gate: buildSpecGate(screens),
73
+ artifacts: {
74
+ current_screen_capture: '.vibepro/design-modernize/<story-id>/screenshots/',
75
+ design_constraint_graph: '.vibepro/design-modernize/<story-id>/design-constraint-graph.json',
76
+ visual_hypothesis_prompts: '.vibepro/design-modernize/<story-id>/visual-hypothesis-prompts.md',
77
+ visual_hypothesis_candidates: '.vibepro/design-modernize/<story-id>/visual-hypotheses/',
78
+ design_system_bundle: '.vibepro/design-modernize/<story-id>/design-system-bundle.json',
79
+ visual_foundations_reference: '.vibepro/design-modernize/<story-id>/visual-foundations-reference.json',
80
+ derived_design_system: '.vibepro/design-modernize/<story-id>/derived-design-system.json',
81
+ product_semantic_model: '.vibepro/design-modernize/<story-id>/product-semantic-model.json',
82
+ component_role_map: '.vibepro/design-modernize/<story-id>/component-role-map.json',
83
+ composition_guidelines: '.vibepro/design-modernize/<story-id>/composition-guidelines.md',
84
+ ds_gate: '.vibepro/design-modernize/<story-id>/ds-gate.json',
85
+ screen_specs: '.vibepro/design-modernize/<story-id>/design-modernize.json',
86
+ design_briefs: '.vibepro/design-modernize/<story-id>/design-briefs.md',
87
+ implementation_spec: '.vibepro/design-modernize/<story-id>/implementation-spec.md'
88
+ }
89
+ };
90
+
91
+ const outDir = path.join(root, '.vibepro', 'design-modernize', storyId);
92
+ await mkdir(outDir, { recursive: true });
93
+ await writeFile(path.join(outDir, 'design-modernize.json'), `${JSON.stringify(plan, null, 2)}\n`);
94
+ await writeFile(path.join(outDir, 'design-modernize.md'), renderDesignModernizePlan(plan));
95
+ await writeFile(path.join(outDir, 'design-briefs.md'), renderDesignBriefs(plan));
96
+ await writeFile(path.join(outDir, 'implementation-spec.md'), renderImplementationSpec(plan));
97
+ await writeFile(path.join(outDir, 'design-constraint-graph.json'), `${JSON.stringify(designConstraintGraph, null, 2)}\n`);
98
+ await writeFile(path.join(outDir, 'visual-hypothesis-prompts.md'), renderVisualHypothesisPrompts(plan));
99
+ await writeDerivedDesignSystemArtifacts(outDir, {
100
+ storyId,
101
+ productSemanticModel,
102
+ derivedDesignSystem
103
+ });
104
+ if (bundle) {
105
+ await writeFile(path.join(outDir, 'design-system-bundle.json'), `${JSON.stringify(bundle, null, 2)}\n`);
106
+ }
107
+ if (designSystem.visual_foundations) {
108
+ await writeFile(path.join(outDir, 'visual-foundations-reference.json'), `${JSON.stringify(designSystem.visual_foundations, null, 2)}\n`);
109
+ }
110
+
111
+ return { outDir, plan };
112
+ }
113
+
114
+ export async function deriveProductDesignSystem(repoRoot, options = {}) {
115
+ const root = path.resolve(repoRoot);
116
+ const storyId = options.storyId ?? 'design-modernize';
117
+ const product = options.product ?? inferProductName(root);
118
+ const routes = await resolveDesignRoutes(root, options.routes);
119
+ const bundle = await readDesignSystemBundle(root, options.designSystemBundle);
120
+ const referenceDesignSystem = normalizeDesignSystemBundle(bundle, {
121
+ designSystemId: options.designSystemId,
122
+ title: options.designSystemTitle ?? product
123
+ });
124
+ const screens = await collectScreens(root, routes, {
125
+ product,
126
+ designSystem: referenceDesignSystem,
127
+ baseUrl: options.baseUrl
128
+ });
129
+ const productSemanticModel = buildProductSemanticModel({
130
+ product,
131
+ brief: options.brief,
132
+ routes,
133
+ screens
134
+ });
135
+ const derivedDesignSystem = buildDerivedDesignSystem({
136
+ product,
137
+ semanticModel: productSemanticModel,
138
+ screens,
139
+ referenceDesignSystem
140
+ });
141
+ const result = {
142
+ schema_version: '0.1.0',
143
+ workflow: 'design-system-derivation',
144
+ story_id: storyId,
145
+ product,
146
+ generated_at: new Date().toISOString(),
147
+ external_generator_required: false,
148
+ authority: 'vibepro_internal_design_constraints',
149
+ product_semantic_model: productSemanticModel,
150
+ derived_design_system: derivedDesignSystem,
151
+ component_role_map: derivedDesignSystem.component_role_map,
152
+ composition_guidelines: derivedDesignSystem.composition_guidelines,
153
+ ds_gate: buildDesignSystemGate({ storyId, derivedDesignSystem })
154
+ };
155
+ const outDir = path.join(root, '.vibepro', 'design-modernize', storyId);
156
+ await mkdir(outDir, { recursive: true });
157
+ await writeDerivedDesignSystemArtifacts(outDir, {
158
+ storyId,
159
+ productSemanticModel,
160
+ derivedDesignSystem
161
+ });
162
+ await writeFile(path.join(outDir, 'design-system-derivation.json'), `${JSON.stringify(result, null, 2)}\n`);
163
+ await writeFile(path.join(outDir, 'design-system-derivation.md'), renderDerivedDesignSystemSummary(result));
164
+ return { outDir, result };
165
+ }
166
+
167
+ export async function captureDesignModernizeScreens(repoRoot, options = {}) {
168
+ const root = path.resolve(repoRoot);
169
+ const storyId = options.storyId ?? 'design-modernize';
170
+ const outDir = path.join(root, '.vibepro', 'design-modernize', storyId);
171
+ const screenshotDir = path.join(outDir, 'screenshots');
172
+ await mkdir(screenshotDir, { recursive: true });
173
+ const plan = await readPlan(outDir);
174
+ const routes = options.routes?.length > 0
175
+ ? options.routes
176
+ : plan?.screens?.map((screen) => screen.route) ?? await resolveDesignRoutes(root, []);
177
+ const plannedUrl = plan?.screens?.[0]?.capture?.url;
178
+ const baseUrl = options.baseUrl ?? (/^https?:\/\//.test(plannedUrl ?? '') ? plannedUrl : null);
179
+ const result = {
180
+ schema_version: '0.1.0',
181
+ workflow: 'design-modernize-capture',
182
+ story_id: storyId,
183
+ generated_at: new Date().toISOString(),
184
+ status: 'needs_setup',
185
+ base_url: baseUrl ?? null,
186
+ screenshots: [],
187
+ setup: {
188
+ next_commands: []
189
+ }
190
+ };
191
+
192
+ if (!baseUrl || !/^https?:\/\//.test(baseUrl)) {
193
+ result.setup.next_commands.push('Run the target app and pass --base-url http://localhost:<port>');
194
+ await writeCaptureResult(outDir, result);
195
+ return { outDir, result };
196
+ }
197
+
198
+ const playwright = await loadPlaywright(root);
199
+ if (!playwright) {
200
+ result.setup.next_commands.push('npm install -D @playwright/test');
201
+ result.setup.next_commands.push('npx playwright install chromium');
202
+ await writeCaptureResult(outDir, result);
203
+ return { outDir, result };
204
+ }
205
+
206
+ const browser = await playwright.chromium.launch({ headless: true });
207
+ try {
208
+ const context = await browser.newContext({
209
+ viewport: { width: 390, height: 844 },
210
+ deviceScaleFactor: 2,
211
+ isMobile: true
212
+ });
213
+ const page = await context.newPage();
214
+ result.status = 'pass';
215
+ for (const route of routes) {
216
+ const url = new URL(route.replace(/\[hotel_id\]/g, options.sampleHotelId ?? 'sample'), baseUrl).toString();
217
+ const fileName = `${routeToKey(route).toLowerCase()}.png`;
218
+ const filePath = path.join(screenshotDir, fileName);
219
+ try {
220
+ await page.goto(url, { waitUntil: 'networkidle', timeout: options.timeoutMs ?? 30000 });
221
+ await page.screenshot({ path: filePath, fullPage: true });
222
+ result.screenshots.push({
223
+ route,
224
+ url,
225
+ status: 'pass',
226
+ artifact: path.relative(root, filePath).split(path.sep).join('/')
227
+ });
228
+ } catch (error) {
229
+ result.status = 'fail';
230
+ result.screenshots.push({
231
+ route,
232
+ url,
233
+ status: 'fail',
234
+ error: error.message
235
+ });
236
+ }
237
+ }
238
+ await context.close();
239
+ } finally {
240
+ await browser.close();
241
+ }
242
+ await writeCaptureResult(outDir, result);
243
+ return { outDir, result };
244
+ }
245
+
246
+ export function normalizeDesignSystemBundle(bundle, options = {}) {
247
+ const source = bundle && typeof bundle === 'object' ? bundle : {};
248
+ const payload = source.bundle && typeof source.bundle === 'object' ? source.bundle : source;
249
+ const tokens = payload.tokens
250
+ ?? payload.designTokens
251
+ ?? payload.files?.tokens
252
+ ?? source.semantic_tokens
253
+ ?? source.theme_tokens
254
+ ?? [payload.theme, payload.styles].filter(Boolean).join('\n')
255
+ ?? {};
256
+ const components = payload.components
257
+ ?? source.files?.components
258
+ ?? source.component_roles?.roles
259
+ ?? source.component_roles
260
+ ?? [payload.componentsCss, payload.componentsJs].filter(Boolean).join('\n')
261
+ ?? [];
262
+ const guidelines = payload.guidelines
263
+ ?? source.files?.guidelines
264
+ ?? source.overview
265
+ ?? payload.documentation
266
+ ?? [];
267
+ return {
268
+ id: source.id ?? source.designSystemId ?? source.designSystem?.id ?? options.designSystemId ?? null,
269
+ title: source.title ?? source.name ?? source.designSystem?.title ?? options.title ?? null,
270
+ version: source.version?.versionNumber ?? source.version ?? source.latestVersion ?? source.publishedVersion ?? null,
271
+ status: bundle ? 'available' : 'missing_bundle',
272
+ token_summary: summarizeTokens(tokens),
273
+ component_summary: summarizeComponents(components),
274
+ guideline_summary: summarizeGuidelines(guidelines),
275
+ visual_foundations: source.visual_foundations ?? payload.visual_foundations ?? null,
276
+ constraints: buildDesignConstraints({ tokens, components, guidelines })
277
+ };
278
+ }
279
+
280
+ export function renderDesignModernizePlan(plan) {
281
+ return `# Design Modernize Plan
282
+
283
+ | Item | Value |
284
+ |------|-------|
285
+ | Story | ${plan.story_id} |
286
+ | Product | ${plan.product} |
287
+ | Design Intelligence | ${plan.design_intelligence.model} |
288
+ | External generator required | ${plan.design_intelligence.external_generator_required} |
289
+ | Reference Design System | ${plan.reference_design_system.title ?? '-'} (${plan.reference_design_system.id ?? '-'}) |
290
+ | Visual Foundations | ${plan.visual_foundations_reference?.source ?? '-'} |
291
+
292
+ ## Workflow
293
+
294
+ 1. Graphify/Codex extract routes, components, state, CTA, data dependency, and preserved UX from current code.
295
+ 2. Capture current browser screenshots for each route before asking for visual redesign.
296
+ 3. Convert optional brand/design-system material into VibePro design constraints.
297
+ - Visual foundations are reference material only; current code, graph evidence, implementation mapping, and gates remain authoritative.
298
+ 4. Generate one screen-level design brief per route with invariants, allowed visual changes, anti-patterns, rubric, and Codex acceptance criteria.
299
+ 5. Use VibePro's Design Quality DAG to review hierarchy, density, CTA priority, state clarity, accessibility, interaction continuity, and implementation fit.
300
+ 6. Implement with Codex using this spec, Graphify evidence, current screenshots, and current code as the source of truth.
301
+
302
+ ## Screens
303
+
304
+ ${plan.screens.map((screen) => `### ${screen.route}
305
+
306
+ - Files: ${screen.evidence.files.map((file) => file.path).join(', ') || '-'}
307
+ - Preserve: ${screen.invariants.map((item) => item.id).join(', ')}
308
+ - Design brief: ${screen.design_brief.title}
309
+ `).join('\n')}
310
+
311
+ ## Spec Gate
312
+
313
+ ${plan.spec_gate.checks.map((check) => `- ${check.id}: ${check.statement}`).join('\n')}
314
+ `;
315
+ }
316
+
317
+ export function renderDesignBriefs(plan) {
318
+ return plan.screens.map((screen) => `## ${screen.route}
319
+
320
+ ${screen.design_brief.body}
321
+ `).join('\n');
322
+ }
323
+
324
+ export function renderImplementationSpec(plan) {
325
+ const clauses = plan.spec_gate.checks.map((check) => `- ${check.id}: ${check.statement}`).join('\n');
326
+ return `# ${plan.story_id} Implementation Spec
327
+
328
+ ## Invariants
329
+
330
+ ${plan.screens.flatMap((screen) => screen.invariants.map((item) => `- ${item.id}: ${item.statement}`)).join('\n')}
331
+
332
+ ## Contracts
333
+
334
+ ${plan.screens.flatMap((screen) => screen.contracts.map((item) => `- ${item.id}: ${item.statement}`)).join('\n')}
335
+
336
+ ## Scenarios
337
+
338
+ ${plan.screens.flatMap((screen) => screen.scenarios.map((item) => `- ${item.id}: ${item.statement}`)).join('\n')}
339
+
340
+ ## Anti-patterns
341
+
342
+ ${plan.screens.flatMap((screen) => screen.anti_patterns.map((item) => `- ${item.id}: ${item.statement}`)).join('\n')}
343
+
344
+ ## Verification
345
+
346
+ ${clauses}
347
+ `;
348
+ }
349
+
350
+ export function renderVisualHypothesisPrompts(plan) {
351
+ return `# ${plan.story_id} Visual Hypothesis Prompts
352
+
353
+ Image generation is optional evidence for visual exploration. Generated images are not implementation authority.
354
+
355
+ ${plan.visual_hypothesis.screens.map((screen) => `## ${screen.route}
356
+
357
+ ${screen.prompt}
358
+
359
+ ### Gate
360
+
361
+ ${screen.gate_checks.map((check) => `- ${check}`).join('\n')}
362
+ `).join('\n')}`;
363
+ }
364
+
365
+ export function renderDerivedDesignSystemSummary(result) {
366
+ const ds = result.derived_design_system;
367
+ return `# ${result.story_id} Derived Design System
368
+
369
+ VibePro derived this design system from product brief, current UI evidence, and route-level invariants. It is an internal constraint model, not an external generator output.
370
+
371
+ ## Identity
372
+
373
+ - Product: ${result.product}
374
+ - Design language: ${ds.identity.design_language}
375
+ - Interaction model: ${ds.identity.interaction_model}
376
+ - Authority: ${result.authority}
377
+
378
+ ## Semantic Color Roles
379
+
380
+ ${ds.semantic_tokens.color_roles.map((role) => `- ${role.name}: ${role.purpose}`).join('\n')}
381
+
382
+ ## Component Roles
383
+
384
+ ${ds.component_role_map.roles.map((role) => `- ${role.name}: ${role.responsibility}`).join('\n')}
385
+
386
+ ## Composition Rules
387
+
388
+ ${ds.composition_guidelines.rules.map((rule) => `- ${rule.id}: ${rule.statement}`).join('\n')}
389
+
390
+ ## Anti-patterns
391
+
392
+ ${ds.anti_patterns.map((item) => `- ${item.id}: ${item.statement}`).join('\n')}
393
+
394
+ ## Gate
395
+
396
+ ${buildDesignSystemGate({ storyId: result.story_id, derivedDesignSystem: ds }).checks.map((check) => `- ${check.id}: ${check.statement}`).join('\n')}
397
+ `;
398
+ }
399
+
400
+ export function renderCaptureSummary({ outDir, result }) {
401
+ return `# Design Modernize Capture
402
+
403
+ | Item | Value |
404
+ |------|-------|
405
+ | Story | ${result.story_id} |
406
+ | Status | ${result.status} |
407
+ | Base URL | ${result.base_url ?? '-'} |
408
+ | Output | ${outDir} |
409
+
410
+ ## Screenshots
411
+
412
+ ${result.screenshots.length === 0 ? '- なし' : result.screenshots.map((item) => `- ${item.route}: ${item.status} ${item.artifact ?? item.error ?? ''}`.trim()).join('\n')}
413
+
414
+ ## Setup
415
+
416
+ ${result.setup.next_commands.length === 0 ? '- なし' : result.setup.next_commands.map((command) => `- ${command}`).join('\n')}
417
+ `;
418
+ }
419
+
420
+ async function readDesignSystemBundle(repoRoot, bundlePath) {
421
+ if (!bundlePath) return null;
422
+ const absolutePath = path.isAbsolute(bundlePath) ? bundlePath : path.join(repoRoot, bundlePath);
423
+ return JSON.parse(await readFile(absolutePath, 'utf8'));
424
+ }
425
+
426
+ async function writeDerivedDesignSystemArtifacts(outDir, { storyId, productSemanticModel, derivedDesignSystem }) {
427
+ await writeFile(path.join(outDir, 'product-semantic-model.json'), `${JSON.stringify(productSemanticModel, null, 2)}\n`);
428
+ await writeFile(path.join(outDir, 'derived-design-system.json'), `${JSON.stringify(derivedDesignSystem, null, 2)}\n`);
429
+ await writeFile(path.join(outDir, 'component-role-map.json'), `${JSON.stringify(derivedDesignSystem.component_role_map, null, 2)}\n`);
430
+ await writeFile(path.join(outDir, 'ds-gate.json'), `${JSON.stringify(buildDesignSystemGate({ storyId, derivedDesignSystem }), null, 2)}\n`);
431
+ await writeFile(path.join(outDir, 'composition-guidelines.md'), renderCompositionGuidelines(derivedDesignSystem));
432
+ }
433
+
434
+ function renderCompositionGuidelines(derivedDesignSystem) {
435
+ return `# Composition Guidelines
436
+
437
+ ${derivedDesignSystem.composition_guidelines.rules.map((rule) => `## ${rule.id}
438
+
439
+ ${rule.statement}
440
+ `).join('\n')}
441
+ ## Color Discipline
442
+
443
+ ${derivedDesignSystem.semantic_tokens.color_roles.map((role) => `- ${role.name}: ${role.purpose}`).join('\n')}
444
+
445
+ ## CTA Hierarchy
446
+
447
+ ${derivedDesignSystem.cta_hierarchy.map((item, index) => `${index + 1}. ${item}`).join('\n')}
448
+ `;
449
+ }
450
+
451
+ async function readPlan(outDir) {
452
+ try {
453
+ return JSON.parse(await readFile(path.join(outDir, 'design-modernize.json'), 'utf8'));
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+
459
+ async function writeCaptureResult(outDir, result) {
460
+ await writeFile(path.join(outDir, 'screen-capture.json'), `${JSON.stringify(result, null, 2)}\n`);
461
+ await writeFile(path.join(outDir, 'screen-capture.md'), renderCaptureSummary({ outDir, result }));
462
+ }
463
+
464
+ async function loadPlaywright(repoRoot) {
465
+ try {
466
+ const requireFromRepo = createRequire(path.join(repoRoot, 'package.json'));
467
+ return requireFromRepo('playwright');
468
+ } catch {
469
+ try {
470
+ const requireFromRepo = createRequire(path.join(repoRoot, 'package.json'));
471
+ return requireFromRepo('@playwright/test');
472
+ } catch {
473
+ return null;
474
+ }
475
+ }
476
+ }
477
+
478
+ export async function collectScreens(repoRoot, routes, { product, designSystem, baseUrl }) {
479
+ const screens = [];
480
+ for (const route of routes) {
481
+ const evidence = await collectScreenEvidence(repoRoot, route);
482
+ screens.push(buildScreenSpec({
483
+ product,
484
+ route,
485
+ evidence,
486
+ designSystem,
487
+ baseUrl
488
+ }));
489
+ }
490
+ return screens;
491
+ }
492
+
493
+ export async function resolveDesignRoutes(repoRoot, routes = []) {
494
+ if (Array.isArray(routes) && routes.length > 0) return routes;
495
+ return discoverDesignRoutes(repoRoot);
496
+ }
497
+
498
+ export async function discoverDesignRoutes(repoRoot) {
499
+ const roots = [
500
+ { dir: path.join(repoRoot, 'src', 'app'), kind: 'app' },
501
+ { dir: path.join(repoRoot, 'app'), kind: 'app' },
502
+ { dir: path.join(repoRoot, 'src', 'pages'), kind: 'pages' },
503
+ { dir: path.join(repoRoot, 'pages'), kind: 'pages' }
504
+ ];
505
+ const routes = [];
506
+ for (const root of roots) {
507
+ if (!await exists(root.dir)) continue;
508
+ const files = await listUiFiles(repoRoot, root.dir);
509
+ for (const file of files) {
510
+ const route = root.kind === 'app'
511
+ ? routeFromAppFile(file, root.dir, repoRoot)
512
+ : routeFromPagesFile(file, root.dir, repoRoot);
513
+ if (route) routes.push(route);
514
+ }
515
+ }
516
+ const discovered = unique(routes).sort((a, b) => a.localeCompare(b));
517
+ return discovered.length > 0 ? discovered.slice(0, 20) : DEFAULT_SCREEN_ROUTES;
518
+ }
519
+
520
+ export function buildProductSemanticModel({ product, brief, routes, screens }) {
521
+ const rawText = [
522
+ product,
523
+ brief,
524
+ routes.join(' '),
525
+ ...screens.flatMap((screen) => [
526
+ screen.route,
527
+ ...screen.evidence.files.flatMap((file) => [
528
+ ...file.components,
529
+ ...file.ctas,
530
+ ...file.states,
531
+ ...file.navigation,
532
+ ...file.data_dependencies
533
+ ])
534
+ ])
535
+ ].join('\n');
536
+ const text = rawText.toLowerCase();
537
+ const positiveText = stripNegatedDomainEvidence(rawText).toLowerCase();
538
+ const currentCtas = unique(screens.flatMap((screen) => screen.evidence.files.flatMap((file) => file.ctas))).slice(0, 20);
539
+ const routeIntents = screens.map((screen) => ({
540
+ route: screen.route,
541
+ intent: inferScreenIntent(screen.route),
542
+ current_ctas: unique(screen.evidence.files.flatMap((file) => file.ctas)).slice(0, 12),
543
+ current_navigation: unique(screen.evidence.files.flatMap((file) => file.navigation)).slice(0, 12)
544
+ }));
545
+ const isJapanese = /日本|japanese|渋谷|新宿|休憩|宿泊|地図|検索|電話/.test(text);
546
+ const hotelDiscovery = hasHotelDiscoveryEvidence(positiveText);
547
+ const aiPhone = /ai電話|ai phone|phone confirmation|空室確認|電話/.test(positiveText);
548
+ return {
549
+ schema_version: '0.1.0',
550
+ product,
551
+ brief: brief ?? null,
552
+ primary_domain: hotelDiscovery ? 'hotel_discovery' : 'product_workflow',
553
+ language_policy: isJapanese ? 'japanese_ui_first' : 'preserve_current_product_language',
554
+ interaction_model: aiPhone ? 'discovery_to_ai_phone_confirmation' : 'preserve_current_primary_action_model',
555
+ domain_concepts: unique([
556
+ ...(/current|現在地|location|map|地図/.test(positiveText) && hotelDiscovery ? ['location_search', 'map_exploration'] : []),
557
+ ...(/condition|filter|条件|絞り/.test(text) ? ['condition_search', 'filter_refinement'] : []),
558
+ ...(/休憩/.test(positiveText) ? ['plan_rest'] : []),
559
+ ...(/宿泊|stay/.test(positiveText) && hotelDiscovery ? ['plan_stay'] : []),
560
+ ...(/サービスタイム|service/.test(positiveText) && hotelDiscovery ? ['plan_service_time'] : []),
561
+ ...(/今すぐ|now|空室/.test(positiveText) && hotelDiscovery ? ['plan_now', 'availability'] : []),
562
+ ...(/price|価格|¥|円/.test(text) ? ['price'] : []),
563
+ ...(/distance|距離|徒歩|km|m/.test(text) && hotelDiscovery ? ['distance'] : []),
564
+ ...(/facility|設備|wi-fi|駐車場/.test(text) ? ['facility'] : []),
565
+ ...(/user posts|投稿|口コミ/.test(text) ? ['user_posts'] : [])
566
+ ]),
567
+ route_intents: routeIntents,
568
+ native_ctas: currentCtas,
569
+ forbidden_patterns: unique([
570
+ 'net_new_app_concept',
571
+ 'navigation_rewrite_without_evidence',
572
+ 'invented_backend_data',
573
+ 'marketing_landing_page',
574
+ ...(/book now|booking|予約/.test(text) || aiPhone ? ['generic_book_now_cta'] : [])
575
+ ])
576
+ };
577
+ }
578
+
579
+ export function buildDerivedDesignSystem({ product, semanticModel, screens, referenceDesignSystem }) {
580
+ const routeIntents = semanticModel.route_intents.map((item) => item.intent);
581
+ const componentSamples = unique([
582
+ ...screens.flatMap((screen) => screen.evidence.files.flatMap((file) => file.components)),
583
+ ...(referenceDesignSystem.component_summary?.names ?? [])
584
+ ]);
585
+ const colorRoles = buildSemanticColorRoles(semanticModel);
586
+ const componentRoleMap = buildComponentRoleMap({ semanticModel, componentSamples, routeIntents });
587
+ return {
588
+ schema_version: '0.1.0',
589
+ source: 'vibepro_derived_from_product_evidence',
590
+ authority: 'internal_design_constraints',
591
+ identity: {
592
+ product,
593
+ design_language: inferDesignLanguage(semanticModel),
594
+ interaction_model: semanticModel.interaction_model,
595
+ language_policy: semanticModel.language_policy
596
+ },
597
+ foundations: {
598
+ theme_order: ['color_ramps', 'typography', 'spacing', 'radii', 'motion', 'shadows'],
599
+ token_dependency_order: ['raw_theme', 'semantic_tokens', 'recipes', 'component_roles', 'composition_rules'],
600
+ density_policy: semanticModel.primary_domain === 'hotel_discovery' ? 'mobile_dense_scannable' : 'preserve_current_density',
601
+ typography_policy: semanticModel.language_policy === 'japanese_ui_first'
602
+ ? 'compact_japanese_mobile_scale_with_tabular_numerals'
603
+ : 'preserve_current_readability_scale',
604
+ motion_policy: ['snappy_utility_feedback', 'spatial_context_for_sheets_and_overlays', 'respect_reduced_motion']
605
+ },
606
+ semantic_tokens: {
607
+ color_roles: colorRoles,
608
+ state_semantics: ['loading', 'empty', 'error', 'selected', 'disabled', 'success', 'available', 'limited', 'unavailable'],
609
+ cta_priority: ['primary_domain_action', 'route_navigation', 'filter_refinement', 'secondary_reference'],
610
+ domain_semantics: semanticModel.domain_concepts
611
+ },
612
+ component_role_map: componentRoleMap,
613
+ composition_guidelines: buildCompositionGuidelines(semanticModel),
614
+ cta_hierarchy: buildCtaHierarchy(semanticModel),
615
+ anti_patterns: semanticModel.forbidden_patterns.map((pattern, index) => ({
616
+ id: `DS-AP-${index + 1}`,
617
+ statement: antiPatternStatement(pattern)
618
+ })),
619
+ visual_hypothesis_policy: {
620
+ image_generation_role: 'explore_candidate_visual_directions_only',
621
+ candidates_per_screen: { min: 2, max: 4 },
622
+ required_candidate_notes: [
623
+ 'preserved UX',
624
+ 'design moves',
625
+ 'risky or rejected moves',
626
+ 'implementation notes',
627
+ 'DS drift risks'
628
+ ],
629
+ implementation_authority: 'VibePro spec, current code, screenshots, and gate evidence'
630
+ }
631
+ };
632
+ }
633
+
634
+ function buildSemanticColorRoles(semanticModel) {
635
+ const roles = [
636
+ { name: 'surface_base', purpose: 'primary app background and depth foundation' },
637
+ { name: 'surface_raised', purpose: 'cards, sheets, and grouped controls' },
638
+ { name: 'text_primary', purpose: 'primary readable content' },
639
+ { name: 'text_muted', purpose: 'secondary metadata and disabled context' },
640
+ { name: 'brand_interactive', purpose: 'primary interaction and selected state, not decoration' }
641
+ ];
642
+ if (semanticModel.primary_domain === 'hotel_discovery') {
643
+ roles.push(
644
+ { name: 'availability_positive', purpose: 'available or confirmed state' },
645
+ { name: 'geo_distance', purpose: 'location, distance, and map exploration cues' },
646
+ { name: 'urgency_caution', purpose: 'limited availability, now intent, price attention, and caution' },
647
+ { name: 'plan_rest', purpose: '休憩 plan identity, consistent across selector badge card and pin' },
648
+ { name: 'plan_stay', purpose: '宿泊 plan identity, consistent across selector badge card and pin' },
649
+ { name: 'plan_service_time', purpose: 'サービスタイム plan identity, consistent across selector badge card and pin' },
650
+ { name: 'plan_now', purpose: '今すぐ plan identity, consistent across selector badge card and pin' }
651
+ );
652
+ }
653
+ return roles;
654
+ }
655
+
656
+ function buildComponentRoleMap({ semanticModel, componentSamples, routeIntents }) {
657
+ const names = componentSamples.map(String);
658
+ const defaults = semanticModel.primary_domain === 'hotel_discovery'
659
+ ? [
660
+ ['SearchBar', 'top-level discovery entry point; never buried inside cards'],
661
+ ['SegmentedSearchMode', 'switches search approach without changing plan semantics'],
662
+ ['PlanTypeSelector', 'selects domain plan views; not a generic filter substitute'],
663
+ ['FilterChip', 'fast condition refinement in horizontal rows'],
664
+ ['HotelCard', 'full result card for browsing feeds'],
665
+ ['CompactHotelCard', 'dense result card for bottom sheets and long lists'],
666
+ ['MapPricePin', 'only result marker on map surfaces'],
667
+ ['BottomSheet', 'map result container and contextual mobile overlay'],
668
+ ['BottomNavigation', 'primary mobile navigation anchor'],
669
+ ['PageHeader', 'route title, back action, and scoped actions'],
670
+ ['AIPhoneCTA', 'primary domain action after the user focuses on a hotel'],
671
+ ['FacilityBadge', 'quiet amenity metadata'],
672
+ ['AvailabilityBadge', 'semantic availability status'],
673
+ ['PlanBadge', 'domain plan identity marker']
674
+ ]
675
+ : [
676
+ ...inferGenericComponentRoleDefaults(names),
677
+ ['PrimaryAction', 'highest-priority domain action'],
678
+ ['FilterControl', 'condition refinement without route rewrite'],
679
+ ['ResultCard', 'repeatable entity summary'],
680
+ ['StatusBadge', 'state or status indicator'],
681
+ ['NavigationShell', 'stable route navigation']
682
+ ];
683
+ const roles = uniqueRoleDefaults(defaults).map(([name, responsibility]) => ({
684
+ name,
685
+ responsibility,
686
+ evidence: names.filter((sample) => sample.toLowerCase().includes(name.toLowerCase())).slice(0, 8),
687
+ required_for_intents: routeIntents.filter(Boolean).slice(0, 8)
688
+ }));
689
+ return {
690
+ schema_version: '0.1.0',
691
+ roles,
692
+ consistency_rules: [
693
+ 'same component role must carry the same semantic color meaning across screens',
694
+ 'dense and full-size variants must not be mixed in one homogeneous list',
695
+ 'primary domain action must not be replaced by generic conversion language',
696
+ 'component role changes require matching route-level regression evidence'
697
+ ]
698
+ };
699
+ }
700
+
701
+ function routeFromAppFile(file, appRoot, repoRoot) {
702
+ if (!/(^|\/)page\.[cm]?[jt]sx?$/.test(file)) return null;
703
+ const relativeRoot = path.relative(repoRoot, appRoot).split(path.sep).join('/');
704
+ const relative = file.slice(relativeRoot.length).replace(/^\//, '');
705
+ const segments = relative.split('/').slice(0, -1)
706
+ .filter((segment) => segment && !segment.startsWith('(') && !segment.startsWith('@'));
707
+ return `/${segments.join('/')}`.replace(/\/$/, '') || '/';
708
+ }
709
+
710
+ function routeFromPagesFile(file, pagesRoot, repoRoot) {
711
+ if (!/\.[cm]?[jt]sx?$/.test(file)) return null;
712
+ const relativeRoot = path.relative(repoRoot, pagesRoot).split(path.sep).join('/');
713
+ const relative = file.slice(relativeRoot.length).replace(/^\//, '');
714
+ if (relative.startsWith('api/') || /^_(app|document|error)\./.test(relative)) return null;
715
+ const withoutExt = relative.replace(/\.[cm]?[jt]sx?$/, '');
716
+ const segments = withoutExt.split('/').filter(Boolean);
717
+ if (segments.at(-1) === 'index') segments.pop();
718
+ return `/${segments.join('/')}`.replace(/\/$/, '') || '/';
719
+ }
720
+
721
+ function stripNegatedDomainEvidence(text) {
722
+ return String(text ?? '')
723
+ .split(/[\n.。!?!?;;]+/)
724
+ .filter((segment) => {
725
+ const value = segment.toLowerCase();
726
+ const hasDomainTerm = /hotel|ホテル|宿泊|休憩|stay|map|地図|空室|電話|booking|予約/.test(value);
727
+ if (!hasDomainTerm) return true;
728
+ return !/(do\s+not|don't|avoid|without|禁止|避け|使わない|使用しない|しない|ではない|not\s+a|not\s+an|no\s+)/i.test(segment);
729
+ })
730
+ .join('\n');
731
+ }
732
+
733
+ function hasHotelDiscoveryEvidence(text) {
734
+ const value = String(text ?? '').toLowerCase();
735
+ return /hotel|ホテル|宿泊|休憩|旅館|ラブホテル|空室|空室確認|ai電話|サービスタイム/.test(value);
736
+ }
737
+
738
+ function inferGenericComponentRoleDefaults(names) {
739
+ return unique(names)
740
+ .filter((name) => /^[A-Z][A-Za-z0-9_]*$/.test(name))
741
+ .map((name) => [name, inferGenericComponentResponsibility(name)])
742
+ .slice(0, 16);
743
+ }
744
+
745
+ function inferGenericComponentResponsibility(name) {
746
+ const text = String(name ?? '').toLowerCase();
747
+ if (/shell|layout|nav|sidebar|menu/.test(text)) return 'application navigation and layout surface';
748
+ if (/header|toolbar|topbar/.test(text)) return 'page-level orientation and action grouping';
749
+ if (/project|company|product|template|customer|account|user/.test(text) && /list|table|grid|card/.test(text)) return 'repeatable business entity summary';
750
+ if (/list|table|grid/.test(text)) return 'repeatable information management surface';
751
+ if (/form|editor|create|settings/.test(text)) return 'structured input and configuration workflow';
752
+ if (/dialog|modal|drawer|sheet/.test(text)) return 'focused decision or detail surface';
753
+ if (/badge|status|pill|tag/.test(text)) return 'state or classification indicator';
754
+ if (/filter|search|segment|tab/.test(text)) return 'finding and narrowing control';
755
+ if (/button|cta|action|submit/.test(text)) return 'explicit command surface';
756
+ return 'product-local component role discovered from current code';
757
+ }
758
+
759
+ function uniqueRoleDefaults(defaults) {
760
+ const byName = new Map();
761
+ for (const item of defaults) {
762
+ const [name, responsibility] = item;
763
+ if (!name || byName.has(name)) continue;
764
+ byName.set(name, [name, responsibility]);
765
+ }
766
+ return [...byName.values()];
767
+ }
768
+
769
+ function buildCompositionGuidelines(semanticModel) {
770
+ const hotelRules = [
771
+ {
772
+ id: 'DS-COMP-1',
773
+ statement: 'Search flows keep a stable vertical hierarchy: search entry, search mode, plan intent, filters, then results.'
774
+ },
775
+ {
776
+ id: 'DS-COMP-2',
777
+ statement: 'Map screens show results through map pins and a bottom sheet; avoid floating result cards directly on the map.'
778
+ },
779
+ {
780
+ id: 'DS-COMP-3',
781
+ statement: 'Plan identity appears before availability, and facility metadata stays visually quieter than plan or availability signals.'
782
+ },
783
+ {
784
+ id: 'DS-COMP-4',
785
+ statement: 'AI phone confirmation appears only after a user has focused on a hotel or result, not as a generic search CTA.'
786
+ },
787
+ {
788
+ id: 'DS-COMP-5',
789
+ statement: 'Prices use yen prefix and aligned numerals; avoid crossed-out discounts or deal-app patterns.'
790
+ }
791
+ ];
792
+ const genericRules = [
793
+ {
794
+ id: 'DS-COMP-1',
795
+ statement: 'Preserve current route purpose, CTA order, and navigation anchors while improving visual hierarchy.'
796
+ },
797
+ {
798
+ id: 'DS-COMP-2',
799
+ statement: 'Use repeated component roles consistently instead of one-off visual treatments.'
800
+ },
801
+ {
802
+ id: 'DS-COMP-3',
803
+ statement: 'State, status, and action colors must be semantic and stable across screens.'
804
+ }
805
+ ];
806
+ return {
807
+ schema_version: '0.1.0',
808
+ rules: semanticModel.primary_domain === 'hotel_discovery' ? hotelRules : genericRules
809
+ };
810
+ }
811
+
812
+ function buildCtaHierarchy(semanticModel) {
813
+ if (semanticModel.interaction_model === 'discovery_to_ai_phone_confirmation') {
814
+ return ['AI電話で空室確認', '現在地から探す', 'このエリアで検索', '条件で絞り込む', '地図で見る', '公式サイト', '行きたい'];
815
+ }
816
+ return unique(['primary domain action', ...semanticModel.native_ctas]).slice(0, 10);
817
+ }
818
+
819
+ function inferDesignLanguage(semanticModel) {
820
+ if (semanticModel.primary_domain === 'hotel_discovery') return 'premium_utility_travel';
821
+ return 'product_local_utility';
822
+ }
823
+
824
+ function antiPatternStatement(pattern) {
825
+ const statements = {
826
+ net_new_app_concept: 'Do not turn modernization into a new app concept or unrelated product direction.',
827
+ navigation_rewrite_without_evidence: 'Do not rewrite navigation structure unless Graphify/Codex evidence proves the current route contract changed.',
828
+ invented_backend_data: 'Do not invent backend data, domain entities, or unavailable states.',
829
+ marketing_landing_page: 'Do not collapse operational product screens into marketing or landing-page composition.',
830
+ generic_book_now_cta: 'Do not replace product-native confirmation actions with generic Book Now or booking-funnel CTAs.'
831
+ };
832
+ return statements[pattern] ?? `Avoid ${pattern}.`;
833
+ }
834
+
835
+ export function buildDesignSystemGate({ storyId, derivedDesignSystem }) {
836
+ return {
837
+ schema_version: '0.1.0',
838
+ story_id: storyId,
839
+ mode: 'explicit',
840
+ fallback_allowed: false,
841
+ checks: [
842
+ {
843
+ id: 'DS-GATE-IDENTITY',
844
+ statement: 'Derived design system includes product identity, interaction model, language policy, and forbidden patterns.'
845
+ },
846
+ {
847
+ id: 'DS-GATE-SEMANTICS',
848
+ statement: 'Semantic tokens cover surface, text, brand/interactive, state colors, CTA priority, density, motion, and domain semantics.'
849
+ },
850
+ {
851
+ id: 'DS-GATE-COMPONENT-ROLES',
852
+ statement: 'Component roles define responsibility and usage constraints, not only visual component names.'
853
+ },
854
+ {
855
+ id: 'DS-GATE-COMPOSITION',
856
+ statement: 'Composition guidelines preserve route hierarchy, navigation, card/list usage, badge order, and primary CTA placement.'
857
+ },
858
+ {
859
+ id: 'DS-GATE-VISUAL-HYPOTHESIS',
860
+ statement: 'Image generation is treated as candidate evidence with critique notes, never as implementation authority.'
861
+ },
862
+ {
863
+ id: 'DS-GATE-ANTI-PATTERN',
864
+ statement: `Anti-pattern coverage is explicit (${derivedDesignSystem.anti_patterns.map((item) => item.id).join(', ')}).`
865
+ }
866
+ ]
867
+ };
868
+ }
869
+
870
+ async function collectScreenEvidence(repoRoot, route) {
871
+ const files = await resolveRouteFiles(repoRoot, route);
872
+ const fileReports = [];
873
+ for (const file of files.slice(0, 24)) {
874
+ const content = await readFile(path.join(repoRoot, file), 'utf8');
875
+ fileReports.push({
876
+ path: file,
877
+ routes: [route],
878
+ components: collectComponentNames(content),
879
+ states: collectStateNames(content),
880
+ ctas: collectCtas(content),
881
+ data_dependencies: collectDataDependencies(content),
882
+ navigation: collectNavigationTargets(content)
883
+ });
884
+ }
885
+ return {
886
+ route,
887
+ files: fileReports,
888
+ summary: {
889
+ file_count: fileReports.length,
890
+ component_count: unique(fileReports.flatMap((file) => file.components)).length,
891
+ state_count: unique(fileReports.flatMap((file) => file.states)).length,
892
+ cta_count: unique(fileReports.flatMap((file) => file.ctas)).length,
893
+ data_dependency_count: unique(fileReports.flatMap((file) => file.data_dependencies)).length
894
+ }
895
+ };
896
+ }
897
+
898
+ async function resolveRouteFiles(repoRoot, route) {
899
+ const routeParts = route.replace(/^\//, '').split('/').filter(Boolean);
900
+ const candidates = [
901
+ path.join('src', 'app', '(app)', ...routeParts),
902
+ path.join('src', 'app', '(public)', ...routeParts),
903
+ path.join('src', 'app', ...routeParts),
904
+ path.join('app', ...routeParts),
905
+ path.join('pages', ...routeParts)
906
+ ];
907
+ const files = [];
908
+ for (const candidate of candidates) {
909
+ const absolute = path.join(repoRoot, candidate);
910
+ if (await exists(absolute)) {
911
+ files.push(...await listUiFiles(repoRoot, absolute));
912
+ }
913
+ }
914
+ return unique(files);
915
+ }
916
+
917
+ async function listUiFiles(repoRoot, current) {
918
+ let stats;
919
+ try {
920
+ stats = await stat(current);
921
+ } catch {
922
+ return [];
923
+ }
924
+ if (stats.isFile()) {
925
+ return UI_EXTENSIONS.has(path.extname(current)) ? [path.relative(repoRoot, current).split(path.sep).join('/')] : [];
926
+ }
927
+ const entries = await readdir(current, { withFileTypes: true });
928
+ const files = [];
929
+ for (const entry of entries) {
930
+ if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
931
+ const absolute = path.join(current, entry.name);
932
+ if (entry.isDirectory()) {
933
+ files.push(...await listUiFiles(repoRoot, absolute));
934
+ continue;
935
+ }
936
+ if (entry.isFile() && UI_EXTENSIONS.has(path.extname(entry.name))) {
937
+ files.push(path.relative(repoRoot, absolute).split(path.sep).join('/'));
938
+ }
939
+ }
940
+ return files;
941
+ }
942
+
943
+ function buildScreenSpec({ product, route, evidence, designSystem, baseUrl }) {
944
+ const key = routeToKey(route);
945
+ const files = evidence.files.map((file) => file.path);
946
+ return {
947
+ route,
948
+ capture: {
949
+ url: baseUrl ? new URL(route.replace(/\[hotel_id\]/, 'sample'), baseUrl).toString() : route,
950
+ required: true,
951
+ viewport: { width: 390, height: 844, device_scale_factor: 2 },
952
+ screenshot_name: `${key}.png`
953
+ },
954
+ evidence,
955
+ invariants: [
956
+ {
957
+ id: `INV-${key}-1`,
958
+ statement: `Keep the current ${route} route, primary user goal, and discovered data dependencies intact.`
959
+ },
960
+ {
961
+ id: `INV-${key}-2`,
962
+ statement: `Do not remove or rename existing CTAs without an explicit matching implementation change and regression test.`
963
+ }
964
+ ],
965
+ contracts: [
966
+ {
967
+ id: `C-${key}-1`,
968
+ statement: `Implementation must stay within the discovered files unless Graphify evidence identifies a required shared component.`
969
+ },
970
+ {
971
+ id: `C-${key}-2`,
972
+ statement: `Visual changes must use the ingested ${designSystem.title ?? 'Design System'} constraints for color roles, component roles, CTA priority, density, and state colors.`
973
+ }
974
+ ],
975
+ scenarios: [
976
+ {
977
+ id: `S-${key}-1`,
978
+ statement: `A user can complete the same route-level task after modernization as before modernization.`
979
+ }
980
+ ],
981
+ anti_patterns: [
982
+ {
983
+ id: `AP-${key}-1`,
984
+ statement: `Do not implement a net-new app concept, new navigation model, or simplified mock flow that bypasses the current information structure.`
985
+ },
986
+ {
987
+ id: `AP-${key}-2`,
988
+ statement: `Do not treat any generated design suggestion as source of truth when it conflicts with current code or VibePro invariants.`
989
+ }
990
+ ],
991
+ design_brief: buildDesignBrief({ product, route, evidence, designSystem })
992
+ };
993
+ }
994
+
995
+ function buildDesignBrief({ product, route, evidence, designSystem }) {
996
+ const ctas = unique(evidence.files.flatMap((file) => file.ctas)).slice(0, 12);
997
+ const states = unique(evidence.files.flatMap((file) => file.states)).slice(0, 12);
998
+ const data = unique(evidence.files.flatMap((file) => file.data_dependencies)).slice(0, 12);
999
+ return {
1000
+ title: `${route} design-quality modernization brief`,
1001
+ body: `Modernize the current ${product} screen for route ${route} using VibePro's Design Quality DAG.
1002
+
1003
+ Preserve the current information structure and UX constraints:
1004
+ - Route: ${route}
1005
+ - Current implementation files: ${evidence.files.map((file) => file.path).join(', ') || '(not discovered)'}
1006
+ - Current CTAs: ${ctas.join(', ') || '(none discovered)'}
1007
+ - Current state names: ${states.join(', ') || '(none discovered)'}
1008
+ - Current data dependencies: ${data.join(', ') || '(none discovered)'}
1009
+
1010
+ Design quality target:
1011
+ - Clear visual hierarchy: primary task, secondary task, and metadata are visually distinguishable.
1012
+ - CTA priority: primary action is obvious; secondary actions do not compete with it.
1013
+ - State clarity: loading, empty, error, selected, disabled, and success states have distinct visual treatment.
1014
+ - Information density: keep operational/search density while improving scanability.
1015
+ - Navigation continuity: preserve route purpose and existing transition paths.
1016
+ - Component responsibility: repeated UI uses consistent roles and interaction affordances.
1017
+ - Accessibility: text contrast, target size, focus, and labels are not regressed.
1018
+
1019
+ Optional reference system:
1020
+ - ${designSystem.status === 'available'
1021
+ ? `Use ${designSystem.title ?? product} tokens/components/guidelines as brand constraints.`
1022
+ : 'No external design-system bundle is required; infer a coherent product-local system from current code and evidence.'}
1023
+
1024
+ Allowed changes:
1025
+ - Improve layout polish, spacing, hierarchy, typography, icon usage, state color consistency, and component finish.
1026
+ - Apply or infer product-local tokens and component roles.
1027
+
1028
+ Do not:
1029
+ - Create a new app idea or replace the current flow.
1030
+ - Remove existing CTAs or navigation paths.
1031
+ - Invent backend data, onboarding, or route structure.
1032
+ - Collapse dense operational information into marketing-style cards.
1033
+
1034
+ Return an implementation-ready screen direction with concrete component/layout changes and verification notes.`
1035
+ };
1036
+ }
1037
+
1038
+ function buildDesignQualityDag({ storyId, product, screens }) {
1039
+ const screenNodes = screens.map((screen) => ({
1040
+ id: `design:screen:${routeToKey(screen.route).toLowerCase()}`,
1041
+ type: 'design_screen_gate',
1042
+ label: `${screen.route} Design Brief`,
1043
+ status: screen.evidence.files.length > 0 ? 'present' : 'needs_evidence',
1044
+ required: true,
1045
+ route: screen.route,
1046
+ files: screen.evidence.files.map((file) => file.path),
1047
+ checks: [
1048
+ 'preserve_information_architecture',
1049
+ 'preserve_cta_and_navigation_contracts',
1050
+ 'improve_visual_hierarchy',
1051
+ 'maintain_information_density',
1052
+ 'clarify_interaction_states',
1053
+ 'keep_implementation_scope_reviewable'
1054
+ ]
1055
+ }));
1056
+ const nodes = [
1057
+ {
1058
+ id: 'design:current_ui_evidence',
1059
+ type: 'design_evidence_gate',
1060
+ label: 'Current UI Evidence',
1061
+ status: screens.some((screen) => screen.evidence.files.length > 0) ? 'present' : 'needs_evidence',
1062
+ required: true
1063
+ },
1064
+ {
1065
+ id: 'design:invariant_lock',
1066
+ type: 'design_invariant_gate',
1067
+ label: 'UX Invariant Lock',
1068
+ status: screens.every((screen) => screen.invariants.length > 0 && screen.anti_patterns.length > 0) ? 'present' : 'needs_evidence',
1069
+ required: true
1070
+ },
1071
+ ...screenNodes,
1072
+ {
1073
+ id: 'design:implementation_acceptance',
1074
+ type: 'design_acceptance_gate',
1075
+ label: 'Implementation Acceptance',
1076
+ status: 'needs_evidence',
1077
+ required: true,
1078
+ checks: [
1079
+ 'before_after_screenshot_or_needs_setup_record',
1080
+ 'typecheck_or_build_record',
1081
+ 'route_level_ui_review',
1082
+ 'no_design_drift_from_invariants'
1083
+ ]
1084
+ }
1085
+ ];
1086
+ return {
1087
+ schema_version: '0.1.0',
1088
+ story_id: storyId,
1089
+ product,
1090
+ model: 'vibepro-design-quality-dag-v1',
1091
+ status: nodes.some((node) => node.status === 'needs_evidence') ? 'needs_evidence' : 'ready_for_review',
1092
+ nodes,
1093
+ edges: [
1094
+ { from: 'design:current_ui_evidence', to: 'design:invariant_lock' },
1095
+ ...screenNodes.map((node) => ({ from: 'design:invariant_lock', to: node.id })),
1096
+ ...screenNodes.map((node) => ({ from: node.id, to: 'design:implementation_acceptance' }))
1097
+ ]
1098
+ };
1099
+ }
1100
+
1101
+ function buildDesignConstraintGraph(designSystem, screens, derivedDesignSystem = null) {
1102
+ const tokenSample = designSystem.token_summary?.sample ?? [];
1103
+ const componentNames = designSystem.component_summary?.names ?? [];
1104
+ const guidelineTopics = designSystem.guideline_summary?.topics ?? [];
1105
+ const derivedColorRoles = derivedDesignSystem?.semantic_tokens?.color_roles?.map((role) => role.name) ?? [];
1106
+ const derivedComponentRoles = derivedDesignSystem?.component_role_map?.roles?.map((role) => role.name) ?? [];
1107
+ return {
1108
+ schema_version: '0.1.0',
1109
+ source_design_system: {
1110
+ id: designSystem.id ?? null,
1111
+ title: designSystem.title ?? null,
1112
+ version: designSystem.version ?? null,
1113
+ status: designSystem.status
1114
+ },
1115
+ color_roles: unique([...derivedColorRoles, ...inferRoles({
1116
+ samples: tokenSample,
1117
+ defaults: ['brand', 'surface', 'text', 'success', 'warning', 'location', 'urgency', 'disabled']
1118
+ })]).slice(0, 24),
1119
+ component_roles: unique([...derivedComponentRoles, ...inferRoles({
1120
+ samples: componentNames,
1121
+ defaults: ['primary_cta', 'result_card', 'status_badge', 'filter_chip', 'bottom_sheet', 'bottom_navigation']
1122
+ })]).slice(0, 24),
1123
+ cta_priority: ['primary', 'secondary', 'tertiary'],
1124
+ state_semantics: ['loading', 'empty', 'error', 'selected', 'disabled', 'success', 'available', 'limited', 'unavailable'],
1125
+ density_policy: inferDensityPolicy(guidelineTopics),
1126
+ navigation_policy: ['preserve_route_purpose', 'preserve_existing_navigation_paths', 'preserve_back_and_bottom_nav_affordances'],
1127
+ motion_policy: ['snappy_state_transition', 'no_navigation_rewrite', 'respect_reduced_motion'],
1128
+ screen_intents: screens.map((screen) => ({
1129
+ route: screen.route,
1130
+ intent: inferScreenIntent(screen.route),
1131
+ current_ctas: unique(screen.evidence.files.flatMap((file) => file.ctas)).slice(0, 12),
1132
+ current_states: unique(screen.evidence.files.flatMap((file) => file.states)).slice(0, 12),
1133
+ data_dependencies: unique(screen.evidence.files.flatMap((file) => file.data_dependencies)).slice(0, 12)
1134
+ }))
1135
+ };
1136
+ }
1137
+
1138
+ function buildVisualHypothesisPlan({ storyId, product, screens, designConstraintGraph }) {
1139
+ return {
1140
+ schema_version: '0.1.0',
1141
+ story_id: storyId,
1142
+ status: 'needs_image_generation',
1143
+ provider_required: false,
1144
+ candidates_per_screen: { min: 2, max: 4 },
1145
+ authority: 'evidence_only',
1146
+ artifact_root: `.vibepro/design-modernize/${storyId}/visual-hypotheses/`,
1147
+ screens: screens.map((screen) => buildVisualHypothesisScreen({ product, screen, designConstraintGraph }))
1148
+ };
1149
+ }
1150
+
1151
+ function buildVisualHypothesisScreen({ product, screen, designConstraintGraph }) {
1152
+ const key = routeToKey(screen.route);
1153
+ const intent = designConstraintGraph.screen_intents.find((item) => item.route === screen.route)?.intent ?? 'existing product screen';
1154
+ const invariants = screen.invariants.map((item) => `${item.id}: ${item.statement}`).join('\n');
1155
+ const antiPatterns = screen.anti_patterns.map((item) => `${item.id}: ${item.statement}`).join('\n');
1156
+ const constraints = [
1157
+ `color roles: ${designConstraintGraph.color_roles.join(', ')}`,
1158
+ `component roles: ${designConstraintGraph.component_roles.join(', ')}`,
1159
+ `CTA priority: ${designConstraintGraph.cta_priority.join(' > ')}`,
1160
+ `state semantics: ${designConstraintGraph.state_semantics.join(', ')}`,
1161
+ `density policy: ${designConstraintGraph.density_policy}`,
1162
+ `navigation policy: ${designConstraintGraph.navigation_policy.join(', ')}`
1163
+ ].join('\n');
1164
+ return {
1165
+ route: screen.route,
1166
+ screen_intent: intent,
1167
+ screenshot_required: true,
1168
+ output_dir: `visual-hypotheses/${key.toLowerCase()}/`,
1169
+ prompt: `Modernize the existing ${product} screen ${screen.route} using the current screenshot.
1170
+
1171
+ Screen intent: ${intent}
1172
+
1173
+ Keep the same information structure, CTAs, route purpose, navigation, state behavior, and data dependencies:
1174
+ ${invariants}
1175
+
1176
+ Apply these design constraints:
1177
+ ${constraints}
1178
+
1179
+ Generate 2-4 visual candidates for the same screen. Explore hierarchy, spacing, CTA prominence, state clarity, brand fit, and scanability. Do not create a new app concept, remove dense operational content, invent data, or change navigation.
1180
+
1181
+ Forbidden changes:
1182
+ ${antiPatterns}
1183
+
1184
+ For each candidate, return design moves, preserved UX, risky or rejected moves, and implementation notes suitable for Codex.`,
1185
+ gate_checks: [
1186
+ `VH-${key}-INV preserves route purpose, information structure, CTAs, navigation, and data dependencies`,
1187
+ `VH-${key}-CTA keeps primary, secondary, and tertiary CTA hierarchy aligned with the DesignConstraintGraph`,
1188
+ `VH-${key}-DENSITY improves scanability without reducing required information density`,
1189
+ `VH-${key}-STATE keeps semantic states visually distinct`,
1190
+ `VH-${key}-BRAND uses product-local visual vocabulary`,
1191
+ `VH-${key}-IMPL is implementable within discovered files or justified shared components`,
1192
+ `VH-${key}-AP rejects new app concepts, invented data, and navigation rewrites`
1193
+ ]
1194
+ };
1195
+ }
1196
+
1197
+ function buildImplementationPlan(screens) {
1198
+ return screens.map((screen, index) => ({
1199
+ order: index + 1,
1200
+ route: screen.route,
1201
+ files: screen.evidence.files.map((file) => file.path),
1202
+ acceptance: [
1203
+ `All ${screen.route} invariants pass`,
1204
+ 'Current screenshot and after screenshot are stored for review',
1205
+ 'DS drift and UX regression gate checks have explicit pass/fail evidence'
1206
+ ]
1207
+ }));
1208
+ }
1209
+
1210
+ function buildSpecGate(screens) {
1211
+ return {
1212
+ mode: 'explicit',
1213
+ fallback_allowed: false,
1214
+ checks: [
1215
+ {
1216
+ id: 'INV-GLOBAL-1',
1217
+ statement: 'Every screen spec has at least one explicit invariant and one explicit anti-pattern.'
1218
+ },
1219
+ {
1220
+ id: 'S-GLOBAL-1',
1221
+ statement: 'Every changed screen has before/after screenshot evidence or a needs_setup verification record.'
1222
+ },
1223
+ {
1224
+ id: 'AP-GLOBAL-1',
1225
+ statement: 'Generated or inferred design output must not introduce a new route, net-new navigation model, or remove discovered CTAs.'
1226
+ },
1227
+ {
1228
+ id: 'DQ-GLOBAL-1',
1229
+ statement: 'Design Quality DAG must preserve information architecture while improving hierarchy, CTA priority, state clarity, density, accessibility, and implementation fit.'
1230
+ },
1231
+ {
1232
+ id: 'V-GLOBAL-1',
1233
+ statement: 'Verification must include typecheck/build plus route-level visual review for all screens in this plan.'
1234
+ },
1235
+ ...screens.map((screen) => ({
1236
+ id: `INV-${routeToKey(screen.route)}-GATE`,
1237
+ statement: `${screen.route} keeps route, current files, CTAs, and data dependencies aligned with Graphify/Codex evidence.`
1238
+ }))
1239
+ ]
1240
+ };
1241
+ }
1242
+
1243
+ function summarizeTokens(tokens) {
1244
+ if (typeof tokens === 'string') {
1245
+ const cssVariables = [...tokens.matchAll(/--([A-Za-z0-9_-]+)\s*:/g)].map((match) => match[1]);
1246
+ return {
1247
+ count: cssVariables.length,
1248
+ color_count: cssVariables.filter((key) => /color|background|foreground|border|surface|text|brand|semantic|state|success|warning|error/i.test(key)).length,
1249
+ spacing_count: cssVariables.filter((key) => /space|spacing|gap|padding|margin/i.test(key)).length,
1250
+ typography_count: cssVariables.filter((key) => /font|type|text|line-height|letter/i.test(key)).length,
1251
+ sample: unique(cssVariables).slice(0, 30)
1252
+ };
1253
+ }
1254
+ const flat = flattenKeys(tokens);
1255
+ return {
1256
+ count: flat.length,
1257
+ color_count: flat.filter((key) => /color|background|foreground|border|semantic|state/i.test(key)).length,
1258
+ spacing_count: flat.filter((key) => /space|spacing|gap|padding|margin/i.test(key)).length,
1259
+ typography_count: flat.filter((key) => /font|type|text|line-height|letter/i.test(key)).length,
1260
+ sample: flat.slice(0, 30)
1261
+ };
1262
+ }
1263
+
1264
+ function summarizeComponents(components) {
1265
+ if (typeof components === 'string') {
1266
+ const customElements = [...components.matchAll(/\b(ds-[A-Za-z0-9-]+)\b/g)].map((match) => match[1]);
1267
+ const classNames = [...components.matchAll(/\.([A-Za-z][A-Za-z0-9_-]+)\b/g)].map((match) => match[1]);
1268
+ return {
1269
+ count: unique([...customElements, ...classNames]).length,
1270
+ names: unique([...customElements, ...classNames]).slice(0, 40)
1271
+ };
1272
+ }
1273
+ const names = Array.isArray(components)
1274
+ ? components.map((item) => item?.name ?? item?.title ?? item).filter(Boolean)
1275
+ : flattenKeys(components);
1276
+ return {
1277
+ count: names.length,
1278
+ names: unique(names.map(String)).slice(0, 40)
1279
+ };
1280
+ }
1281
+
1282
+ function summarizeGuidelines(guidelines) {
1283
+ if (typeof guidelines === 'string') {
1284
+ const topics = guidelines
1285
+ .split(/\n+/)
1286
+ .map((line) => line.replace(/^#+\s*/, '').trim())
1287
+ .filter(Boolean);
1288
+ return {
1289
+ count: topics.length,
1290
+ topics: unique(topics).slice(0, 30)
1291
+ };
1292
+ }
1293
+ const entries = Array.isArray(guidelines)
1294
+ ? guidelines.map((item) => typeof item === 'string' ? item : item?.title ?? item?.name ?? item?.summary).filter(Boolean)
1295
+ : flattenKeys(guidelines);
1296
+ return {
1297
+ count: entries.length,
1298
+ topics: unique(entries.map(String)).slice(0, 30)
1299
+ };
1300
+ }
1301
+
1302
+ function buildDesignConstraints({ tokens, components, guidelines }) {
1303
+ return {
1304
+ token_roles: summarizeTokens(tokens),
1305
+ component_roles: summarizeComponents(components),
1306
+ guideline_roles: summarizeGuidelines(guidelines),
1307
+ required_dimensions: [
1308
+ 'semantic color roles',
1309
+ 'state colors',
1310
+ 'CTA priority',
1311
+ 'information density',
1312
+ 'navigation structure',
1313
+ 'motion guidance',
1314
+ 'component responsibility'
1315
+ ]
1316
+ };
1317
+ }
1318
+
1319
+ function inferRoles({ samples, defaults }) {
1320
+ const normalized = samples.map((sample) => String(sample).toLowerCase());
1321
+ const roles = defaults.filter((role) => normalized.some((sample) => sample.includes(role.replace('_', '-')) || sample.includes(role)));
1322
+ if (normalized.some((sample) => /brand|primary|purple/.test(sample))) roles.push('brand');
1323
+ if (normalized.some((sample) => /surface|background|base/.test(sample))) roles.push('surface');
1324
+ if (normalized.some((sample) => /text|foreground/.test(sample))) roles.push('text');
1325
+ if (normalized.some((sample) => /success|available|mint/.test(sample))) roles.push('success');
1326
+ if (normalized.some((sample) => /warning|limited|amber|urgency/.test(sample))) roles.push('warning');
1327
+ if (normalized.some((sample) => /location|distance|cyan/.test(sample))) roles.push('location');
1328
+ if (normalized.some((sample) => /cta|button|phone/.test(sample))) roles.push('primary_cta');
1329
+ if (normalized.some((sample) => /card|hotel/.test(sample))) roles.push('result_card');
1330
+ if (normalized.some((sample) => /badge|status|availability/.test(sample))) roles.push('status_badge');
1331
+ if (normalized.some((sample) => /filter|chip/.test(sample))) roles.push('filter_chip');
1332
+ if (normalized.some((sample) => /sheet/.test(sample))) roles.push('bottom_sheet');
1333
+ if (normalized.some((sample) => /navigation|nav/.test(sample))) roles.push('bottom_navigation');
1334
+ return unique([...roles, ...defaults]).slice(0, 16);
1335
+ }
1336
+
1337
+ function inferDensityPolicy(guidelineTopics) {
1338
+ const text = guidelineTopics.join(' ').toLowerCase();
1339
+ if (/dense|compact|scan|検索|比較/.test(text)) return 'dense-operational';
1340
+ return 'preserve-current-density';
1341
+ }
1342
+
1343
+ function inferScreenIntent(route) {
1344
+ if (route.includes('map')) return 'spatial exploration';
1345
+ if (route.includes('detail')) return 'filter refinement and result review';
1346
+ if (route.includes('hotel')) return 'hotel decision detail';
1347
+ if (route.includes('home')) return 'search entry';
1348
+ return 'existing product screen';
1349
+ }
1350
+
1351
+ function collectComponentNames(content) {
1352
+ const names = [];
1353
+ for (const match of content.matchAll(/(?:function|const)\s+([A-Z][A-Za-z0-9_]*)/g)) names.push(match[1]);
1354
+ for (const match of content.matchAll(/<([A-Z][A-Za-z0-9_.]*)\b/g)) names.push(match[1]);
1355
+ return unique(names).slice(0, 40);
1356
+ }
1357
+
1358
+ function collectStateNames(content) {
1359
+ const names = [];
1360
+ for (const match of content.matchAll(/useState(?:<[^>]+>)?\(([^)]*)\)/g)) names.push(clean(match[1]));
1361
+ for (const match of content.matchAll(/\b(is[A-Z][A-Za-z0-9_]+|has[A-Z][A-Za-z0-9_]+|selected[A-Z][A-Za-z0-9_]+|loading|error)\b/g)) names.push(match[1]);
1362
+ return unique(names.filter(Boolean)).slice(0, 40);
1363
+ }
1364
+
1365
+ function collectCtas(content) {
1366
+ const labels = [];
1367
+ for (const match of content.matchAll(/<button\b[^>]*>([\s\S]{0,160}?)<\/button>/g)) labels.push(clean(match[1]));
1368
+ for (const match of content.matchAll(/<Button\b[^>]*>([\s\S]{0,160}?)<\/Button>/g)) labels.push(clean(match[1]));
1369
+ for (const match of content.matchAll(/aria-label=["']([^"']+)["']/g)) labels.push(clean(match[1]));
1370
+ return unique(labels.filter(isLikelyHumanCtaLabel)).slice(0, 40);
1371
+ }
1372
+
1373
+ function isLikelyHumanCtaLabel(label) {
1374
+ const value = String(label ?? '').trim();
1375
+ if (value.length === 0 || value.length >= 80) return false;
1376
+ if (/^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(value)) return false;
1377
+ if (/^(true|false|null|undefined|loading|error)$/i.test(value)) return false;
1378
+ if (/[{}()[\]<>;]/.test(value)) return false;
1379
+ if (/\b(className|onClick|props|children|return|const|function|=>)\b/.test(value)) return false;
1380
+ return /[\p{L}\p{N}]/u.test(value);
1381
+ }
1382
+
1383
+ function collectDataDependencies(content) {
1384
+ const items = [];
1385
+ for (const match of content.matchAll(/\b(fetch|useSWR|prisma|supabase|serverAction|searchParams|params|cookies|headers)\b/g)) items.push(match[1]);
1386
+ for (const match of content.matchAll(/\b(api\/[A-Za-z0-9_/-]+)\b/g)) items.push(match[1]);
1387
+ return unique(items).slice(0, 40);
1388
+ }
1389
+
1390
+ function collectNavigationTargets(content) {
1391
+ const targets = [];
1392
+ for (const match of content.matchAll(/href=["']([^"']+)["']/g)) targets.push(match[1]);
1393
+ for (const match of content.matchAll(/router\.(push|replace)\(["']([^"']+)["']\)/g)) targets.push(match[2]);
1394
+ return unique(targets).slice(0, 40);
1395
+ }
1396
+
1397
+ function routeToKey(route) {
1398
+ return route.replace(/^\//, '').replace(/[^A-Za-z0-9]+/g, '-').replace(/^-|-$/g, '').toUpperCase() || 'ROOT';
1399
+ }
1400
+
1401
+ function inferProductName(repoRoot) {
1402
+ return path.basename(repoRoot).replace(/^session-\d+-/i, '').replace(/^g\d+-/i, '') || 'product';
1403
+ }
1404
+
1405
+ function flattenKeys(value, prefix = '') {
1406
+ if (!value || typeof value !== 'object') return [];
1407
+ const keys = [];
1408
+ for (const [key, child] of Object.entries(value)) {
1409
+ const next = prefix ? `${prefix}.${key}` : key;
1410
+ keys.push(next);
1411
+ if (child && typeof child === 'object' && !Array.isArray(child)) keys.push(...flattenKeys(child, next));
1412
+ }
1413
+ return keys;
1414
+ }
1415
+
1416
+ function clean(value) {
1417
+ return String(value ?? '')
1418
+ .replace(/<[^>]+>/g, ' ')
1419
+ .replace(/\{[^}]+\}/g, ' ')
1420
+ .replace(/\s+/g, ' ')
1421
+ .trim();
1422
+ }
1423
+
1424
+ async function exists(absolutePath) {
1425
+ try {
1426
+ await stat(absolutePath);
1427
+ return true;
1428
+ } catch {
1429
+ return false;
1430
+ }
1431
+ }
1432
+
1433
+ function unique(items) {
1434
+ return [...new Set(items.filter(Boolean))];
1435
+ }