@tangle-network/browser-agent-driver 0.12.1 → 0.13.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 (62) hide show
  1. package/README.md +28 -0
  2. package/dist/brain/index.d.ts.map +1 -1
  3. package/dist/brain/index.js +8 -0
  4. package/dist/brain/index.js.map +1 -1
  5. package/dist/cli-design-audit.d.ts +2 -0
  6. package/dist/cli-design-audit.d.ts.map +1 -1
  7. package/dist/cli-design-audit.js +111 -303
  8. package/dist/cli-design-audit.js.map +1 -1
  9. package/dist/cli.js +2 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/design/audit/classify.d.ts +20 -0
  12. package/dist/design/audit/classify.d.ts.map +1 -0
  13. package/dist/design/audit/classify.js +148 -0
  14. package/dist/design/audit/classify.js.map +1 -0
  15. package/dist/design/audit/evaluate.d.ts +49 -0
  16. package/dist/design/audit/evaluate.d.ts.map +1 -0
  17. package/dist/design/audit/evaluate.js +353 -0
  18. package/dist/design/audit/evaluate.js.map +1 -0
  19. package/dist/design/audit/measure/a11y.d.ts +25 -0
  20. package/dist/design/audit/measure/a11y.d.ts.map +1 -0
  21. package/dist/design/audit/measure/a11y.js +144 -0
  22. package/dist/design/audit/measure/a11y.js.map +1 -0
  23. package/dist/design/audit/measure/contrast.d.ts +18 -0
  24. package/dist/design/audit/measure/contrast.d.ts.map +1 -0
  25. package/dist/design/audit/measure/contrast.js +201 -0
  26. package/dist/design/audit/measure/contrast.js.map +1 -0
  27. package/dist/design/audit/measure/index.d.ts +14 -0
  28. package/dist/design/audit/measure/index.d.ts.map +1 -0
  29. package/dist/design/audit/measure/index.js +38 -0
  30. package/dist/design/audit/measure/index.js.map +1 -0
  31. package/dist/design/audit/pipeline.d.ts +33 -0
  32. package/dist/design/audit/pipeline.d.ts.map +1 -0
  33. package/dist/design/audit/pipeline.js +109 -0
  34. package/dist/design/audit/pipeline.js.map +1 -0
  35. package/dist/design/audit/roi.d.ts +49 -0
  36. package/dist/design/audit/roi.d.ts.map +1 -0
  37. package/dist/design/audit/roi.js +157 -0
  38. package/dist/design/audit/roi.js.map +1 -0
  39. package/dist/design/audit/rubric/fragments/domain-ai.md +19 -0
  40. package/dist/design/audit/rubric/fragments/domain-crypto.md +24 -0
  41. package/dist/design/audit/rubric/fragments/domain-devtools.md +21 -0
  42. package/dist/design/audit/rubric/fragments/domain-fintech.md +19 -0
  43. package/dist/design/audit/rubric/fragments/maturity-prototype.md +36 -0
  44. package/dist/design/audit/rubric/fragments/type-blog.md +22 -0
  45. package/dist/design/audit/rubric/fragments/type-docs.md +23 -0
  46. package/dist/design/audit/rubric/fragments/type-ecommerce.md +23 -0
  47. package/dist/design/audit/rubric/fragments/type-marketing.md +22 -0
  48. package/dist/design/audit/rubric/fragments/type-saas-app.md +20 -0
  49. package/dist/design/audit/rubric/fragments/universal-calibration.md +16 -0
  50. package/dist/design/audit/rubric/fragments/universal-effort-anchor.md +46 -0
  51. package/dist/design/audit/rubric/fragments/universal-foundation.md +49 -0
  52. package/dist/design/audit/rubric/loader.d.ts +49 -0
  53. package/dist/design/audit/rubric/loader.d.ts.map +1 -0
  54. package/dist/design/audit/rubric/loader.js +216 -0
  55. package/dist/design/audit/rubric/loader.js.map +1 -0
  56. package/dist/design/audit/types.d.ts +166 -0
  57. package/dist/design/audit/types.d.ts.map +1 -0
  58. package/dist/design/audit/types.js +8 -0
  59. package/dist/design/audit/types.js.map +1 -0
  60. package/dist/types.d.ts +10 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +3 -2
@@ -14,224 +14,8 @@ import { PlaywrightDriver } from './drivers/playwright.js';
14
14
  import { resolveProviderApiKey, resolveProviderModelName } from './provider-defaults.js';
15
15
  import { loadLocalEnvFiles } from './env-loader.js';
