rivet-design 0.10.7 → 0.10.9

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 (170) hide show
  1. package/dist/index.d.ts +54 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +302 -129
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp/agent-variants/SessionStore.d.ts +23 -1
  6. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/SessionStore.js +42 -0
  8. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  9. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +99 -2
  10. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +734 -74
  12. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  13. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +8 -2
  14. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  15. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +24 -5
  16. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  17. package/dist/mcp/agent-variants/contracts.d.ts +1175 -233
  18. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  19. package/dist/mcp/agent-variants/contracts.js +12 -4
  20. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  21. package/dist/mcp/agent-variants/designCritique.d.ts +163 -0
  22. package/dist/mcp/agent-variants/designCritique.d.ts.map +1 -0
  23. package/dist/mcp/agent-variants/designCritique.js +710 -0
  24. package/dist/mcp/agent-variants/designCritique.js.map +1 -0
  25. package/dist/mcp/agent-variants/diffQa.d.ts +7 -0
  26. package/dist/mcp/agent-variants/diffQa.d.ts.map +1 -0
  27. package/dist/mcp/agent-variants/diffQa.js +67 -0
  28. package/dist/mcp/agent-variants/diffQa.js.map +1 -0
  29. package/dist/mcp/agent-variants/index.d.ts +2 -2
  30. package/dist/mcp/agent-variants/index.d.ts.map +1 -1
  31. package/dist/mcp/agent-variants/index.js +2 -1
  32. package/dist/mcp/agent-variants/index.js.map +1 -1
  33. package/dist/mcp/agent-variants/pinterestSourceContext.d.ts.map +1 -1
  34. package/dist/mcp/agent-variants/pinterestSourceContext.js +11 -1
  35. package/dist/mcp/agent-variants/pinterestSourceContext.js.map +1 -1
  36. package/dist/mcp/agent-variants/previewQa.d.ts.map +1 -1
  37. package/dist/mcp/agent-variants/previewQa.js +7 -0
  38. package/dist/mcp/agent-variants/previewQa.js.map +1 -1
  39. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  40. package/dist/mcp/agent-variants/tools.js +15 -5
  41. package/dist/mcp/agent-variants/tools.js.map +1 -1
  42. package/dist/mcp/auth/httpOAuthProvider.d.ts +103 -0
  43. package/dist/mcp/auth/httpOAuthProvider.d.ts.map +1 -0
  44. package/dist/mcp/auth/httpOAuthProvider.js +454 -0
  45. package/dist/mcp/auth/httpOAuthProvider.js.map +1 -0
  46. package/dist/mcp/auth/tools.d.ts +2 -0
  47. package/dist/mcp/auth/tools.d.ts.map +1 -1
  48. package/dist/mcp/auth/tools.js +12 -5
  49. package/dist/mcp/auth/tools.js.map +1 -1
  50. package/dist/mcp/httpServer.d.ts +36 -0
  51. package/dist/mcp/httpServer.d.ts.map +1 -0
  52. package/dist/mcp/httpServer.js +307 -0
  53. package/dist/mcp/httpServer.js.map +1 -0
  54. package/dist/mcp/server.d.ts +17 -0
  55. package/dist/mcp/server.d.ts.map +1 -1
  56. package/dist/mcp/server.js +50 -29
  57. package/dist/mcp/server.js.map +1 -1
  58. package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
  59. package/dist/proxy-middleware/proxy-config.js +5 -2
  60. package/dist/proxy-middleware/proxy-config.js.map +1 -1
  61. package/dist/routes/agentVariants.d.ts +2 -1
  62. package/dist/routes/agentVariants.d.ts.map +1 -1
  63. package/dist/routes/agentVariants.js +268 -19
  64. package/dist/routes/agentVariants.js.map +1 -1
  65. package/dist/routes/design.d.ts.map +1 -1
  66. package/dist/routes/design.js +0 -122
  67. package/dist/routes/design.js.map +1 -1
  68. package/dist/routes/mcp.d.ts.map +1 -1
  69. package/dist/routes/mcp.js +2 -1
  70. package/dist/routes/mcp.js.map +1 -1
  71. package/dist/server.d.ts +15 -0
  72. package/dist/server.d.ts.map +1 -1
  73. package/dist/server.js +24 -11
  74. package/dist/server.js.map +1 -1
  75. package/dist/services/AgentSessionService.d.ts +5 -1
  76. package/dist/services/AgentSessionService.d.ts.map +1 -1
  77. package/dist/services/AgentSessionService.js +11 -4
  78. package/dist/services/AgentSessionService.js.map +1 -1
  79. package/dist/services/AuthService.d.ts +1 -0
  80. package/dist/services/AuthService.d.ts.map +1 -1
  81. package/dist/services/AuthService.js +11 -1
  82. package/dist/services/AuthService.js.map +1 -1
  83. package/dist/services/ConfigManager.d.ts +5 -0
  84. package/dist/services/ConfigManager.d.ts.map +1 -1
  85. package/dist/services/ConfigManager.js +25 -3
  86. package/dist/services/ConfigManager.js.map +1 -1
  87. package/dist/services/DevServerRuntimeService.d.ts +119 -0
  88. package/dist/services/DevServerRuntimeService.d.ts.map +1 -0
  89. package/dist/services/DevServerRuntimeService.js +657 -0
  90. package/dist/services/DevServerRuntimeService.js.map +1 -0
  91. package/dist/services/GatewayClient.d.ts +25 -0
  92. package/dist/services/GatewayClient.d.ts.map +1 -1
  93. package/dist/services/GatewayClient.js +70 -11
  94. package/dist/services/GatewayClient.js.map +1 -1
  95. package/dist/services/InlineVariantGenerationService.d.ts +2 -3
  96. package/dist/services/InlineVariantGenerationService.d.ts.map +1 -1
  97. package/dist/services/InlineVariantGenerationService.js +7 -5
  98. package/dist/services/InlineVariantGenerationService.js.map +1 -1
  99. package/dist/services/IntegrationsClient.d.ts +20 -0
  100. package/dist/services/IntegrationsClient.d.ts.map +1 -1
  101. package/dist/services/IntegrationsClient.js +82 -24
  102. package/dist/services/IntegrationsClient.js.map +1 -1
  103. package/dist/services/RequestAuthContext.d.ts +7 -1
  104. package/dist/services/RequestAuthContext.d.ts.map +1 -1
  105. package/dist/services/RequestAuthContext.js +15 -2
  106. package/dist/services/RequestAuthContext.js.map +1 -1
  107. package/dist/services/SessionBridgeService.d.ts +1 -0
  108. package/dist/services/SessionBridgeService.d.ts.map +1 -1
  109. package/dist/services/SessionBridgeService.js +16 -1
  110. package/dist/services/SessionBridgeService.js.map +1 -1
  111. package/dist/services/TelemetryService.d.ts +2 -0
  112. package/dist/services/TelemetryService.d.ts.map +1 -1
  113. package/dist/services/TelemetryService.js +2 -0
  114. package/dist/services/TelemetryService.js.map +1 -1
  115. package/dist/services/VariantHistoryService.d.ts +8 -0
  116. package/dist/services/VariantHistoryService.d.ts.map +1 -1
  117. package/dist/services/VariantHistoryService.js +23 -0
  118. package/dist/services/VariantHistoryService.js.map +1 -1
  119. package/dist/services/VariantRunService.d.ts +56 -0
  120. package/dist/services/VariantRunService.d.ts.map +1 -0
  121. package/dist/services/VariantRunService.js +56 -0
  122. package/dist/services/VariantRunService.js.map +1 -0
  123. package/dist/services/VariantsRuntime.d.ts +22 -0
  124. package/dist/services/VariantsRuntime.d.ts.map +1 -0
  125. package/dist/services/VariantsRuntime.js +32 -0
  126. package/dist/services/VariantsRuntime.js.map +1 -0
  127. package/dist/services/VisualVariantAgentRunner.d.ts +20 -0
  128. package/dist/services/VisualVariantAgentRunner.d.ts.map +1 -0
  129. package/dist/services/VisualVariantAgentRunner.js +66 -0
  130. package/dist/services/VisualVariantAgentRunner.js.map +1 -0
  131. package/dist/services/WorktreeManager.d.ts +34 -0
  132. package/dist/services/WorktreeManager.d.ts.map +1 -1
  133. package/dist/services/WorktreeManager.js +172 -23
  134. package/dist/services/WorktreeManager.js.map +1 -1
  135. package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
  136. package/dist/services/createAgentVariantsOrchestrator.js +9 -0
  137. package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
  138. package/dist/utils/devServerCommand.d.ts +11 -4
  139. package/dist/utils/devServerCommand.d.ts.map +1 -1
  140. package/dist/utils/devServerCommand.js +17 -8
  141. package/dist/utils/devServerCommand.js.map +1 -1
  142. package/dist/utils/devServerError.d.ts +34 -0
  143. package/dist/utils/devServerError.d.ts.map +1 -0
  144. package/dist/utils/devServerError.js +39 -0
  145. package/dist/utils/devServerError.js.map +1 -0
  146. package/dist/utils/elementRefToContext.d.ts +4 -0
  147. package/dist/utils/elementRefToContext.d.ts.map +1 -0
  148. package/dist/utils/elementRefToContext.js +63 -0
  149. package/dist/utils/elementRefToContext.js.map +1 -0
  150. package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
  151. package/dist/utils/skills/shared-variants-protocol.js +5 -2
  152. package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
  153. package/dist/utils/variantSessionStart.d.ts +3 -0
  154. package/dist/utils/variantSessionStart.d.ts.map +1 -0
  155. package/dist/utils/variantSessionStart.js +7 -0
  156. package/dist/utils/variantSessionStart.js.map +1 -0
  157. package/package.json +3 -1
  158. package/src/ui/dist/assets/main-Cwwhwfjq.js +645 -0
  159. package/src/ui/dist/assets/main-Do6whVIm.css +1 -0
  160. package/src/ui/dist/index.html +2 -2
  161. package/dist/services/CommentVariationService.d.ts +0 -34
  162. package/dist/services/CommentVariationService.d.ts.map +0 -1
  163. package/dist/services/CommentVariationService.js +0 -136
  164. package/dist/services/CommentVariationService.js.map +0 -1
  165. package/dist/services/VariantCodeGeneratorService.d.ts +0 -39
  166. package/dist/services/VariantCodeGeneratorService.d.ts.map +0 -1
  167. package/dist/services/VariantCodeGeneratorService.js +0 -109
  168. package/dist/services/VariantCodeGeneratorService.js.map +0 -1
  169. package/src/ui/dist/assets/main-DUIrSkV3.css +0 -1
  170. package/src/ui/dist/assets/main-DYpxGvCu.js +0 -646
