rivet-design 0.11.11 → 0.11.13

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 (105) hide show
  1. package/dist/install/harnesses.d.ts.map +1 -1
  2. package/dist/install/harnesses.js +122 -4
  3. package/dist/install/harnesses.js.map +1 -1
  4. package/dist/mcp/agent-variants/SessionStore.d.ts +150 -10
  5. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  6. package/dist/mcp/agent-variants/SessionStore.js +468 -23
  7. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  8. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +143 -51
  9. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  10. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +465 -299
  11. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  12. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +5 -4
  13. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  14. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +24 -5
  15. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  16. package/dist/mcp/agent-variants/contracts.d.ts +102 -89
  17. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  18. package/dist/mcp/agent-variants/contracts.js +29 -14
  19. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  20. package/dist/mcp/agent-variants/index.d.ts +3 -3
  21. package/dist/mcp/agent-variants/index.d.ts.map +1 -1
  22. package/dist/mcp/agent-variants/index.js +1 -2
  23. package/dist/mcp/agent-variants/index.js.map +1 -1
  24. package/dist/mcp/agent-variants/previewQa.d.ts.map +1 -1
  25. package/dist/mcp/agent-variants/previewQa.js +0 -6
  26. package/dist/mcp/agent-variants/previewQa.js.map +1 -1
  27. package/dist/mcp/agent-variants/tools.d.ts +12 -4
  28. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  29. package/dist/mcp/agent-variants/tools.js +69 -13
  30. package/dist/mcp/agent-variants/tools.js.map +1 -1
  31. package/dist/mcp/changeBatchClassification.d.ts +30 -0
  32. package/dist/mcp/changeBatchClassification.d.ts.map +1 -1
  33. package/dist/mcp/changeBatchClassification.js +76 -0
  34. package/dist/mcp/changeBatchClassification.js.map +1 -1
  35. package/dist/mcp/instructions.d.ts.map +1 -1
  36. package/dist/mcp/instructions.js +7 -1
  37. package/dist/mcp/instructions.js.map +1 -1
  38. package/dist/mcp/server.d.ts +51 -3
  39. package/dist/mcp/server.d.ts.map +1 -1
  40. package/dist/mcp/server.js +404 -156
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/mcp/watchLoopResumePrompt.d.ts +16 -0
  43. package/dist/mcp/watchLoopResumePrompt.d.ts.map +1 -0
  44. package/dist/mcp/watchLoopResumePrompt.js +21 -0
  45. package/dist/mcp/watchLoopResumePrompt.js.map +1 -0
  46. package/dist/routes/agentVariants.d.ts +10 -1
  47. package/dist/routes/agentVariants.d.ts.map +1 -1
  48. package/dist/routes/agentVariants.js +214 -3
  49. package/dist/routes/agentVariants.js.map +1 -1
  50. package/dist/routes/mcp.d.ts.map +1 -1
  51. package/dist/routes/mcp.js +106 -47
  52. package/dist/routes/mcp.js.map +1 -1
  53. package/dist/services/BridgeBatchPipeline.d.ts +99 -0
  54. package/dist/services/BridgeBatchPipeline.d.ts.map +1 -0
  55. package/dist/services/BridgeBatchPipeline.js +278 -0
  56. package/dist/services/BridgeBatchPipeline.js.map +1 -0
  57. package/dist/services/BridgeQueueStore.d.ts +28 -0
  58. package/dist/services/BridgeQueueStore.d.ts.map +1 -0
  59. package/dist/services/BridgeQueueStore.js +88 -0
  60. package/dist/services/BridgeQueueStore.js.map +1 -0
  61. package/dist/services/InlineVariantGenerationService.d.ts.map +1 -1
  62. package/dist/services/InlineVariantGenerationService.js +15 -3
  63. package/dist/services/InlineVariantGenerationService.js.map +1 -1
  64. package/dist/services/SessionBridgeService.d.ts +127 -31
  65. package/dist/services/SessionBridgeService.d.ts.map +1 -1
  66. package/dist/services/SessionBridgeService.js +428 -83
  67. package/dist/services/SessionBridgeService.js.map +1 -1
  68. package/dist/services/TelemetryService.d.ts +30 -0
  69. package/dist/services/TelemetryService.d.ts.map +1 -1
  70. package/dist/services/TelemetryService.js +42 -0
  71. package/dist/services/TelemetryService.js.map +1 -1
  72. package/dist/services/WorktreeManager.d.ts +29 -2
  73. package/dist/services/WorktreeManager.d.ts.map +1 -1
  74. package/dist/services/WorktreeManager.js +73 -7
  75. package/dist/services/WorktreeManager.js.map +1 -1
  76. package/dist/services/createAgentVariantsOrchestrator.d.ts +0 -8
  77. package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
  78. package/dist/services/createAgentVariantsOrchestrator.js +1 -18
  79. package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
  80. package/dist/utils/portUtils.d.ts +9 -0
  81. package/dist/utils/portUtils.d.ts.map +1 -1
  82. package/dist/utils/portUtils.js +26 -0
  83. package/dist/utils/portUtils.js.map +1 -1
  84. package/dist/utils/skills/claude-skill.d.ts +1 -1
  85. package/dist/utils/skills/claude-skill.d.ts.map +1 -1
  86. package/dist/utils/skills/claude-skill.js +31 -12
  87. package/dist/utils/skills/claude-skill.js.map +1 -1
  88. package/dist/utils/skills/cursor-rules.d.ts +1 -1
  89. package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
  90. package/dist/utils/skills/cursor-rules.js +26 -11
  91. package/dist/utils/skills/cursor-rules.js.map +1 -1
  92. package/dist/utils/skills/shared-variants-protocol.d.ts +28 -0
  93. package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
  94. package/dist/utils/skills/shared-variants-protocol.js +88 -14
  95. package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
  96. package/package.json +3 -2
  97. package/src/ui/dist/assets/main-DREt3GGU.css +1 -0
  98. package/src/ui/dist/assets/main-Dt7ShmjM.js +655 -0
  99. package/src/ui/dist/index.html +2 -2
  100. package/dist/mcp/agent-variants/designCritique.d.ts +0 -167
  101. package/dist/mcp/agent-variants/designCritique.d.ts.map +0 -1
  102. package/dist/mcp/agent-variants/designCritique.js +0 -774
  103. package/dist/mcp/agent-variants/designCritique.js.map +0 -1
  104. package/src/ui/dist/assets/main-B1LAuLK_.js +0 -655
  105. package/src/ui/dist/assets/main-D-YwdYjm.css +0 -1