16
16
  import { cliError } from './cli-ui.js';
17
- // ---------------------------------------------------------------------------
18
- // Audit profiles domain-specific rubrics injected into the system prompt
19
- // ---------------------------------------------------------------------------
20
- const PROFILE_RUBRICS = {
21
- general: `
22
- CALIBRATION — be ruthlessly honest:
23
- - 9-10: Only world-class design (Linear, Stripe, Vercel). Exceptional typography, spacing, micro-interactions.
24
- - 7-8: Professional and polished. Minor inconsistencies tolerated if the overall system is coherent.
25
- - 5-6: Functional but clearly lacks design investment. Default component libraries with no customization.
26
- - 3-4: Noticeably broken or amateurish. Inconsistent spacing, clashing colors, poor hierarchy.
27
- - 1-2: Unusable or visually broken. Overlapping elements, unreadable text, broken layouts.
28
-
29
- Most production apps score 5-7. Very few deserve 8+. Do NOT grade on a curve.`,
30
- saas: `
31
- SaaS APPLICATION AUDIT — evaluate as a paying customer would:
32
- - Information density: is data presented efficiently without clutter?
33
- - Navigation: can I find features in <3 clicks? Is the sidebar/nav intuitive?
34
- - Empty states: what happens with no data? Are there helpful onboarding prompts?
35
- - Loading states: are there skeleton screens or spinners, or does content pop in?
36
- - Form design: inline validation? Clear labels? Logical tab order?
37
- - Dashboard layout: is the most important data prominent? Card hierarchy clear?
38
- - Error states: are errors actionable with clear recovery paths?
39
- - Consistency: do buttons, inputs, modals follow the same patterns throughout?
40
-
41
- CALIBRATION: Linear, Notion, Figma = 9. Generic admin templates = 5. Broken CRUD apps = 3.`,
42
- defi: `
43
- DeFi/CRYPTO APPLICATION AUDIT — evaluate as a trader managing real money:
44
- - Trust signals: does the UI feel safe to connect a wallet to? Professional or sketchy?
45
- - Token displays: are balances formatted correctly? Right decimal places? USD equivalents?
46
- - Transaction clarity: is it clear what you're signing? Amount, fees, slippage shown?
47
- - Loading states: RPC calls are slow — are there proper loading indicators for balances, quotes, gas estimates?
48
- - Mobile responsiveness: most DeFi usage is mobile — does it work on small viewports?
49
- - Dark mode: most DeFi apps use dark mode — is contrast sufficient? Are borders visible?
50
- - Error handling: what happens when RPC fails? Wallet rejects? Insufficient balance?
51
- - Swap UX: is token selection intuitive? Can you paste addresses? Is the price impact shown?
52
- - Wallet connection: is the connect flow smooth? Are supported wallets clearly shown?
53
- - Gas/fee transparency: are gas estimates shown before confirmation?
54
-
55
- CALIBRATION: Uniswap v4, Aave v3 = 8. Average DEX = 5. Rug-pull-looking sites = 2.
56
- Uniswap is good but not perfect — dense token lists, some spacing issues, swap review could be clearer.`,
57
- marketing: `
58
- MARKETING/LANDING PAGE AUDIT — evaluate as a potential customer deciding in 10 seconds:
59
- - Hero clarity: in 5 seconds, can I tell what this product does and who it's for?
60
- - Visual hierarchy: does the eye flow naturally from headline → subtext → CTA?
61
- - CTA prominence: is the primary call-to-action obvious and compelling?
62
- - Social proof: are testimonials, logos, or metrics shown convincingly?
63
- - Typography: is the headline typography impactful? Body text readable?
64
- - Imagery: are images/illustrations high quality and relevant, or stock photo generic?
65
- - Whitespace: does the page breathe, or is it cramped?
66
- - Mobile: does the hero and CTA work on mobile without scrolling?
67
- - Performance perception: does it feel fast? Are images optimized? No layout shift?
68
- - Footer: is navigation complete? Legal links present?
69
-
70
- CALIBRATION: Stripe, Linear, Vercel = 9. Average startup landing page = 5. Template sites = 3.`,
71
- vibecoded: `
72
- VIBECODED / AI-GENERATED APP AUDIT — evaluate as a design-literate user who can smell defaults:
73
-
74
- TEMPLATE DETECTION (the #1 sin of vibecoded apps):
75
- - Is this clearly an unmodified shadcn/ui, MUI, Ant Design, or Chakra template? Score ceiling: 4 if yes.
76
- - Default border-radius (6-8px shadcn, 4px MUI), default color palette (zinc/slate grays, blue-600 primary)?
77
- - Default component spacing with no customization? Standard card shadows? Stock empty states?
78
- - "Looks like every other AI-generated app" = automatic 3-4 score.
79
-
80
- HIERARCHY & INFORMATION ARCHITECTURE:
81
- - Is everything the same visual weight? (Common AI pattern: all cards same size, no primary/secondary distinction)
82
- - Is there clear information hierarchy? Primary action vs secondary vs tertiary?
83
- - Does the layout have purpose or is it "centered column of cards" (the AI default)?
84
- - Navigation: is it a dumped list of features or thoughtfully organized?
85
-
86
- DESIGN SYSTEM COHERENCE:
87
- - Are there more than 3 distinct border-radius values? (Incoherent)
88
- - Color palette: intentional and limited (4-6 colors) or random accumulation?
89
- - Spacing: consistent rhythm on an 8px grid, or arbitrary per-component?
90
- - Typography: deliberate scale with 3-4 sizes, or every component picking its own?
91
- - Are interactive states (hover, focus, active, disabled) designed or browser-default?
92
-
93
- CRAFT SIGNALS (what separates 7 from 9):
94
- - Custom icons or generic Lucide/Heroicons dump?
95
- - Micro-interactions: button press feedback, page transitions, loading skeletons?
96
- - Empty states: designed illustrations or "No data found" text?
97
- - Error states: helpful messages with recovery actions or raw error strings?
98
- - Dark mode (if present): properly designed or just "invert colors"?
99
- - Content-first: does real content drive the layout, or is it a container waiting for content?
100
-
101
- AGENTIC APP SPECIFICS:
102
- - Agent status indicators: is it clear what the agent is doing? Progress feedback?
103
- - Streaming/loading: smooth token streaming or janky text replacement?
104
- - Conversation UI: proper message bubbles with timestamps, or plain text dump?
105
- - Tool call visualization: can the user see what tools the agent used?
106
- - Error recovery: when the agent fails, is there a clear retry/edit path?
107
-
108
- CALIBRATION:
109
- - 9-10: Custom design system, thoughtful hierarchy, polished interactions (Linear, Cursor, v0.dev)
110
- - 7-8: Modified template with intentional design decisions, consistent system
111
- - 5-6: Lightly customized template, functional but generic (most AI-generated apps)
112
- - 3-4: Unmodified component library, no design investment, "it works" energy
113
- - 1-2: Broken layout, clashing styles, unusable
114
-
115
- Most vibecoded apps score 3-5. The ceiling for unmodified templates is 4 regardless of functionality.`,
116
- };
117
- // ---------------------------------------------------------------------------
118
- // Upgraded system prompt — much more opinionated than the original
119
- // ---------------------------------------------------------------------------
120
- function buildAuditPrompt(profile) {
121
- const rubric = PROFILE_RUBRICS[profile] || PROFILE_RUBRICS.general;
122
- return `You are a principal design engineer who has shipped design systems at Linear, Stripe, and Vercel. You review with the precision of a typographer and the ruthlessness of a design director. You have built and maintained production design systems used by millions.
123
-
124
- Your job: perform an exhaustive visual design audit of this page. You must be specific enough that a developer could fix every issue from your report alone — reference exact elements, computed values, pixel measurements, and CSS properties.
125
-
126
- EVALUATION FRAMEWORK (score each area 1-10, then weight into overall):
127
-
128
- 1. LAYOUT & GRID (weight: 15%)
129
- - Is there a consistent grid system? What grid unit? (4px, 8px, etc.)
130
- - Column alignment: do content blocks align to the same left/right edges?
131
- - Content width: is max-width appropriate? (prose: 65-75ch, app: fluid with sidebar)
132
- - Responsive: does the layout reflow intentionally or just shrink?
133
- - Z-index layering: any stacking context issues? Overlapping elements?
134
- - Check for: orphaned elements floating outside the grid, inconsistent container padding
135
-
136
- 2. TYPOGRAPHY SYSTEM (weight: 15%)
137
- - Type scale: is there a clear hierarchy? Count distinct font-size values — more than 5-6 suggests no scale.
138
- - Line height: body text should be 1.4-1.6, headings 1.1-1.3. Flag violations.
139
- - Letter spacing: headings often need negative (-0.01 to -0.03em). Is it tuned?
140
- - Font pairing: max 2 families (heading + body). Flag 3+.
141
- - Text rendering: are long paragraphs wider than 75ch? That harms readability.
142
- - Orphans/widows: any single-word last lines in headings?
143
- - Font loading: is there FOUT/FOIT? Font-display strategy?
144
-
145
- 3. COLOR & CONTRAST (weight: 15%)
146
- - WCAG AA compliance: normal text needs 4.5:1, large text (18px+/14px+ bold) needs 3:1. ESTIMATE ratios.
147
- - Palette size: count distinct hues. More than 5-6 non-neutral hues = incoherent.
148
- - Semantic color usage: is the primary color used consistently for primary actions?
149
- - Background layering: do nested surfaces have clear elevation (bg-0, bg-1, bg-2)?
150
- - Gray scale: are grays consistent? All blue-gray, or mixed warm/cool? Mixed = incoherent.
151
- - Accent usage: are accent colors used sparingly or splashed everywhere?
152
-
153
- 4. SPACING & RHYTHM (weight: 15%)
154
- - Grid adherence: what % of spacing values are multiples of the base unit?
155
- - Vertical rhythm: are section gaps consistent? Measure gap between each major section.
156
- - Component internal spacing: is padding consistent within similar components (all cards, all inputs)?
157
- - Whitespace ratio: is there enough breathing room, or is everything cramped?
158
- - Margin collapse issues: any unintended spacing from margin collapse?
159
-
160
- 5. COMPONENT CONSISTENCY (weight: 15%)
161
- - Button variants: how many distinct button styles? Are they intentional variants or accidents?
162
- - Input styling: are all form inputs styled consistently? Border, focus ring, label position?
163
- - Card patterns: same border-radius, shadow, padding across all cards?
164
- - Icon system: consistent size (16/20/24px), stroke width, and style?
165
- - Border radius: count distinct values. More than 3 (e.g., 4px, 8px, full) = incoherent.
166
- - Shadow system: consistent elevation scale or random drop shadows?
167
-
168
- 6. INTERACTION DESIGN (weight: 10%)
169
- - Hover states: do interactive elements have visible hover feedback?
170
- - Focus indicators: are there visible focus rings for keyboard navigation?
171
- - Active/pressed states: button feedback on click?
172
- - Transitions: are they present? Consistent duration (150-300ms)? Appropriate easing?
173
- - Loading states: skeleton screens, spinners, or no loading feedback at all?
174
- - Cursor changes: does cursor change to pointer on clickable elements?
175
-
176
- 7. ACCESSIBILITY (weight: 10%)
177
- - Semantic HTML: are headings in order (h1 → h2 → h3)? Are buttons actually <button>?
178
- - ARIA labels: do icon-only buttons have labels? Do images have alt text?
179
- - Keyboard navigation: can you tell what's focused? Is tab order logical?
180
- - Touch targets: are mobile tap targets at least 44x44px?
181
- - Screen reader: is content structured so screen reader users get meaningful navigation?
182
-
183
- 8. VISUAL POLISH (weight: 5%)
184
- - Pixel precision: any elements off by 1px? Misaligned text baselines?
185
- - Image quality: are images sharp on retina displays (2x resolution)?
186
- - Icon consistency: all from the same set, or a mix of styles/weights?
187
- - Empty states: designed or raw "no data" text?
188
- - Error states: styled or browser-default?
189
- - Favicon and meta: present and professional?
190
-
191
- ${rubric}
192
-
193
- SPECIFICITY REQUIREMENTS — your findings must be THIS specific:
194
- - BAD: "Spacing is inconsistent" (vague, useless)
195
- - GOOD: "Section gap between hero and features is 48px, but features-to-pricing is 24px and pricing-to-footer is 64px. Use consistent 48px or 64px vertical rhythm throughout."
196
- - BAD: "Colors don't look right" (vague)
197
- - GOOD: "Body text (#6b7280) on white background has ~4.6:1 contrast ratio (barely passes AA). The same gray on the light-gray card background (#f9fafb) drops to ~3.8:1 — fails AA for normal text. Darken body text to #4b5563 (7:1+)."
198
- - BAD: "Typography needs work" (vague)
199
- - GOOD: "6 distinct font sizes detected (12, 13, 14, 16, 20, 32px) with no clear scale. Consolidate to a 4-step scale: 14px body, 16px large, 24px h2, 36px h1. Current h2 at 20px lacks sufficient contrast with 16px body text."
200
-
201
- For EACH finding, you MUST include a concrete CSS fix in the suggestion field. Not "improve spacing" but "gap: 48px" or "font-size: 14px; line-height: 1.5".
202
-
203
- RESPOND WITH ONLY a JSON object:
204
- {
205
- "score": 6,
206
- "summary": "One-sentence overall assessment with the key design system failure mode",
207
- "strengths": ["Specific thing done well with evidence", "Another measured strength"],
208
- "findings": [
209
- {
210
- "category": "spacing",
211
- "severity": "major",
212
- "description": "Hero section has 64px top padding but only 16px bottom padding before the feature grid, creating visual imbalance. The 4:1 ratio breaks vertical rhythm.",
213
- "location": "Hero section → feature grid transition (main > section:nth-child(2))",
214
- "suggestion": "padding-bottom: 48px on hero section. Standardize all section gaps to 48px or 64px.",
215
- "cssSelector": "main > section:first-child",
216
- "cssFix": "padding-bottom: 48px"
217
- }
218
- ],
219
- "designSystemScore": {
220
- "layout": 7,
221
- "typography": 5,
222
- "color": 6,
223
- "spacing": 4,
224
- "components": 6,
225
- "interactions": 3,
226
- "accessibility": 5,
227
- "polish": 4
228
- }
229
- }
230
-
231
- Categories: visual-bug, layout, contrast, alignment, spacing, typography, accessibility, ux
232
- Severities: critical (blocks usage or fails WCAG), major (looks unprofessional), minor (polish detail)
233
- Score: 1-10 per calibration above. Most production apps score 5-7. Template apps score 3-5. Only world-class ships get 8+. Be honest — inflated scores help nobody.`;
234
- }
17
+ import { auditOnePage } from './design/audit/pipeline.js';
18
+ import { detectSystemicFindings, topByRoi } from './design/audit/roi.js';
235
19
  // ---------------------------------------------------------------------------