@@ -0,0 +1,710 @@
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
+ const STATIC_PREVIEW_ALLOWED_PROTOCOLS = new Set(['about:', 'blob:', 'data:']);
80
+ const STATIC_PREVIEW_ASSET_ORIGIN = 'http://rivet-static-preview.local';
81
+ /**
82
+ * Must-pass bar for the aesthetic gate. Deliberately conservative: the
83
+ * critique ships advisory-first to calibrate against real output before it
84
+ * blocks, and the orchestrator caps regeneration at one retry so a harsh
85
+ * critique can never loop. A variant passes only when its holistic score
86
+ * clears `OVERALL_PASS_BAR` AND no single dimension drops below
87
+ * `DIMENSION_FLOOR` (one badly broken dimension fails even a high overall).
88
+ */
89
+ exports.OVERALL_PASS_BAR = 6.5;
90
+ exports.DIMENSION_FLOOR = 4;
91
+ const MAX_SCORE = 10;
92
+ const MIN_SCORE = 0;
93
+ /**
94
+ * Ceiling applied to the effective overall score when at least one `major`
95
+ * finding is present. Sits below `OVERALL_PASS_BAR` so any unresolved major
96
+ * usability issue forces a retry/degrade even when the raw visual scores are
97
+ * high — a high vibe score can never carry a variant past a real defect.
98
+ */
99
+ exports.MAJOR_FINDING_SCORE_CAP = 5;
100
+ /**
101
+ * The five task-aware checks a 0→1 variant is evaluated against, beyond the
102
+ * holistic aesthetic dimensions. Mirrors `CritiqueCheckCategory` in contracts.
103
+ */
104
+ exports.CRITIQUE_CHECK_CATEGORIES = [
105
+ 'task_fit',
106
+ 'source_fidelity',
107
+ 'ui_polish',
108
+ 'readability',
109
+ 'responsive',
110
+ ];
111
+ const parseUrl = (value) => {
112
+ try {
113
+ return new URL(value);
114
+ }
115
+ catch {
116
+ return undefined;
117
+ }
118
+ };
119
+ /**
120
+ * Decide whether a Playwright request may run while rendering a critique
121
+ * target. Static HTML previews are networkless by default; URL targets are
122
+ * not rendered by the critique sandbox.
123
+ */
124
+ const getDesignCritiqueRouteAction = (target, requestUrl) => {
125
+ const parsedRequestUrl = parseUrl(requestUrl);
126
+ if (!parsedRequestUrl)
127
+ return 'abort';
128
+ if (target.kind === 'html') {
129
+ if (target.assetBase &&
130
+ parsedRequestUrl.origin === STATIC_PREVIEW_ASSET_ORIGIN) {
131
+ return 'allow';
132
+ }
133
+ return STATIC_PREVIEW_ALLOWED_PROTOCOLS.has(parsedRequestUrl.protocol)
134
+ ? 'allow'
135
+ : 'abort';
136
+ }
137
+ return 'abort';
138
+ };
139
+ exports.getDesignCritiqueRouteAction = getDesignCritiqueRouteAction;
140
+ const assertAllowedDesignCritiqueTarget = (target) => {
141
+ if (target.kind === 'html')
142
+ return;
143
+ throw new Error('Design critique URL targets are not supported');
144
+ };
145
+ const addStaticPreviewBase = (html) => {
146
+ const base = `<base href="${STATIC_PREVIEW_ASSET_ORIGIN}/">`;
147
+ if (/<head[\s>]/i.test(html)) {
148
+ return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}\n${base}`);
149
+ }
150
+ return `${base}\n${html}`;
151
+ };
152
+ const contentTypeForAsset = (filePath) => {
153
+ const extension = filePath.toLowerCase().split('.').pop();
154
+ switch (extension) {
155
+ case 'avif':
156
+ return 'image/avif';
157
+ case 'css':
158
+ return 'text/css';
159
+ case 'gif':
160
+ return 'image/gif';
161
+ case 'glb':
162
+ return 'model/gltf-binary';
163
+ case 'gltf':
164
+ return 'model/gltf+json';
165
+ case 'htm':
166
+ case 'html':
167
+ return 'text/html';
168
+ case 'jpeg':
169
+ case 'jpg':
170
+ return 'image/jpeg';
171
+ case 'js':
172
+ return 'text/javascript';
173
+ case 'png':
174
+ return 'image/png';
175
+ case 'svg':
176
+ return 'image/svg+xml';
177
+ case 'webp':
178
+ return 'image/webp';
179
+ case 'woff':
180
+ return 'font/woff';
181
+ case 'woff2':
182
+ return 'font/woff2';
183
+ default:
184
+ return 'application/octet-stream';
185
+ }
186
+ };
187
+ const resolveStaticPreviewCritiqueAsset = (target, requestUrl) => {
188
+ if (target.kind !== 'html' || !target.assetBase)
189
+ return null;
190
+ const parsedRequestUrl = parseUrl(requestUrl);
191
+ if (!parsedRequestUrl)
192
+ return null;
193
+ if (parsedRequestUrl.origin !== STATIC_PREVIEW_ASSET_ORIGIN)
194
+ return null;
195
+ const relativePath = decodeURIComponent(parsedRequestUrl.pathname.slice(1));
196
+ if (!relativePath)
197
+ return null;
198
+ const candidate = path_1.default.resolve(target.assetBase, relativePath);
199
+ if (candidate !== target.assetBase &&
200
+ !candidate.startsWith(target.assetBase + path_1.default.sep)) {
201
+ return null;
202
+ }
203
+ try {
204
+ const realTarget = fs_1.default.realpathSync(candidate);
205
+ if (realTarget !== target.assetBase &&
206
+ !realTarget.startsWith(target.assetBase + path_1.default.sep)) {
207
+ return null;
208
+ }
209
+ const stat = fs_1.default.statSync(realTarget);
210
+ if (!stat.isFile() || stat.nlink !== 1)
211
+ return null;
212
+ return realTarget;
213
+ }
214
+ catch {
215
+ return null;
216
+ }
217
+ };
218
+ const critiqueFindingSchema = v3_1.z.object({
219
+ severity: v3_1.z.enum(['critical', 'major', 'minor']),
220
+ category: v3_1.z.enum([
221
+ 'task_fit',
222
+ 'source_fidelity',
223
+ 'ui_polish',
224
+ 'readability',
225
+ 'responsive',
226
+ ]),
227
+ evidence: v3_1.z.string(),
228
+ fix: v3_1.z.string(),
229
+ });
230
+ const designScoresSchema = v3_1.z.object({
231
+ hierarchy: v3_1.z.number(),
232
+ typography: v3_1.z.number(),
233
+ color: v3_1.z.number(),
234
+ spacing: v3_1.z.number(),
235
+ slopResistance: v3_1.z.number(),
236
+ overall: v3_1.z.number(),
237
+ critique: v3_1.z.string(),
238
+ // Optional so existing stubs (and a model that surfaces no issues) parse
239
+ // cleanly; absence is treated as "no findings".
240
+ findings: v3_1.z.array(critiqueFindingSchema).optional(),
241
+ });
242
+ const SUBMIT_DESIGN_SCORES_TOOL = {
243
+ name: 'submit_design_scores',
244
+ strict: true,
245
+ description: 'Submit the per-dimension aesthetic scores (0-10), a short critique, and any severity-coded findings for the rendered variant. Call exactly once.',
246
+ input_schema: {
247
+ type: 'object',
248
+ properties: {
249
+ hierarchy: { type: 'number' },
250
+ typography: { type: 'number' },
251
+ color: { type: 'number' },
252
+ spacing: { type: 'number' },
253
+ slopResistance: { type: 'number' },
254
+ overall: { type: 'number' },
255
+ critique: { type: 'string' },
256
+ findings: {
257
+ type: 'array',
258
+ 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.',
259
+ items: {
260
+ type: 'object',
261
+ properties: {
262
+ severity: { type: 'string', enum: ['critical', 'major', 'minor'] },
263
+ category: {
264
+ type: 'string',
265
+ enum: [
266
+ 'task_fit',
267
+ 'source_fidelity',
268
+ 'ui_polish',
269
+ 'readability',
270
+ 'responsive',
271
+ ],
272
+ },
273
+ evidence: { type: 'string' },
274
+ fix: { type: 'string' },
275
+ },
276
+ required: ['severity', 'category', 'evidence', 'fix'],
277
+ additionalProperties: false,
278
+ },
279
+ },
280
+ },
281
+ required: [
282
+ 'hierarchy',
283
+ 'typography',
284
+ 'color',
285
+ 'spacing',
286
+ 'slopResistance',
287
+ 'overall',
288
+ 'critique',
289
+ 'findings',
290
+ ],
291
+ additionalProperties: false,
292
+ },
293
+ };
294
+ 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.
295
+
296
+ Score the screenshot on each dimension from 0 (broken / generic) to 10 (exceptional, ships at a top design-led company):
297
+ - hierarchy: is the visual hierarchy and focal flow clear and intentional?
298
+ - typography: type scale, pairing, rhythm, and legibility.
299
+ - color: palette cohesion and disciplined use of color roles.
300
+ - spacing: spacing rhythm, alignment, and density.
301
+ - 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.
302
+
303
+ 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.
304
+
305
+ Separately, raise severity-coded findings for concrete usability/quality problems across these checks:
306
+ - task_fit: does it actually do the requested job (right artifact, required sections/actions present)?
307
+ - source_fidelity: does it honor explicit required content, assets, and constraints from the request?
308
+ - ui_polish: clipped/overlapping/misaligned elements, broken images, placeholder text, visual artifacts.
309
+ - readability: is primary text and are key controls legible (contrast, size, not clipped or hidden)?
310
+ - responsive: is core content and the primary action usable at the review viewport?
311
+
312
+ 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.`;
313
+ const TASK_FIT_RUBRIC = {
314
+ 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.',
315
+ 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.',
316
+ 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.',
317
+ app_screen: 'This is an APP SCREEN: require a clear primary task, usable navigation/controls, and legible content for the core action.',
318
+ generic: 'Judge it against the most reasonable interpretation of the requested artifact: the core content and primary action must be present and usable.',
319
+ };
320
+ const PLACEHOLDER_TEXT_PATTERN = /lorem ipsum|dolor sit amet/i;
321
+ const HTML_TAG_PATTERN = /<[^>]+>/g;
322
+ const SCRIPT_STYLE_PATTERN = /<(script|style)[\s\S]*?<\/\1>/gi;
323
+ /** Minimum count of rendered, non-whitespace text characters before the body
324
+ * is treated as having visible content. Below this the variant is effectively
325
+ * blank. */
326
+ const MIN_VISIBLE_TEXT_CHARS = 1;
327
+ const LANDING_ACTION_PATTERN = /<(a|button)\b|role\s*=\s*["']button["']/i;
328
+ const BROKEN_IMG_PATTERN = /<img\b(?![^>]*\bsrc\s*=\s*("|')\s*[^"'\s][^"']*\1)[^>]*>/i;
329
+ 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;
330
+ const ARTIFACT_TYPE_KEYWORDS = [
331
+ [
332
+ 'landing_page',
333
+ /landing page|marketing site|hero section|homepage|home page/i,
334
+ ],
335
+ ['dashboard', /dashboard|analytics|admin panel|metrics|reporting/i],
336
+ ['form', /\bform\b|sign[ -]?up|sign[ -]?in|login|checkout|contact page/i],
337
+ [
338
+ 'app_screen',
339
+ /\bapp\b|screen|todo|kanban|timer|settings page|profile page/i,
340
+ ],
341
+ ];
342
+ /**
343
+ * Infer the artifact type from free-text request copy (prompt and/or brief).
344
+ * Falls back to `generic` when nothing matches so the gate stays usable for
345
+ * arbitrary requests.
346
+ */
347
+ const inferDesignArtifactType = (text) => {
348
+ for (const [type, pattern] of ARTIFACT_TYPE_KEYWORDS) {
349
+ if (pattern.test(text))
350
+ return type;
351
+ }
352
+ return 'generic';
353
+ };
354
+ exports.inferDesignArtifactType = inferDesignArtifactType;
355
+ /**
356
+ * Derive the design contract from the request copy. `artifactType` drives the
357
+ * task-specific rubric; `requirements` echoes the raw request lines so the
358
+ * critique prompt can hold the model to explicit asks.
359
+ */
360
+ const buildDesignContract = (input) => {
361
+ const prompt = input.prompt?.trim() ?? '';
362
+ const brief = input.brief?.trim() ?? '';
363
+ const requirements = [prompt, brief].filter((line) => line.length > 0);
364
+ const requiredAssetFilenames = extractRequiredAssetFilenames(requirements);
365
+ return {
366
+ artifactType: (0, exports.inferDesignArtifactType)(`${prompt}\n${brief}`),
367
+ requirements,
368
+ requiredAssetFilenames,
369
+ };
370
+ };
371
+ exports.buildDesignContract = buildDesignContract;
372
+ const extractRequiredAssetFilenames = (requirements) => {
373
+ const filenames = new Set();
374
+ for (const requirement of requirements) {
375
+ REQUIRED_ASSET_PATH_PATTERN.lastIndex = 0;
376
+ let match;
377
+ while ((match = REQUIRED_ASSET_PATH_PATTERN.exec(requirement))) {
378
+ filenames.add(match[1].toLowerCase());
379
+ }
380
+ }
381
+ return [...filenames];
382
+ };
383
+ const buildCritiquePrompt = (designContextMarkdown, designContract) => {
384
+ const sections = [];
385
+ if (designContextMarkdown && designContextMarkdown.trim().length > 0) {
386
+ 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 ---`);
387
+ }
388
+ else {
389
+ sections.push('No bound DESIGN.md — score the attached screenshot on general product-design quality using the rubric.');
390
+ }
391
+ if (designContract) {
392
+ sections.push(TASK_FIT_RUBRIC[designContract.artifactType]);
393
+ if (designContract.requirements.length > 0) {
394
+ sections.push(`The request asked for:\n${designContract.requirements
395
+ .map((line) => `- ${line}`)
396
+ .join('\n')}\nRaise source_fidelity findings for anything explicitly required that is missing or wrong.`);
397
+ }
398
+ }
399
+ return sections.join('\n\n');
400
+ };
401
+ const clampScore = (value) => Math.max(MIN_SCORE, Math.min(MAX_SCORE, value));
402
+ /**
403
+ * Apply the must-pass bar to a set of dimension scores. Returns true only
404
+ * when the holistic score clears `OVERALL_PASS_BAR` and every dimension is at
405
+ * or above `DIMENSION_FLOOR`.
406
+ */
407
+ const passesDesignBar = (scores) => {
408
+ if (scores.overall < exports.OVERALL_PASS_BAR)
409
+ return false;
410
+ const dimensions = [
411
+ scores.hierarchy,
412
+ scores.typography,
413
+ scores.color,
414
+ scores.spacing,
415
+ scores.slopResistance,
416
+ ];
417
+ return dimensions.every((value) => value >= exports.DIMENSION_FLOOR);
418
+ };
419
+ exports.passesDesignBar = passesDesignBar;
420
+ const statusForSeverity = (severity) => severity === 'minor' ? 'warn' : 'fail';
421
+ /** Derive per-category pass/warn/fail status from the gathered findings. */
422
+ const deriveChecks = (findings) => {
423
+ const checks = {};
424
+ for (const category of exports.CRITIQUE_CHECK_CATEGORIES) {
425
+ checks[category] = 'pass';
426
+ }
427
+ for (const finding of findings) {
428
+ const next = statusForSeverity(finding.severity);
429
+ if (next === 'fail' || checks[finding.category] === 'pass') {
430
+ // fail dominates warn; warn dominates pass.
431
+ if (next === 'fail' || checks[finding.category] !== 'fail') {
432
+ checks[finding.category] = next;
433
+ }
434
+ }
435
+ }
436
+ return checks;
437
+ };
438
+ /**
439
+ * Apply the strict critique gate: a `critical` finding fails the variant
440
+ * outright and caps the overall score to the floor; a `major` finding caps the
441
+ * overall score below the pass bar. The variant passes only when no critical is
442
+ * present and the (capped) overall + dimension floors clear the aesthetic bar.
443
+ */
444
+ const evaluateCritique = (scores, findings) => {
445
+ const hasCritical = findings.some((f) => f.severity === 'critical');
446
+ const hasMajor = findings.some((f) => f.severity === 'major');
447
+ const scoreCaps = [];
448
+ let overallScore = scores.overall;
449
+ if (hasCritical) {
450
+ overallScore = MIN_SCORE;
451
+ for (const finding of findings) {
452
+ if (finding.severity === 'critical') {
453
+ scoreCaps.push({
454
+ category: finding.category,
455
+ max: MIN_SCORE,
456
+ reason: `Critical: ${finding.evidence}`,
457
+ });
458
+ }
459
+ }
460
+ }
461
+ else if (hasMajor) {
462
+ overallScore = Math.min(overallScore, exports.MAJOR_FINDING_SCORE_CAP);
463
+ for (const finding of findings) {
464
+ if (finding.severity === 'major') {
465
+ scoreCaps.push({
466
+ category: finding.category,
467
+ max: exports.MAJOR_FINDING_SCORE_CAP,
468
+ reason: `Major: ${finding.evidence}`,
469
+ });
470
+ }
471
+ }
472
+ }
473
+ const passed = !hasCritical && (0, exports.passesDesignBar)({ ...scores, overall: overallScore });
474
+ return {
475
+ passed,
476
+ blocked: hasCritical,
477
+ overallScore,
478
+ scoreCaps,
479
+ checks: deriveChecks(findings),
480
+ findings,
481
+ };
482
+ };
483
+ exports.evaluateCritique = evaluateCritique;
484
+ const stripToVisibleText = (html) => html
485
+ .replace(SCRIPT_STYLE_PATTERN, ' ')
486
+ .replace(HTML_TAG_PATTERN, ' ')
487
+ .replace(/&[a-z]+;/gi, ' ')
488
+ .replace(/\s+/g, ' ')
489
+ .trim();
490
+ /**
491
+ * Deterministic artifact lint over a static-preview HTML document. Catches
492
+ * mechanical defects the vision model can underweight — a blank document,
493
+ * placeholder copy, broken images, or a landing page with no primary action —
494
+ * and emits them as severity-coded findings so they feed the same gate as the
495
+ * model's findings. Runs before the subjective critique; never throws.
496
+ */
497
+ const lintStaticPreview = (html, contract) => {
498
+ const findings = [];
499
+ const visibleText = stripToVisibleText(html);
500
+ if (visibleText.length < MIN_VISIBLE_TEXT_CHARS) {
501
+ findings.push({
502
+ severity: 'critical',
503
+ category: 'readability',
504
+ evidence: 'The rendered document has no visible text content.',
505
+ fix: 'Render the core content of the requested artifact as visible text.',
506
+ });
507
+ }
508
+ if (PLACEHOLDER_TEXT_PATTERN.test(visibleText)) {
509
+ findings.push({
510
+ severity: 'major',
511
+ category: 'ui_polish',
512
+ evidence: 'Placeholder lorem-ipsum copy is present in the output.',
513
+ fix: 'Replace placeholder copy with real, task-appropriate content.',
514
+ });
515
+ }
516
+ if (BROKEN_IMG_PATTERN.test(html)) {
517
+ findings.push({
518
+ severity: 'major',
519
+ category: 'ui_polish',
520
+ evidence: 'An <img> element is missing a usable src.',
521
+ fix: 'Give every image a valid src or remove the broken element.',
522
+ });
523
+ }
524
+ if (contract?.artifactType === 'landing_page' &&
525
+ !LANDING_ACTION_PATTERN.test(html)) {
526
+ findings.push({
527
+ severity: 'critical',
528
+ category: 'task_fit',
529
+ evidence: 'A landing page was requested but it has no primary action (link or button).',
530
+ fix: 'Add a clear primary call-to-action (a link or button) above the fold.',
531
+ });
532
+ }
533
+ for (const filename of contract?.requiredAssetFilenames ?? []) {
534
+ if (html.toLowerCase().includes(filename))
535
+ continue;
536
+ findings.push({
537
+ severity: 'critical',
538
+ category: 'source_fidelity',
539
+ evidence: `The output does not reference the explicitly requested asset '${filename}'.`,
540
+ fix: `Use the requested '${filename}' asset in the generated preview instead of recreating or omitting it.`,
541
+ });
542
+ }
543
+ return findings;
544
+ };
545
+ exports.lintStaticPreview = lintStaticPreview;
546
+ /**
547
+ * Render → screenshot → VLM rubric → strict gate → verdict. Never throws:
548
+ * render/model failures resolve to a non-blocking skipped outcome.
549
+ */
550
+ const runDesignCritique = async (input, deps = {}) => {
551
+ if (input.target.kind === 'url') {
552
+ log.warn('Design critique skipped — live URL targets are not supported');
553
+ return {
554
+ ran: false,
555
+ passed: true,
556
+ blocked: false,
557
+ findings: [],
558
+ scoreCaps: [],
559
+ skippedReason: 'live_url_target',
560
+ };
561
+ }
562
+ const screenshot = deps.screenshot ?? exports.capturePlaywrightScreenshot;
563
+ const gatewayClient = deps.gatewayClient ?? new GatewayClient_1.GatewayClient();
564
+ let image;
565
+ try {
566
+ image = await screenshot(input.target);
567
+ }
568
+ catch (err) {
569
+ const reason = err instanceof Error ? err.message : String(err);
570
+ log.warn(`Design critique skipped — screenshot failed: ${reason}`);
571
+ return {
572
+ ran: false,
573
+ passed: true,
574
+ blocked: false,
575
+ findings: [],
576
+ scoreCaps: [],
577
+ skippedReason: `screenshot: ${reason}`,
578
+ };
579
+ }
580
+ let raw;
581
+ try {
582
+ const toolInput = await gatewayClient.scoreImage({
583
+ model: DESIGN_CRITIQUE_MODEL,
584
+ maxTokens: DESIGN_CRITIQUE_MAX_TOKENS,
585
+ temperature: DESIGN_CRITIQUE_TEMPERATURE,
586
+ system: DESIGN_CRITIQUE_SYSTEM_PROMPT,
587
+ prompt: buildCritiquePrompt(input.designContextMarkdown, input.designContract),
588
+ images: [image],
589
+ tool: SUBMIT_DESIGN_SCORES_TOOL,
590
+ timeoutMs: exports.DESIGN_CRITIQUE_GATEWAY_TIMEOUT_MS,
591
+ });
592
+ const parsed = designScoresSchema.safeParse(toolInput);
593
+ if (!parsed.success) {
594
+ log.warn('Design critique skipped — model response failed schema');
595
+ return {
596
+ ran: false,
597
+ passed: true,
598
+ blocked: false,
599
+ findings: [],
600
+ scoreCaps: [],
601
+ skippedReason: 'invalid_model_response',
602
+ };
603
+ }
604
+ raw = parsed.data;
605
+ }
606
+ catch (err) {
607
+ const reason = err instanceof Error ? err.message : String(err);
608
+ log.warn(`Design critique skipped — scoring failed: ${reason}`);
609
+ return {
610
+ ran: false,
611
+ passed: true,
612
+ blocked: false,
613
+ findings: [],
614
+ scoreCaps: [],
615
+ skippedReason: `score: ${reason}`,
616
+ };
617
+ }
618
+ const scores = {
619
+ hierarchy: clampScore(raw.hierarchy),
620
+ typography: clampScore(raw.typography),
621
+ color: clampScore(raw.color),
622
+ spacing: clampScore(raw.spacing),
623
+ slopResistance: clampScore(raw.slopResistance),
624
+ overall: clampScore(raw.overall),
625
+ };
626
+ // Deterministic artifact lint runs only when the rendered HTML is available
627
+ // (static_preview). vite_app targets are URL-only here, so they rely on the
628
+ // model's findings alone.
629
+ const lintFindings = input.target.kind === 'html'
630
+ ? (0, exports.lintStaticPreview)(input.target.html, input.designContract)
631
+ : [];
632
+ const findings = [
633
+ ...lintFindings,
634
+ ...(raw.findings ?? []),
635
+ ];
636
+ const evaluation = (0, exports.evaluateCritique)(scores, findings);
637
+ return {
638
+ ran: true,
639
+ passed: evaluation.passed,
640
+ blocked: evaluation.blocked,
641
+ scores,
642
+ overallScore: evaluation.overallScore,
643
+ findings: evaluation.findings,
644
+ checks: evaluation.checks,
645
+ scoreCaps: evaluation.scoreCaps,
646
+ summary: raw.critique,
647
+ };
648
+ };
649
+ exports.runDesignCritique = runDesignCritique;
650
+ /**
651
+ * Default screenshot capturer backed by Playwright Chromium. Lazily imports
652
+ * Playwright so the dependency is only loaded when a real critique runs (unit
653
+ * tests inject a stub and never reach this path).
654
+ *
655
+ * @effect Launches a headless Chromium browser and renders the target.
656
+ */
657
+ const capturePlaywrightScreenshot = async (target) => {
658
+ assertAllowedDesignCritiqueTarget(target);
659
+ // Lazy import keeps Playwright out of the module's static dependency graph
660
+ // for callers that always inject a stub screenshot seam.
661
+ const { chromium } = await Promise.resolve().then(() => __importStar(require('playwright')));
662
+ const browser = await chromium.launch();
663
+ try {
664
+ const page = await browser.newPage({
665
+ viewport: {
666
+ width: SCREENSHOT_VIEWPORT_WIDTH,
667
+ height: SCREENSHOT_VIEWPORT_HEIGHT,
668
+ },
669
+ });
670
+ await page.routeWebSocket('**/*', (ws) => ws.close());
671
+ await page.route('**/*', async (route) => {
672
+ const assetPath = resolveStaticPreviewCritiqueAsset(target, route.request().url());
673
+ if (assetPath) {
674
+ await route.fulfill({
675
+ path: assetPath,
676
+ contentType: contentTypeForAsset(assetPath),
677
+ });
678
+ return;
679
+ }
680
+ const action = (0, exports.getDesignCritiqueRouteAction)(target, route.request().url());
681
+ if (action === 'allow') {
682
+ await route.continue();
683
+ return;
684
+ }
685
+ await route.abort();
686
+ });
687
+ if (target.kind === 'html') {
688
+ const html = target.assetBase
689
+ ? addStaticPreviewBase(target.html)
690
+ : target.html;
691
+ await page.setContent(html, {
692
+ waitUntil: 'domcontentloaded',
693
+ timeout: SCREENSHOT_TIMEOUT_MS,
694
+ });
695
+ }
696
+ else {
697
+ await page.goto(target.url, {
698
+ waitUntil: 'domcontentloaded',
699
+ timeout: SCREENSHOT_TIMEOUT_MS,
700
+ });
701
+ }
702
+ const buffer = await page.screenshot({ type: 'png' });
703
+ return { mediaType: 'image/png', data: buffer.toString('base64') };
704
+ }
705
+ finally {
706
+ await browser.close();
707
+ }
708
+ };
709
+ exports.capturePlaywrightScreenshot = capturePlaywrightScreenshot;
710
+ //# sourceMappingURL=designCritique.js.map