@@ -1,774 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.capturePlaywrightScreenshot = exports.runDesignCritique = exports.lintStaticPreview = exports.evaluateCritique = exports.passesDesignBar = exports.buildDesignContract = exports.inferDesignArtifactType = exports.getDesignCritiqueRouteAction = exports.CRITIQUE_CHECK_CATEGORIES = exports.MAJOR_FINDING_SCORE_CAP = exports.DIMENSION_FLOOR = exports.OVERALL_PASS_BAR = exports.DESIGN_CRITIQUE_GATEWAY_TIMEOUT_MS = void 0;
40
- const v3_1 = require("zod/v3");
41
- const fs_1 = __importDefault(require("fs"));
42
- const path_1 = __importDefault(require("path"));
43
- const logger_1 = require("../../utils/logger");
44
- const GatewayClient_1 = require("../../services/GatewayClient");
45
- const log = (0, logger_1.createLogger)('designCritique');
46
- /**
47
- * Aesthetic self-critique for 0→1 variants. Renders a generated variant with
48
- * Playwright, screenshots it, and scores the screenshot on a small design
49
- * rubric via a vision model — preferentially against the variant's bound
50
- * DESIGN.md, falling back to a generic rubric when none is bound.
51
- *
52
- * On top of the holistic aesthetic score, the critique enforces a stricter,
53
- * task-aware quality gate: a deterministic artifact lint plus the model's own
54
- * severity-coded findings across five usability checks (task fit, source
55
- * fidelity, UI polish, readability, responsive). Any `critical` finding fails
56
- * the variant outright regardless of how high its visual scores are; `major`
57
- * findings cap the overall score below the pass bar so a strong vibe can never
58
- * mask a serious defect.
59
- *
60
- * The GatewayClient and the screenshot capture are injected as seams so unit
61
- * tests can stub both and never touch the real model or launch a browser.
62
- * The runner is failure-tolerant by design: render or model errors
63
- * degrades to a non-blocking "skipped" outcome rather than failing the
64
- * variant, so an unauthenticated / headless / browserless environment never
65
- * breaks the existing objective QA gate.
66
- */
67
- /** Sonnet 4.6 is multimodal and fast enough for single-screenshot scoring. */
68
- const DESIGN_CRITIQUE_MODEL = 'claude-sonnet-4-6';
69
- const DESIGN_CRITIQUE_MAX_TOKENS = 1_024;
70
- const DESIGN_CRITIQUE_TEMPERATURE = 0;
71
- exports.DESIGN_CRITIQUE_GATEWAY_TIMEOUT_MS = 30_000;
72
- /** Default screenshot viewport — desktop-first, tall enough to capture a
73
- * typical hero + first fold without scrolling the whole page. */
74
- const SCREENSHOT_VIEWPORT_WIDTH = 1_280;
75
- const SCREENSHOT_VIEWPORT_HEIGHT = 900;
76
- /** Hard ceiling on a single render+screenshot so a hung page can't stall the
77
- * QA gate. */
78
- const SCREENSHOT_TIMEOUT_MS = 15_000;
79
- /** Short settle after force-revealing scroll/entrance-gated content, before the
80
- * capture. Two animation frames plus this give reveal classes/animations time
81
- * to land on their final visible state. The capture is networkless, so this is
82
- * a fixed settle rather than a networkidle wait. */
83
- const SCREENSHOT_SETTLE_MS = 250;
84
- /**
85
- * Capture-only stylesheet that neutralizes entrance animations and transitions
86
- * so reveal-gated content is never screenshotted mid-fade or stuck at its
87
- * pre-animation `opacity:0` start. Animations are collapsed to ~0 duration with
88
- * no delay and `forwards` fill so they settle on their final (visible) keyframe;
89
- * transitions are collapsed the same way so an IntersectionObserver class swap
90
- * applies instantly. Deliberately does NOT blanket-force `opacity:1` on every
91
- * element so genuinely-hidden UI (off-canvas menus, modals) is not surfaced —
92
- * the scroll-through below is the primary reveal trigger.
93
- */
94
- const CAPTURE_REVEAL_STYLESHEET = `*, *::before, *::after {
95
- animation-delay: 0s !important;
96
- animation-duration: 0.001s !important;
97
- animation-iteration-count: 1 !important;
98
- animation-fill-mode: forwards !important;
99
- transition-delay: 0s !important;
100
- transition-duration: 0.001s !important;
101
- }`;
102
- const STATIC_PREVIEW_ALLOWED_PROTOCOLS = new Set(['about:', 'blob:', 'data:']);
103
- const STATIC_PREVIEW_ASSET_ORIGIN = 'http://rivet-static-preview.local';
104
- /**
105
- * Must-pass bar for the aesthetic gate. Deliberately conservative: the
106
- * critique ships advisory-first to calibrate against real output before it
107
- * blocks, and the orchestrator caps regeneration at one retry so a harsh
108
- * critique can never loop. A variant passes only when its holistic score
109
- * clears `OVERALL_PASS_BAR` AND no single dimension drops below
110
- * `DIMENSION_FLOOR` (one badly broken dimension fails even a high overall).
111
- */
112
- exports.OVERALL_PASS_BAR = 6.5;
113
- exports.DIMENSION_FLOOR = 4;
114
- const MAX_SCORE = 10;
115
- const MIN_SCORE = 0;
116
- /**
117
- * Ceiling applied to the effective overall score when at least one `major`
118
- * finding is present. Sits below `OVERALL_PASS_BAR` so any unresolved major
119
- * usability issue forces a retry/degrade even when the raw visual scores are
120
- * high — a high vibe score can never carry a variant past a real defect.
121
- */
122
- exports.MAJOR_FINDING_SCORE_CAP = 5;
123
- /**
124
- * The five task-aware checks a 0→1 variant is evaluated against, beyond the
125
- * holistic aesthetic dimensions. Mirrors `CritiqueCheckCategory` in contracts.
126
- */
127
- exports.CRITIQUE_CHECK_CATEGORIES = [
128
- 'task_fit',
129
- 'source_fidelity',
130
- 'ui_polish',
131
- 'readability',
132
- 'responsive',
133
- ];
134
- const parseUrl = (value) => {
135
- try {
136
- return new URL(value);
137
- }
138
- catch {
139
- return undefined;
140
- }
141
- };
142
- /**
143
- * Decide whether a Playwright request may run while rendering a critique
144
- * target. Static HTML previews are networkless by default; URL targets are
145
- * not rendered by the critique sandbox.
146
- */
147
- const getDesignCritiqueRouteAction = (target, requestUrl) => {
148
- const parsedRequestUrl = parseUrl(requestUrl);
149
- if (!parsedRequestUrl)
150
- return 'abort';
151
- if (target.kind === 'html') {
152
- if (target.assetBase &&
153
- parsedRequestUrl.origin === STATIC_PREVIEW_ASSET_ORIGIN) {
154
- return 'allow';
155
- }
156
- return STATIC_PREVIEW_ALLOWED_PROTOCOLS.has(parsedRequestUrl.protocol)
157
- ? 'allow'
158
- : 'abort';
159
- }
160
- return 'abort';
161
- };
162
- exports.getDesignCritiqueRouteAction = getDesignCritiqueRouteAction;
163
- const assertAllowedDesignCritiqueTarget = (target) => {
164
- if (target.kind === 'html')
165
- return;
166
- throw new Error('Design critique URL targets are not supported');
167
- };
168
- const addStaticPreviewBase = (html) => {
169
- const base = `<base href="${STATIC_PREVIEW_ASSET_ORIGIN}/">`;
170
- if (/<head[\s>]/i.test(html)) {
171
- return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}\n${base}`);
172
- }
173
- return `${base}\n${html}`;
174
- };
175
- const contentTypeForAsset = (filePath) => {
176
- const extension = filePath.toLowerCase().split('.').pop();
177
- switch (extension) {
178
- case 'avif':
179
- return 'image/avif';
180
- case 'css':
181
- return 'text/css';
182
- case 'gif':
183
- return 'image/gif';
184
- case 'glb':
185
- return 'model/gltf-binary';
186
- case 'gltf':
187
- return 'model/gltf+json';
188
- case 'htm':
189
- case 'html':
190
- return 'text/html';
191
- case 'jpeg':
192
- case 'jpg':
193
- return 'image/jpeg';
194
- case 'js':
195
- return 'text/javascript';
196
- case 'png':
197
- return 'image/png';
198
- case 'svg':
199
- return 'image/svg+xml';
200
- case 'webp':
201
- return 'image/webp';
202
- case 'woff':
203
- return 'font/woff';
204
- case 'woff2':
205
- return 'font/woff2';
206
- default:
207
- return 'application/octet-stream';
208
- }
209
- };
210
- const resolveStaticPreviewCritiqueAsset = (target, requestUrl) => {
211
- if (target.kind !== 'html' || !target.assetBase)
212
- return null;
213
- const parsedRequestUrl = parseUrl(requestUrl);
214
- if (!parsedRequestUrl)
215
- return null;
216
- if (parsedRequestUrl.origin !== STATIC_PREVIEW_ASSET_ORIGIN)
217
- return null;
218
- const relativePath = decodeURIComponent(parsedRequestUrl.pathname.slice(1));
219
- if (!relativePath)
220
- return null;
221
- const candidate = path_1.default.resolve(target.assetBase, relativePath);
222
- if (candidate !== target.assetBase &&
223
- !candidate.startsWith(target.assetBase + path_1.default.sep)) {
224
- return null;
225
- }
226
- try {
227
- const realTarget = fs_1.default.realpathSync(candidate);
228
- if (realTarget !== target.assetBase &&
229
- !realTarget.startsWith(target.assetBase + path_1.default.sep)) {
230
- return null;
231
- }
232
- const stat = fs_1.default.statSync(realTarget);
233
- if (!stat.isFile() || stat.nlink !== 1)
234
- return null;
235
- return realTarget;
236
- }
237
- catch {
238
- return null;
239
- }
240
- };
241
- const critiqueFindingSchema = v3_1.z.object({
242
- severity: v3_1.z.enum(['critical', 'major', 'minor']),
243
- category: v3_1.z.enum([
244
- 'task_fit',
245
- 'source_fidelity',
246
- 'ui_polish',
247
- 'readability',
248
- 'responsive',
249
- ]),
250
- evidence: v3_1.z.string(),
251
- fix: v3_1.z.string(),
252
- });
253
- const designScoresSchema = v3_1.z.object({
254
- hierarchy: v3_1.z.number(),
255
- typography: v3_1.z.number(),
256
- color: v3_1.z.number(),
257
- spacing: v3_1.z.number(),
258
- slopResistance: v3_1.z.number(),
259
- overall: v3_1.z.number(),
260
- critique: v3_1.z.string(),
261
- // Optional so existing stubs (and a model that surfaces no issues) parse
262
- // cleanly; absence is treated as "no findings".
263
- findings: v3_1.z.array(critiqueFindingSchema).optional(),
264
- });
265
- const SUBMIT_DESIGN_SCORES_TOOL = {
266
- name: 'submit_design_scores',
267
- strict: true,
268
- description: 'Submit the per-dimension aesthetic scores (0-10), a short critique, and any severity-coded findings for the rendered variant. Call exactly once.',
269
- input_schema: {
270
- type: 'object',
271
- properties: {
272
- hierarchy: { type: 'number' },
273
- typography: { type: 'number' },
274
- color: { type: 'number' },
275
- spacing: { type: 'number' },
276
- slopResistance: { type: 'number' },
277
- overall: { type: 'number' },
278
- critique: { type: 'string' },
279
- findings: {
280
- type: 'array',
281
- description: 'Concrete usability/quality issues. Use severity "critical" for a defect that makes the variant unusable (unreadable text, broken layout, missing required content/action), "major" for a serious issue that must be fixed before this ships, "minor" for a nit. Each finding names the affected check category and gives evidence + a concrete fix.',
282
- items: {
283
- type: 'object',
284
- properties: {
285
- severity: { type: 'string', enum: ['critical', 'major', 'minor'] },
286
- category: {
287
- type: 'string',
288
- enum: [
289
- 'task_fit',
290
- 'source_fidelity',
291
- 'ui_polish',
292
- 'readability',
293
- 'responsive',
294
- ],
295
- },
296
- evidence: { type: 'string' },
297
- fix: { type: 'string' },
298
- },
299
- required: ['severity', 'category', 'evidence', 'fix'],
300
- additionalProperties: false,
301
- },
302
- },
303
- },
304
- required: [
305
- 'hierarchy',
306
- 'typography',
307
- 'color',
308
- 'spacing',
309
- 'slopResistance',
310
- 'overall',
311
- 'critique',
312
- 'findings',
313
- ],
314
- additionalProperties: false,
315
- },
316
- };
317
- const DESIGN_CRITIQUE_SYSTEM_PROMPT = `You are a discerning senior product designer doing a fast, honest visual QA pass on a single rendered web UI screenshot.
318
-
319
- Score the screenshot on each dimension from 0 (broken / generic) to 10 (exceptional, ships at a top design-led company):
320
- - hierarchy: is the visual hierarchy and focal flow clear and intentional?
321
- - typography: type scale, pairing, rhythm, and legibility.
322
- - color: palette cohesion and disciplined use of color roles.
323
- - spacing: spacing rhythm, alignment, and density.
324
- - slopResistance: how far it is from generic "AI slop" (boilerplate centered hero card, three feature columns, default shadows, lorem-ipsum vibe). Higher = more distinctive and considered.
325
-
326
- Then give an overall 0-10 holistic verdict and a one-paragraph critique naming the most important concrete fixes. Be calibrated and critical — most first-draft AI output lands in the 4-7 range.
327
-
328
- Separately, raise severity-coded findings for concrete usability/quality problems across these checks:
329
- - task_fit: does it actually do the requested job (right artifact, required sections/actions present)?
330
- - source_fidelity: does it honor explicit required content, assets, and constraints from the request?
331
- - ui_polish: clipped/overlapping/misaligned elements, broken images, placeholder text, visual artifacts.
332
- - readability: is primary text and are key controls legible (contrast, size, not clipped or hidden)?
333
- - responsive: is core content and the primary action usable at the review viewport?
334
-
335
- Use severity "critical" for anything that makes the variant unusable or fails the requested task — a critical fails the variant no matter how high the visual scores are. Use "major" for a serious issue that must be fixed before shipping. Use "minor" for nits. Do NOT invent findings; only raise issues you can actually see. Submit via the submit_design_scores tool exactly once.`;
336
- const TASK_FIT_RUBRIC = {
337
- landing_page: 'This is a LANDING PAGE: require a clear message hierarchy, an obvious primary call-to-action above the fold, and trust/intent signals. A landing page with no primary action is a critical task_fit failure.',
338
- dashboard: 'This is a DASHBOARD: require clear data hierarchy, scannable groupings, sensible control placement, and legible metrics. Unreadable or unlabeled data is a readability/task_fit issue.',
339
- form: 'This is a FORM: require labeled, legible inputs, an obvious submit action, and visible field affordances. Unlabeled or unusable inputs are a critical task_fit failure.',
340
- app_screen: 'This is an APP SCREEN: require a clear primary task, usable navigation/controls, and legible content for the core action.',
341
- generic: 'Judge it against the most reasonable interpretation of the requested artifact: the core content and primary action must be present and usable.',
342
- };
343
- const PLACEHOLDER_TEXT_PATTERN = /lorem ipsum|dolor sit amet/i;
344
- const HTML_TAG_PATTERN = /<[^>]+>/g;
345
- const SCRIPT_STYLE_PATTERN = /<(script|style)[\s\S]*?<\/\1>/gi;
346
- /** Minimum count of rendered, non-whitespace text characters before the body
347
- * is treated as having visible content. Below this the variant is effectively
348
- * blank. */
349
- const MIN_VISIBLE_TEXT_CHARS = 1;
350
- const LANDING_ACTION_PATTERN = /<(a|button)\b|role\s*=\s*["']button["']/i;
351
- const BROKEN_IMG_PATTERN = /<img\b(?![^>]*\bsrc\s*=\s*("|')\s*[^"'\s][^"']*\1)[^>]*>/i;
352
- const REQUIRED_ASSET_PATH_PATTERN = /\b[\w@./ -]+\/([\w@ -]+\.(?:glb|gltf|obj|fbx|usdz|png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|m4a|woff2?|ttf|otf|eot|pdf))\b/gi;
353
- const ARTIFACT_TYPE_KEYWORDS = [
354
- [
355
- 'landing_page',
356
- /landing page|marketing site|hero section|homepage|home page/i,
357
- ],
358
- ['dashboard', /dashboard|analytics|admin panel|metrics|reporting/i],
359
- ['form', /\bform\b|sign[ -]?up|sign[ -]?in|login|checkout|contact page/i],
360
- [
361
- 'app_screen',
362
- /\bapp\b|screen|todo|kanban|timer|settings page|profile page/i,
363
- ],
364
- ];
365
- /**
366
- * Infer the artifact type from free-text request copy (prompt and/or brief).
367
- * Falls back to `generic` when nothing matches so the gate stays usable for
368
- * arbitrary requests.
369
- */
370
- const inferDesignArtifactType = (text) => {
371
- for (const [type, pattern] of ARTIFACT_TYPE_KEYWORDS) {
372
- if (pattern.test(text))
373
- return type;
374
- }
375
- return 'generic';
376
- };
377
- exports.inferDesignArtifactType = inferDesignArtifactType;
378
- /**
379
- * Derive the design contract from the request copy. `artifactType` drives the
380
- * task-specific rubric; `requirements` echoes the raw request lines so the
381
- * critique prompt can hold the model to explicit asks.
382
- */
383
- const buildDesignContract = (input) => {
384
- const prompt = input.prompt?.trim() ?? '';
385
- const brief = input.brief?.trim() ?? '';
386
- const requirements = [prompt, brief].filter((line) => line.length > 0);
387
- const requiredAssetFilenames = extractRequiredAssetFilenames(requirements);
388
- return {
389
- artifactType: (0, exports.inferDesignArtifactType)(`${prompt}\n${brief}`),
390
- requirements,
391
- requiredAssetFilenames,
392
- };
393
- };
394
- exports.buildDesignContract = buildDesignContract;
395
- const extractRequiredAssetFilenames = (requirements) => {
396
- const filenames = new Set();
397
- for (const requirement of requirements) {
398
- REQUIRED_ASSET_PATH_PATTERN.lastIndex = 0;
399
- let match;
400
- while ((match = REQUIRED_ASSET_PATH_PATTERN.exec(requirement))) {
401
- filenames.add(match[1].toLowerCase());
402
- }
403
- }
404
- return [...filenames];
405
- };
406
- const buildCritiquePrompt = (designContextMarkdown, designContract) => {
407
- const sections = [];
408
- if (designContextMarkdown && designContextMarkdown.trim().length > 0) {
409
- sections.push(`Score the attached screenshot against the design system it is meant to express. Judge fidelity to this DESIGN.md, not just generic taste.\n\n--- DESIGN.md ---\n${designContextMarkdown}\n--- end DESIGN.md ---`);
410
- }
411
- else {
412
- sections.push('No bound DESIGN.md — score the attached screenshot on general product-design quality using the rubric.');
413
- }
414
- if (designContract) {
415
- sections.push(TASK_FIT_RUBRIC[designContract.artifactType]);
416
- if (designContract.requirements.length > 0) {
417
- sections.push(`The request asked for:\n${designContract.requirements
418
- .map((line) => `- ${line}`)
419
- .join('\n')}\nRaise source_fidelity findings for anything explicitly required that is missing or wrong.`);
420
- }
421
- }
422
- return sections.join('\n\n');
423
- };
424
- const clampScore = (value) => Math.max(MIN_SCORE, Math.min(MAX_SCORE, value));
425
- /**
426
- * Apply the must-pass bar to a set of dimension scores. Returns true only
427
- * when the holistic score clears `OVERALL_PASS_BAR` and every dimension is at
428
- * or above `DIMENSION_FLOOR`.
429
- */
430
- const passesDesignBar = (scores) => {
431
- if (scores.overall < exports.OVERALL_PASS_BAR)
432
- return false;
433
- const dimensions = [
434
- scores.hierarchy,
435
- scores.typography,
436
- scores.color,
437
- scores.spacing,
438
- scores.slopResistance,
439
- ];
440
- return dimensions.every((value) => value >= exports.DIMENSION_FLOOR);
441
- };
442
- exports.passesDesignBar = passesDesignBar;
443
- const statusForSeverity = (severity) => severity === 'minor' ? 'warn' : 'fail';
444
- /** Derive per-category pass/warn/fail status from the gathered findings. */
445
- const deriveChecks = (findings) => {
446
- const checks = {};
447
- for (const category of exports.CRITIQUE_CHECK_CATEGORIES) {
448
- checks[category] = 'pass';
449
- }
450
- for (const finding of findings) {
451
- const next = statusForSeverity(finding.severity);
452
- if (next === 'fail' || checks[finding.category] === 'pass') {
453
- // fail dominates warn; warn dominates pass.
454
- if (next === 'fail' || checks[finding.category] !== 'fail') {
455
- checks[finding.category] = next;
456
- }
457
- }
458
- }
459
- return checks;
460
- };
461
- /**
462
- * Apply the strict critique gate: a `critical` finding fails the variant
463
- * outright and caps the overall score to the floor; a `major` finding caps the
464
- * overall score below the pass bar. The variant passes only when no critical is
465
- * present and the (capped) overall + dimension floors clear the aesthetic bar.
466
- */
467
- const evaluateCritique = (scores, findings) => {
468
- const hasCritical = findings.some((f) => f.severity === 'critical');
469
- const hasMajor = findings.some((f) => f.severity === 'major');
470
- const scoreCaps = [];
471
- let overallScore = scores.overall;
472
- if (hasCritical) {
473
- overallScore = MIN_SCORE;
474
- for (const finding of findings) {
475
- if (finding.severity === 'critical') {
476
- scoreCaps.push({
477
- category: finding.category,
478
- max: MIN_SCORE,
479
- reason: `Critical: ${finding.evidence}`,
480
- });
481
- }
482
- }
483
- }
484
- else if (hasMajor) {
485
- overallScore = Math.min(overallScore, exports.MAJOR_FINDING_SCORE_CAP);
486
- for (const finding of findings) {
487
- if (finding.severity === 'major') {
488
- scoreCaps.push({
489
- category: finding.category,
490
- max: exports.MAJOR_FINDING_SCORE_CAP,
491
- reason: `Major: ${finding.evidence}`,
492
- });
493
- }
494
- }
495
- }
496
- const passed = !hasCritical && (0, exports.passesDesignBar)({ ...scores, overall: overallScore });
497
- return {
498
- passed,
499
- blocked: hasCritical,
500
- overallScore,
501
- scoreCaps,
502
- checks: deriveChecks(findings),
503
- findings,
504
- };
505
- };
506
- exports.evaluateCritique = evaluateCritique;
507
- const stripToVisibleText = (html) => html
508
- .replace(SCRIPT_STYLE_PATTERN, ' ')
509
- .replace(HTML_TAG_PATTERN, ' ')
510
- .replace(/&[a-z]+;/gi, ' ')
511
- .replace(/\s+/g, ' ')
512
- .trim();
513
- /**
514
- * Deterministic artifact lint over a static-preview HTML document. Catches
515
- * mechanical defects the vision model can underweight — a blank document,
516
- * placeholder copy, broken images, or a landing page with no primary action —
517
- * and emits them as severity-coded findings so they feed the same gate as the
518
- * model's findings. Runs before the subjective critique; never throws.
519
- */
520
- const lintStaticPreview = (html, contract) => {
521
- const findings = [];
522
- const visibleText = stripToVisibleText(html);
523
- if (visibleText.length < MIN_VISIBLE_TEXT_CHARS) {
524
- findings.push({
525
- severity: 'critical',
526
- category: 'readability',
527
- evidence: 'The rendered document has no visible text content.',
528
- fix: 'Render the core content of the requested artifact as visible text.',
529
- });
530
- }
531
- if (PLACEHOLDER_TEXT_PATTERN.test(visibleText)) {
532
- findings.push({
533
- severity: 'major',
534
- category: 'ui_polish',
535
- evidence: 'Placeholder lorem-ipsum copy is present in the output.',
536
- fix: 'Replace placeholder copy with real, task-appropriate content.',
537
- });
538
- }
539
- if (BROKEN_IMG_PATTERN.test(html)) {
540
- findings.push({
541
- severity: 'major',
542
- category: 'ui_polish',
543
- evidence: 'An <img> element is missing a usable src.',
544
- fix: 'Give every image a valid src or remove the broken element.',
545
- });
546
- }
547
- if (contract?.artifactType === 'landing_page' &&
548
- !LANDING_ACTION_PATTERN.test(html)) {
549
- findings.push({
550
- severity: 'critical',
551
- category: 'task_fit',
552
- evidence: 'A landing page was requested but it has no primary action (link or button).',
553
- fix: 'Add a clear primary call-to-action (a link or button) above the fold.',
554
- });
555
- }
556
- for (const filename of contract?.requiredAssetFilenames ?? []) {
557
- if (html.toLowerCase().includes(filename))
558
- continue;
559
- findings.push({
560
- severity: 'critical',
561
- category: 'source_fidelity',
562
- evidence: `The output does not reference the explicitly requested asset '${filename}'.`,
563
- fix: `Use the requested '${filename}' asset in the generated preview instead of recreating or omitting it.`,
564
- });
565
- }
566
- return findings;
567
- };
568
- exports.lintStaticPreview = lintStaticPreview;
569
- /**
570
- * Render → screenshot → VLM rubric → strict gate → verdict. Never throws:
571
- * render/model failures resolve to a non-blocking skipped outcome.
572
- */
573
- const runDesignCritique = async (input, deps = {}) => {
574
- if (input.target.kind === 'url') {
575
- log.warn('Design critique skipped — live URL targets are not supported');
576
- return {
577
- ran: false,
578
- passed: true,
579
- blocked: false,
580
- findings: [],
581
- scoreCaps: [],
582
- skippedReason: 'live_url_target',
583
- };
584
- }
585
- const screenshot = deps.screenshot ?? exports.capturePlaywrightScreenshot;
586
- const gatewayClient = deps.gatewayClient ?? new GatewayClient_1.GatewayClient();
587
- let image;
588
- try {
589
- image = await screenshot(input.target);
590
- }
591
- catch (err) {
592
- const reason = err instanceof Error ? err.message : String(err);
593
- log.warn(`Design critique skipped — screenshot failed: ${reason}`);
594
- return {
595
- ran: false,
596
- passed: true,
597
- blocked: false,
598
- findings: [],
599
- scoreCaps: [],
600
- skippedReason: `screenshot: ${reason}`,
601
- };
602
- }
603
- let raw;
604
- try {
605
- const toolInput = await gatewayClient.scoreImage({
606
- model: DESIGN_CRITIQUE_MODEL,
607
- maxTokens: DESIGN_CRITIQUE_MAX_TOKENS,
608
- temperature: DESIGN_CRITIQUE_TEMPERATURE,
609
- system: DESIGN_CRITIQUE_SYSTEM_PROMPT,
610
- prompt: buildCritiquePrompt(input.designContextMarkdown, input.designContract),
611
- images: [image],
612
- tool: SUBMIT_DESIGN_SCORES_TOOL,
613
- timeoutMs: exports.DESIGN_CRITIQUE_GATEWAY_TIMEOUT_MS,
614
- });
615
- const parsed = designScoresSchema.safeParse(toolInput);
616
- if (!parsed.success) {
617
- log.warn('Design critique skipped — model response failed schema');
618
- return {
619
- ran: false,
620
- passed: true,
621
- blocked: false,
622
- findings: [],
623
- scoreCaps: [],
624
- skippedReason: 'invalid_model_response',
625
- };
626
- }
627
- raw = parsed.data;
628
- }
629
- catch (err) {
630
- const reason = err instanceof Error ? err.message : String(err);
631
- log.warn(`Design critique skipped — scoring failed: ${reason}`);
632
- return {
633
- ran: false,
634
- passed: true,
635
- blocked: false,
636
- findings: [],
637
- scoreCaps: [],
638
- skippedReason: `score: ${reason}`,
639
- };
640
- }
641
- const scores = {
642
- hierarchy: clampScore(raw.hierarchy),
643
- typography: clampScore(raw.typography),
644
- color: clampScore(raw.color),
645
- spacing: clampScore(raw.spacing),
646
- slopResistance: clampScore(raw.slopResistance),
647
- overall: clampScore(raw.overall),
648
- };
649
- // Deterministic artifact lint runs only when the rendered HTML is available
650
- // (static_preview). vite_app targets are URL-only here, so they rely on the
651
- // model's findings alone.
652
- const lintFindings = input.target.kind === 'html'
653
- ? (0, exports.lintStaticPreview)(input.target.html, input.designContract)
654
- : [];
655
- const findings = [
656
- ...lintFindings,
657
- ...(raw.findings ?? []),
658
- ];
659
- const evaluation = (0, exports.evaluateCritique)(scores, findings);
660
- return {
661
- ran: true,
662
- passed: evaluation.passed,
663
- blocked: evaluation.blocked,
664
- scores,
665
- overallScore: evaluation.overallScore,
666
- findings: evaluation.findings,
667
- checks: evaluation.checks,
668
- scoreCaps: evaluation.scoreCaps,
669
- summary: raw.critique,
670
- };
671
- };
672
- exports.runDesignCritique = runDesignCritique;
673
- /**
674
- * Force scroll/entrance-gated content into its visible state before a capture.
675
- *
676
- * Generated variants routinely hide their hero and sections behind
677
- * IntersectionObserver scroll reveals or entrance animations that start at
678
- * `opacity:0`. A screenshot taken immediately after `domcontentloaded` can catch
679
- * that content still hidden (or mid-fade), so the vision critique scores an
680
- * otherwise-good variant as a blank canvas and the orchestrator re-queues it.
681
- *
682
- * Two steps fix this without a networkidle wait (the capture is networkless):
683
- * 1. Inject {@link CAPTURE_REVEAL_STYLESHEET} so animations/transitions settle
684
- * on their final visible keyframe instead of being caught mid-fade.
685
- * 2. Scroll through the full document height in viewport-sized steps so every
686
- * IntersectionObserver reveal fires, then return to the top for the capture.
687
- * A couple of animation frames plus a short fixed settle let the reveals land.
688
- *
689
- * @effect Mutates the page's scroll position and injects a capture-only style.
690
- */
691
- const revealGatedContent = async (page) => {
692
- await page.addStyleTag({ content: CAPTURE_REVEAL_STYLESHEET });
693
- await page.evaluate(async () => {
694
- const settle = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
695
- const viewportHeight = window.innerHeight || 900;
696
- const documentHeight = Math.max(document.body?.scrollHeight ?? 0, document.documentElement?.scrollHeight ?? 0, viewportHeight);
697
- for (let y = 0; y <= documentHeight; y += viewportHeight) {
698
- window.scrollTo(0, y);
699
- await settle();
700
- }
701
- window.scrollTo(0, 0);
702
- await settle();
703
- });
704
- await page.waitForTimeout(SCREENSHOT_SETTLE_MS);
705
- };
706
- /**
707
- * Default screenshot capturer backed by Playwright Chromium. Lazily imports
708
- * playwright-core so the dependency is only loaded when a real critique runs
709
- * (unit tests inject a stub and never reach this path).
710
- *
711
- * Uses the `chromium-headless-shell` channel so only the ~190 MB headless shell
712
- * is required, not the full ~340 MB Chromium build. See
713
- * docs/browser-source-evidence-plan.md for the headless-Chromium-only decision.
714
- *
715
- * @effect Launches the headless Chromium shell and renders the target.
716
- */
717
- const capturePlaywrightScreenshot = async (target) => {
718
- assertAllowedDesignCritiqueTarget(target);
719
- // Lazy import keeps Playwright out of the module's static dependency graph
720
- // for callers that always inject a stub screenshot seam.
721
- const { chromium } = await Promise.resolve().then(() => __importStar(require('playwright-core')));
722
- const browser = await chromium.launch({
723
- headless: true,
724
- channel: 'chromium-headless-shell',
725
- });
726
- try {
727
- const page = await browser.newPage({
728
- viewport: {
729
- width: SCREENSHOT_VIEWPORT_WIDTH,
730
- height: SCREENSHOT_VIEWPORT_HEIGHT,
731
- },
732
- });
733
- await page.routeWebSocket('**/*', (ws) => ws.close());
734
- await page.route('**/*', async (route) => {
735
- const assetPath = resolveStaticPreviewCritiqueAsset(target, route.request().url());
736
- if (assetPath) {
737
- await route.fulfill({
738
- path: assetPath,
739
- contentType: contentTypeForAsset(assetPath),
740
- });
741
- return;
742
- }
743
- const action = (0, exports.getDesignCritiqueRouteAction)(target, route.request().url());
744
- if (action === 'allow') {
745
- await route.continue();
746
- return;
747
- }
748
- await route.abort();
749
- });
750
- if (target.kind === 'html') {
751
- const html = target.assetBase
752
- ? addStaticPreviewBase(target.html)
753
- : target.html;
754
- await page.setContent(html, {
755
- waitUntil: 'domcontentloaded',
756
- timeout: SCREENSHOT_TIMEOUT_MS,
757
- });
758
- }
759
- else {
760
- await page.goto(target.url, {
761
- waitUntil: 'domcontentloaded',
762
- timeout: SCREENSHOT_TIMEOUT_MS,
763
- });
764
- }
765
- await revealGatedContent(page);
766
- const buffer = await page.screenshot({ type: 'png' });
767
- return { mediaType: 'image/png', data: buffer.toString('base64') };
768
- }
769
- finally {
770
- await browser.close();
771
- }
772
- };
773
- exports.capturePlaywrightScreenshot = capturePlaywrightScreenshot;
774
- //# sourceMappingURL=designCritique.js.map