236
20
  // Page discovery — find key pages by crawling links
237
21
  // ---------------------------------------------------------------------------
@@ -282,7 +66,7 @@ async function discoverPages(page, startUrl, maxPages) {
282
66
  // ---------------------------------------------------------------------------
283
67
  // Report generation
284
68
  // ---------------------------------------------------------------------------
285
- function generateReport(results, profile) {
69
+ function generateReport(results, profile, topFixes = []) {
286
70
  const lines = [];
287
71
  const avgScore = results.length > 0
288
72
  ? results.reduce((sum, r) => sum + r.score, 0) / results.length
@@ -294,7 +78,18 @@ function generateReport(results, profile) {
294
78
  const totalTokens = results.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
295
79
  lines.push('# Design Audit Report');
296
80
  lines.push('');
297
- lines.push(`**Profile:** ${profile}`);
81
+ if (profile) {
82
+ lines.push(`**Profile:** ${profile}`);
83
+ }
84
+ // Surface per-page classification when present (Gen 2)
85
+ const classifications = results
86
+ .filter(r => r.classification)
87
+ .map(r => `${r.url}: ${r.classification.type}/${r.classification.domain} (${r.classification.maturity})`);
88
+ if (classifications.length > 0) {
89
+ lines.push(`**Auto-classified:**`);
90
+ for (const c of classifications)
91
+ lines.push(`- ${c}`);
92
+ }
298
93
  lines.push(`**Pages audited:** ${results.length}`);
299
94
  lines.push(`**Overall score:** ${avgScore.toFixed(1)}/10`);
300
95
  lines.push(`**Findings:** ${allFindings.length} (${critical} critical, ${major} major, ${minor} minor)`);
@@ -305,6 +100,34 @@ function generateReport(results, profile) {
305
100
  const scoreBar = '█'.repeat(Math.round(avgScore)) + '░'.repeat(10 - Math.round(avgScore));
306
101
  lines.push(`\`${scoreBar}\` ${avgScore.toFixed(1)}/10`);
307
102
  lines.push('');
103
+ // ── Top Fixes (by ROI) — the headline section users actually read first ──
104
+ if (topFixes.length > 0) {
105
+ lines.push('## Top Fixes (by ROI)');
106
+ lines.push('');
107
+ lines.push('Fix these first — sorted by impact × blast / effort:');
108
+ lines.push('');
109
+ for (let i = 0; i < topFixes.length; i++) {
110
+ const f = topFixes[i];
111
+ const tags = [];
112
+ if (f.pageCount && f.pageCount >= 2)
113
+ tags.push(`appears on ${f.pageCount} pages`);
114
+ if (f.blast === 'system')
115
+ tags.push('SYSTEMIC');
116
+ const tagStr = tags.length > 0 ? ` _(${tags.join(', ')})_` : '';
117
+ const roiStr = f.roi !== undefined ? f.roi.toFixed(1) : '—';
118
+ lines.push(`### ${i + 1}. [${f.severity}] ${f.description}${tagStr}`);
119
+ lines.push('');
120
+ lines.push(`- **ROI:** ${roiStr} · Impact ${f.impact ?? '—'} · Effort ${f.effort ?? '—'} · Blast ${f.blast ?? '—'}`);
121
+ lines.push(`- **Location:** ${f.location}`);
122
+ lines.push(`- **Fix:** ${f.suggestion}`);
123
+ if (f.cssSelector && f.cssFix) {
124
+ lines.push(`- **CSS:** \`${f.cssSelector} { ${f.cssFix} }\``);
125
+ }
126
+ lines.push('');
127
+ }
128
+ lines.push('---');
129
+ lines.push('');
130
+ }
308
131
  // Per-page results
309
132
  for (const result of results) {
310
133
  lines.push(`## ${result.url}`);
@@ -326,9 +149,11 @@ function generateReport(results, profile) {
326
149
  lines.push('**Design System Breakdown:**');
327
150
  lines.push('');
328
151
  const ds = result.designSystemScore;
329
- const dsKeys = ['layout', 'typography', 'color', 'spacing', 'components', 'interactions', 'accessibility', 'polish'];
330
- for (const key of dsKeys) {
331
- if (ds[key] !== undefined) {
152
+ // Universal dimensions in fixed order, then any custom dimensions (alpha)
153
+ const universal = ['layout', 'typography', 'color', 'spacing', 'components', 'interactions', 'accessibility', 'polish'];
154
+ const custom = Object.keys(ds).filter(k => !universal.includes(k)).sort();
155
+ for (const key of [...universal, ...custom]) {
156
+ if (typeof ds[key] === 'number') {
332
157
  const bar = '█'.repeat(Math.round(ds[key])) + '░'.repeat(10 - Math.round(ds[key]));
333
158
  lines.push(`- ${key}: \`${bar}\` ${ds[key]}/10`);
334
159
  }
@@ -362,11 +187,9 @@ export async function runDesignAudit(opts) {
362
187
  await runTokenExtraction(opts);
363
188
  return;
364
189
  }
365
- const profile = opts.profile ?? 'general';
366
- if (!PROFILE_RUBRICS[profile]) {
367
- cliError(`unknown profile: ${profile}. Options: ${Object.keys(PROFILE_RUBRICS).join(', ')}`);
368
- process.exit(1);
369
- }
190
+ // Profile is optional — auto-classified by default. Override with --profile
191
+ // selects a single type fragment instead of letting the classifier decide.
192
+ const profile = opts.profile;
370
193
  const maxPages = opts.pages ?? 5;
371
194
  const provider = (opts.provider ?? 'claude-code');
372
195
  const modelName = resolveProviderModelName(provider, opts.model);
@@ -374,7 +197,8 @@ export async function runDesignAudit(opts) {
374
197
  const [vw, vh] = (opts.viewport ?? '1440x900').split('x').map(Number);
375
198
  console.log('');
376
199
  console.log(` ${chalk.bold('bad design-audit')}`);
377
- console.log(` ${profile} ${chalk.dim('·')} ${chalk.cyan(modelName)} ${chalk.dim('·')} ${vw}×${vh} ${chalk.dim('·')} up to ${maxPages} pages`);
200
+ const profileLabel = profile ?? chalk.dim('auto-classify');
201
+ console.log(` ${profileLabel} ${chalk.dim('·')} ${chalk.cyan(modelName)} ${chalk.dim('·')} ${vw}×${vh} ${chalk.dim('·')} up to ${maxPages} pages`);
378
202
  console.log(` ${chalk.dim('→')} ${opts.url}`);
379
203
  console.log('');
380
204
  const browser = await chromium.launch({ headless: opts.headless ?? true });
@@ -402,20 +226,41 @@ export async function runDesignAudit(opts) {
402
226
  llmTimeoutMs: 120_000, // design audits generate ~8k tokens of structured JSON — need 2min
403
227
  });
404
228
  const driver = new PlaywrightDriver(page);
405
- // Audit each page
229
+ // Audit each page through the classify → measure → evaluate pipeline
406
230
  const results = [];
407
231
  for (let i = 0; i < pages.length; i++) {
408
232
  const url = pages[i];
409
233
  console.log(` ${chalk.dim(`[${i + 1}/${pages.length}]`)} ${url}`);
410
- const result = await auditSinglePage(brain, driver, page, url, profile, screenshotDir);
234
+ const gen2 = await auditOnePage({
235
+ brain,
236
+ driver,
237
+ page,
238
+ url,
239
+ profileOverride: profile,
240
+ screenshotDir,
241
+ userRubricsDir: opts.rubricsDir,
242
+ });
243
+ const result = gen2;
411
244
  results.push(result);
412
245
  const icon = result.score >= 8 ? chalk.green('✓') : result.score >= 5 ? chalk.yellow('~') : chalk.red('✗');
413
246
  const scoreColor = result.score >= 8 ? chalk.green : result.score >= 5 ? chalk.yellow : chalk.red;
414
247
  const findingCount = result.findings.length;
415
- console.log(` ${icon} ${scoreColor(`${result.score}/10`)} ${chalk.dim('—')} ${findingCount} finding${findingCount !== 1 ? 's' : ''}`);
248
+ const classLabel = result.classification
249
+ ? chalk.dim(` (${result.classification.type}/${result.classification.domain})`)
250
+ : '';
251
+ console.log(` ${icon} ${scoreColor(`${result.score}/10`)} ${chalk.dim('—')} ${findingCount} finding${findingCount !== 1 ? 's' : ''}${classLabel}`);
252
+ }
253
+ // Cross-page systemic detection + top-fixes ranking.
254
+ // Findings appearing on 2+ pages collapse into a single systemic finding.
255
+ // The deduped set drives the Top Fixes section at the top of the report.
256
+ let topFixes = [];
257
+ if (results.length > 0) {
258
+ const perPage = results.map(r => r.findings);
259
+ const deduped = detectSystemicFindings(perPage);
260
+ topFixes = topByRoi(deduped, 5);
416
261
  }
417
262
  // Generate report
418
- const report = generateReport(results, profile);
263
+ const report = generateReport(results, profile, topFixes);
419
264
  const reportPath = path.join(outputDir, 'report.md');
420
265
  fs.writeFileSync(reportPath, report);
421
266
  const allFindings = results.flatMap(r => r.findings);
@@ -430,6 +275,7 @@ export async function runDesignAudit(opts) {
430
275
  profile,
431
276
  url: opts.url,
432
277
  pages: results,
278
+ topFixes,
433
279
  summary: { avgScore, totalFindings: allFindings.length, critical, major, minor },
434
280
  }, null, 2));
435
281
  console.log(` ${chalk.dim('JSON →')} ${jsonPath}`);
@@ -457,8 +303,15 @@ export async function runDesignAudit(opts) {
457
303
  for (let rep = 0; rep < 2; rep++) {
458
304
  const repResults = [];
459
305
  for (const url of pages) {
460
- const r = await auditSinglePage(brain, driver, page, url, profile);
461
- repResults.push(r);
306
+ const gen2 = await auditOnePage({
307
+ brain,
308
+ driver,
309
+ page,
310
+ url,
311
+ profileOverride: profile,
312
+ userRubricsDir: opts.rubricsDir,
313
+ });
314
+ repResults.push(gen2);
462
315
  }
463
316
  const repAvg = repResults.reduce((s, r) => s + r.score, 0) / repResults.length;
464
317
  scores.push(repAvg);
@@ -482,14 +335,19 @@ export async function runDesignAudit(opts) {
482
335
  // --evolve=claude-code|codex|opencode|<custom> → agent dispatch
483
336
  const evolveMode = opts.evolve === true || opts.evolve === 'true' || opts.evolve === 'css' ? 'css' : opts.evolve;
484
337
  let evolveResult;
338
+ // Evolve loops still use the legacy single-profile re-audit. When Gen 2 auto-classified,
339
+ // synthesize a profile name from the first page's classification (or fall back to 'general').
340
+ const evolveProfile = profile
341
+ ?? results[0]?.classification?.type
342
+ ?? 'general';
485
343
  if (evolveMode !== 'css') {
486
344
  // Agent-dispatched evolve — a coding agent edits the actual source code
487
345
  const projectDir = opts.projectDir ?? process.cwd();
488
- evolveResult = await runAgentEvolveLoop(brain, driver, page, pages, profile, results, outputDir, opts.evolveRounds ?? 3, evolveMode, projectDir, opts.debug);
346
+ evolveResult = await runAgentEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, evolveMode, projectDir, opts.debug);
489
347
  }
490
348
  else {
491
349
  // CSS-injection evolve — ephemeral fixes injected into the browser page
492
- evolveResult = await runEvolveLoop(brain, driver, page, pages, profile, results, outputDir, opts.evolveRounds ?? 3);
350
+ evolveResult = await runEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3);
493
351
  }
494
352
  // Write evolve report
495
353
  const evolvePath = path.join(outputDir, 'evolve-report.md');
@@ -598,7 +456,14 @@ async function runEvolveLoop(brain, driver, page, pages, profile, initialResults
598
456
  // Take screenshot of fixed state
599
457
  const screenshotDir = path.join(outputDir, `screenshots-round-${round}`);
600
458
  fs.mkdirSync(screenshotDir, { recursive: true });
601
- const result = await auditSinglePage(brain, driver, page, url, profile, screenshotDir);
459
+ const result = (await auditOnePage({
460
+ brain,
461
+ driver,
462
+ page,
463
+ url,
464
+ profileOverride: profile,
465
+ screenshotDir,
466
+ }));
602
467
  roundResults.push(result);
603
468
  }
604
469
  catch {
@@ -847,7 +712,14 @@ async function runAgentEvolveLoop(brain, driver, page, pages, profile, initialRe
847
712
  const roundScreenshotDir = path.join(outputDir, `screenshots-round-${round}`);
848
713
  fs.mkdirSync(roundScreenshotDir, { recursive: true });
849
714
  for (const url of pages) {
850
- const result = await auditSinglePage(brain, driver, page, url, profile, roundScreenshotDir);
715
+ const result = (await auditOnePage({
716
+ brain,
717
+ driver,
718
+ page,
719
+ url,
720
+ profileOverride: profile,
721
+ screenshotDir: roundScreenshotDir,
722
+ }));
851
723
  roundResults.push(result);
852
724
  }
853
725
  const roundAvg = roundResults.reduce((s, r) => s + r.score, 0) / roundResults.length;
@@ -892,70 +764,6 @@ async function runAgentEvolveLoop(brain, driver, page, pages, profile, initialRe
892
764
  cssOverride: '', // no CSS override in agent mode — agent edited source directly
893
765
  };
894
766
  }
895
- // Direct page audit using brain.generate with custom prompt
896
- async function auditSinglePage(brain, driver, page, url, profile, screenshotDir) {
897
- try {
898
- await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 }).catch(() => page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15_000 }));
899
- await page.waitForTimeout(2000);
900
- // Dismiss cookie banners
901
- for (const sel of ['button:has-text("Accept all")', 'button:has-text("Accept")', 'button:has-text("Reject all")', 'button:has-text("Got it")', 'button:has-text("Close")']) {
902
- const btn = page.locator(sel).first();
903
- if (await btn.isVisible({ timeout: 500 }).catch(() => false)) {
904
- await btn.click({ timeout: 2000 }).catch(() => null);
905
- await page.waitForTimeout(500);
906
- break;
907
- }
908
- }
909
- const state = await driver.observe();
910
- // Save screenshot
911
- let screenshotPath;
912
- if (screenshotDir) {
913
- const slug = new URL(url).pathname.replace(/\//g, '_').replace(/^_/, '') || 'index';
914
- screenshotPath = path.join(screenshotDir, `${slug}.png`);
915
- await page.screenshot({ path: screenshotPath, fullPage: false });
916
- }
917
- const result = await brain.auditDesign(state, `Audit the design quality of this page: ${url}`, [], buildAuditPrompt(profile));
918
- let summary = '';
919
- let strengths = [];
920
- let designSystemScore;
921
- try {
922
- let text = result.raw.trim();
923
- if (text.startsWith('```'))
924
- text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
925
- const parsed = JSON.parse(text);
926
- summary = parsed.summary || '';
927
- strengths = Array.isArray(parsed.strengths) ? parsed.strengths : [];
928
- if (parsed.designSystemScore && typeof parsed.designSystemScore === 'object') {
929
- designSystemScore = {};
930
- for (const [k, v] of Object.entries(parsed.designSystemScore)) {
931
- if (typeof v === 'number')
932
- designSystemScore[k] = v;
933
- }
934
- }
935
- }
936
- catch { /* use defaults */ }
937
- return {
938
- url,
939
- score: result.score,
940
- summary,
941
- strengths,
942
- findings: result.findings,
943
- screenshotPath,
944
- tokensUsed: result.tokensUsed,
945
- designSystemScore,
946
- };
947
- }
948
- catch (err) {
949
- return {
950
- url,
951
- score: 0,
952
- summary: 'Failed to audit',
953
- strengths: [],
954
- findings: [],
955
- error: err instanceof Error ? err.message : String(err),
956
- };
957
- }
958
- }
959
767
  // ---------------------------------------------------------------------------
960
768
  // Design Token Extraction — pure DOM, no LLM calls
961
769
  // ---------------------------------------------------------------------------