better-design 0.33.0 → 0.35.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.
package/dist/index.js CHANGED
@@ -1252,6 +1252,7 @@ var init_tool_names = __esm({
1252
1252
  "review-dashboard-structure",
1253
1253
  "review-illustration",
1254
1254
  "review-ui-code",
1255
+ "send-feedback",
1255
1256
  "set-design-config",
1256
1257
  "update-feature-map"
1257
1258
  ];
@@ -1290,6 +1291,7 @@ var init_tool_names = __esm({
1290
1291
  "review-dashboard-structure",
1291
1292
  "review-illustration",
1292
1293
  "review-ui-code",
1294
+ "send-feedback",
1293
1295
  "set-design-config",
1294
1296
  "update-feature-map"
1295
1297
  ]);
@@ -1389,9 +1391,9 @@ Better Design provides design systems, design principles and UI review checks fo
1389
1391
  "get-shopify-guide": "Returns Better Design's best practices for Shopify Liquid and Online Store 2.0 themes: theme architecture, Liquid objects and filters, section schema, localization, accessibility, performance and Theme Check validation, with links to Shopify's documentation. Pass `topic`, `query`, or nothing for the index of topics.",
1390
1392
  "get-motion-guide": "Returns best practices and components for building a product demo or promo video in code with Remotion: the brief, brand and motion tokens, format profiles, captions, pacing, rendering and review. `scaffold: true` returns a complete Remotion starter project. `component` returns one video component's source and install command, and no arguments lists the components. The components come from snapcn (MIT) through the Better Design registry.",
1391
1393
  "get-swiftui-guide": "Returns best practices and Swift examples for SwiftUI interfaces: layout and view composition, animation and gestures, Metal shader effects and the platform APIs they use. Pass `topic`, `query`, or nothing for the index of topics.",
1392
- "get-review-rules": "Returns Better Design's review rules for UI code, each with a severity: accessibility (WCAG 2.1), visual design, animation, content, comprehension and AI features. Pass `category` for one group. Use it when the user wants UI code reviewed. `maxTokens` limits the length of the response.",
1394
+ "get-review-rules": "Returns Better Design's review rules for UI code, each with a severity: accessibility (WCAG 2.1), visual design, animation, content, comprehension and AI features. Pass `category` for one group. Use it when the user wants UI code reviewed. `maxTokens` limits the length of the response. The rules are read against source, so a clean self-review is not visual approval.",
1393
1395
  "get-widget-guide": "Returns a build brief for an interactive widget on an article or landing page, such as a calculator, estimator, converter, quiz or checklist. Pass what is known about the widget. The response lists any details still missing, and once the details are complete it returns the brief with rules for calculation accuracy, form behavior, search visibility, performance and privacy.",
1394
- "check-comprehension": "Checks interface copy for clarity with three tests: whether a person outside the team understands it, whether the screen can be explained in 15 seconds, and whether a screenshot alone says what the screen does. Pass the headline, body and action labels, or the markup. Returns each finding with a severity and a suggested fix. `knownTerms` lists words the audience already knows. Better Design runs the checks itself and calls no other API.",
1396
+ "check-comprehension": 'Checks interface copy for clarity with three tests: whether a person outside the team understands it, whether the screen can be explained in 15 seconds, and whether a screenshot alone says what the screen does. Pass the headline, body and action labels, or the markup. Returns each finding with a severity and a suggested fix. `knownTerms` lists words the audience already knows. `surface: "slide"` adds a density test for slides. It reads text only, not screenshots or visual hierarchy, so a pass is not visual approval. Better Design runs the checks itself and calls no other API.',
1395
1397
  "review-dashboard-structure": "Reviews the source of a dashboard or overview page against a fixed rubric: complete source, unchanged data, supported claims, hierarchy, scan order and use of the installed Sidebar, StatCard and Table components. Pass the page source, the data fixture, its interpretation and the component sources. Returns PASS, or FAIL with every finding as a tool error.",
1396
1398
  "finalize-dashboard-review": "Captures a deployed dashboard at a public HTTPS URL in Better Design's own browser at 390, 1440 and 1728 pixels wide, compares it with the submitted source, data and components, and returns a receipt tied to that deployment. The receipt reports hierarchy, component, copy and spacing faults and any hidden data fields.",
1397
1399
  "find-icon-library": `Searches icon libraries by style, mood or use case, such as "minimal clean professional", and returns the closest matches with a note on each library's style.`,
@@ -1401,7 +1403,7 @@ Better Design provides design systems, design principles and UI review checks fo
1401
1403
  "find-design-system": "Searches Better Design's catalog of design systems by industry, personality or a description of the interface, and returns ranked matches with a preview link for each. Use it when the user wants a design system or a visual direction for an app, site, page, dashboard or presentation.",
1402
1404
  "get-design-system-kit": "Returns the build kit for a catalog design system or a generated project. `target` picks the format: `react` returns one shadcn install command for every component and the tokens, `html` returns framework-free token and component CSS for inline canvases, `react-native` and `swiftui` return mobile kits, `shopify` returns Liquid theme files, `dtcg` returns a W3C Design Tokens document, and `store-screenshots` returns slide files and a render script for App Store and Google Play screenshots. `component` narrows the React install to one component.",
1403
1405
  "get-inspiration": 'Returns real reference designs from Better Design\'s library as images. `type` picks the collection: today `app-store-screenshots`, the App Store screenshot sets of shipped apps, each with its title and category. `category`, such as "Productivity", narrows the sets, and `limit` sets how many come back, 1 to 6.',
1404
- "create-design-system": "Starts generating a custom design system from a description of the product and its look, and returns the project id and project URL at once. Generation runs in the background and typically takes 1 to 3 minutes. get-design-system-status reports progress and returns the install command when it finishes. `baseDesignSystemId` starts from a catalog system, and `brandCss` builds from an existing brand token sheet, such as the output of extract-design-system. Each call uses one credit from the user's Better Design account. Anyone with the registry or preview URL can view the generated source and previews.",
1406
+ "create-design-system": "Starts generating a custom design system from a description of the product and its look, and returns the project id and project URL at once. Generation runs in the background and typically takes 1 to 3 minutes. get-design-system-status reports progress and returns the install command when it finishes. `baseDesignSystemId` starts from a catalog system the user picked, `userConfirmedCustom` records that the user chose fully custom, and `brandCss` builds from an existing brand token sheet, such as the output of extract-design-system. With none of the three, it generates nothing and returns the top catalog matches to show the user. Each call uses one credit from the user's Better Design account. Anyone with the registry or preview URL can view the generated source and previews.",
1405
1407
  "get-design-system-status": "Reports the progress of a design system started with create-design-system. While generation runs it returns the current stage. When generation finishes it returns the live preview URL and the `npx shadcn add` install command.",
1406
1408
  "preview-design-system": "Returns a visual preview of a catalog design system: its palette, typography, buttons, card, input and badge, with a link to the full live showcase. Takes an id from find-design-system.",
1407
1409
  "get-design-config": "Returns the design config saved on a generated project: exact color tokens, radius, fonts and icon library, including values the user tuned on the project page.",
@@ -1410,12 +1412,13 @@ Better Design provides design systems, design principles and UI review checks fo
1410
1412
  "extract-design-system": "Reads design tokens from CSS the caller passes in and returns the semantic palette, radius, fonts and shadow scale, with a token sheet that converts every color to `oklch()`. It accepts hex, rgb, hsl, named and oklch colors across `:root`, `.dark` and `@theme` blocks. `sources` merges several inputs, including a sample of computed styles from a live page. The server cannot read files, so the caller passes the text.",
1411
1413
  "extract-from-url": "Fetches a public web page, reads its stylesheets and renders it in a headless browser to sample the colors, fonts and radii of its body, headings, links, buttons, inputs and cards, then returns the brand as an oklch token sheet. It reads the page the way a browser does and calls no other API. A call takes about 10 to 45 seconds.",
1412
1414
  "review-illustration": "Renders an SVG illustration in a browser and returns its visible faults against Better Design's illustration principles, such as open shapes, floating parts, inconsistent light and contents outside their container. Pass the SVG and what it is meant to show. Pass the returned `reviewToken` back to review the same drawing again. SVG that carries scripts, event handlers or remote references is refused.",
1413
- "inspect-spacing": "Measures spacing in a rendered interface from DOM geometry and returns the spacing faults with a correction for each. The first call returns a read-only capture script to run in the page. The second call takes the captured states, such as viewport widths or hover and focus states, and returns the measurements and findings.",
1415
+ "inspect-spacing": "Measures spacing in a rendered interface from DOM geometry and returns the spacing faults with a correction for each. The first call returns a read-only capture script to run in the page. The second call takes the captured states, such as viewport widths or hover and focus states, and returns the measurements and findings. It measures geometry only, so a clean result is not visual approval.",
1414
1416
  "build-feature-map": "Drafts a map of a product's features and flows from its repository file list and returns it as JSON for .better-design/feature-map.json. Routes become screens, nested routes become flows, and components, API routes and hooks join the step they share a name with. Pass every path from `git ls-files` as `files`.",
1415
1417
  "read-feature-map": "Returns the flows in a feature map that match a flow name, a feature or a file path, with the screen, route, components and data files of every step. Pass the contents of .better-design/feature-map.json as `map`. With no filter it lists every flow.",
1416
1418
  "update-feature-map": "Applies one change to a feature map, such as adding, renaming or removing a feature, or setting, renaming or removing a flow, and returns the updated map.",
1417
1419
  "get-team-rules": "Returns the Markdown rules the user's Better Design team wrote for how their product is built and reviewed, or a note that the team has none yet.",
1418
1420
  "review-ui-code": "Reviews React component source and CSS and returns findings with line numbers and fixes: native elements that bypass the installed design system, undefined font variables, hardcoded colors and raw palette classes, generated-UI patterns, and WCAG faults such as missing alt text or accessible names. The same input always returns the same findings.",
1421
+ "send-feedback": "Sends Better Design a report of a bug, a missing feature or wrong output in its tools or API: the kind of problem, the tool or endpoint, a one-sentence summary, and optionally what was tried, expected and returned. A person reads every report. It is available only on connections that use a Better Design API key.",
1419
1422
  "create-figma-prototype": "Reads a frame from the Figma file that has the Better Design plugin open and returns it as one HTML page built from the design system's components, with a report of every layer that could not be mapped. Name a frame, or omit the name to use the current selection."
1420
1423
  };
1421
1424
  }
@@ -4275,12 +4278,15 @@ var init_review_rules_description = __esm({
4275
4278
  "content",
4276
4279
  "comprehension",
4277
4280
  "ai-features",
4281
+ "coverage-gaps",
4278
4282
  "all"
4279
4283
  ];
4280
4284
  REVIEW_RULES_DESCRIPTION = `Retrieves accessibility (WCAG 2.1), visual design, animation, content, comprehension, and AI feature review rules for analyzing code.
4281
4285
 
4282
4286
  You MUST call this after writing UI code to self-review your output, then fix every critical and serious issue BEFORE presenting the result.
4283
4287
 
4288
+ It returns rules to check against. It approves nothing: a clean self-review against these rules is not visual approval, because the rules are read against source, not against what renders.
4289
+
4284
4290
  ${REDESIGN_SCOPE_CONTRACT}
4285
4291
 
4286
4292
  ${DESIGN_TRADEOFF_RULE}
@@ -4323,6 +4329,8 @@ ${DESIGN_TRADEOFF_RULE}
4323
4329
  - Critical: Output saved or sent without the user accepting it, silent failures, no manual path when AI is unavailable, user data read before consent, invented citations or unearned certainty
4324
4330
  - Serious: AI output with no visible marker, long runs with no sign or a Stop that deletes partial output, refusals that name no topic and offer no alternative, feedback controls with no destination
4325
4331
 
4332
+ Every rule carries an ID (\`rule/\u2026\`), a source and an exceptions line. Cite the ID in each finding. The \`coverage-gaps\` category lists the surfaces that have no rule yet.
4333
+
4326
4334
  Use maxTokens to limit rule content (recommended: 5000\u201310000). Required workflow guidance is always returned in full.`;
4327
4335
  }
4328
4336
  });
@@ -5161,6 +5169,22 @@ These rules were read against source. They cannot see layout. Three checks remai
5161
5169
  2. Call \`inspect-spacing\` on the rendered DOM at 1440, 1728 and 390 wide. The first call returns a capture script and measures nothing; the layout is measured only when a second call, with the captured states, returns status \`completed\`. Rules read code; only measurement catches a label sitting 9px off its own circle, an active state that is 2px wider than its siblings and shifts the row, or a sidebar that leaves page background beside it on a wide screen.
5162
5170
  3. For dashboards, deploy the exact source to a public HTTPS preview, then call \`finalize-dashboard-review\` with that URL, the exact complete and visible source, unchanged fixture, and installed component source. Better Design's own browser captures the preview at 390, 1440, and 1728, content-addresses the live deployment, derives the factual interpretation, and issues the only valid review receipt. No trusted browser capture means incomplete. An invalid deployment revision, invalid geometry, or hidden fixture field also blocks approval. Any later source or deployment edit voids the receipt. Then confirm every nav item, link, tab and button works.
5163
5171
 
5172
+ ## Presentations
5173
+
5174
+ A deck passes only on rendered evidence. For every slide:
5175
+
5176
+ 1. Call \`check-comprehension\` with \`surface: "slide"\`. Keep one idea per slide and fix every density finding.
5177
+ 2. Render the deck and review a screenshot of every slide: the title reads first, one element holds the eye, and nothing competes with it. Text checks and \`inspect-spacing\` cannot see this, so no score stands in for the screenshot.
5178
+
5179
+ ## Check the rules below in two passes
5180
+
5181
+ An agent that reviews its own screen in one pass tends to approve it. Split the review:
5182
+
5183
+ 1. Per rule: give each rule section below (each \`# \` heading) to its own subagent, in a clean context that holds only that section and the source. A host without subagents checks one section at a time and writes down its findings before it reads the next.
5184
+ 2. Skeptic pass: give every flag to one fresh subagent that tries to disprove it. It keeps a flag only when it can cite the file and line that break the rule.
5185
+
5186
+ Cite the rule ID with every finding: the \`rule/\u2026\` line under each rule heading. Read its Exceptions line before you flag. A surface listed under coverage gaps has no rule yet, so say that instead of inventing one.
5187
+
5164
5188
  Fix every critical and serious finding before you present.`;
5165
5189
  }
5166
5190
  });
@@ -5184,6 +5208,8 @@ function looksLikeCard(candidate) {
5184
5208
  }
5185
5209
  function redactSecrets(value) {
5186
5210
  let out = value.replace(/\u0000/g, "");
5211
+ for (const pattern of CREDENTIAL_VALUE_PATTERNS) out = out.replace(pattern, "[redacted]");
5212
+ out = out.replace(JWT_PATTERN, "$1[redacted]");
5187
5213
  for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, "[redacted]");
5188
5214
  out = out.replace(EMAIL_PATTERN, "[redacted-email]");
5189
5215
  out = out.replace(
@@ -5202,7 +5228,7 @@ function redactUrl(raw) {
5202
5228
  return "[redacted]";
5203
5229
  }
5204
5230
  }
5205
- var SECRET_PATTERNS, EMAIL_PATTERN, CARD_CANDIDATE_PATTERN, PHONE_PATTERNS;
5231
+ var SECRET_PATTERNS, CREDENTIAL_VALUE_PATTERNS, JWT_PATTERN, EMAIL_PATTERN, CARD_CANDIDATE_PATTERN, PHONE_PATTERNS;
5206
5232
  var init_redact = __esm({
5207
5233
  "../shared/src/redact.ts"() {
5208
5234
  "use strict";
@@ -5218,9 +5244,9 @@ var init_redact = __esm({
5218
5244
  /\bwhsec_[A-Za-z0-9]{16,}/g,
5219
5245
  /\bAIza[A-Za-z0-9_-]{30,}/g,
5220
5246
  /\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
5221
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g,
5222
- // JWT
5223
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
5247
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g
5248
+ ];
5249
+ CREDENTIAL_VALUE_PATTERNS = [
5224
5250
  // key=value forms for the names that carry credentials. A quoted value is
5225
5251
  // taken whole, so a passphrase with spaces and a short password both go:
5226
5252
  // the old form needed eight characters and no spaces, which let
@@ -5228,8 +5254,13 @@ var init_redact = __esm({
5228
5254
  // through into a corpus held for 180 days. [BD-697]
5229
5255
  /(?:\b|_)(?:api[_-]?key|access[_-]?token|refresh[_-]?token|secret(?:[_-]?key)?|password|passwd|authorization)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[A-Za-z0-9._~+/-]+)/gi,
5230
5256
  // Authorization headers separate the scheme from the token with a space.
5231
- /\b(?:bearer|basic|token)\s+[A-Za-z0-9._~+/-]{12,}=*/gi
5257
+ /\b(?:bearer|basic|token)\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
5258
+ // A token in a query string, such as the claim link an agent gets when its
5259
+ // free budget runs out. That link hands the agent's keys to whoever opens
5260
+ // it. [BD-956]
5261
+ /[?&]token=[A-Za-z0-9_-]{16,}/gi
5232
5262
  ];
5263
+ JWT_PATTERN = /(?<![A-Za-z0-9_-])(?=[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-])([A-Za-z0-9_-]*?)(?<![A-Za-z0-9_])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g;
5233
5264
  EMAIL_PATTERN = /(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
5234
5265
  CARD_CANDIDATE_PATTERN = /\b(?:\d[ -]?){12,18}\d\b/g;
5235
5266
  PHONE_PATTERNS = [
@@ -5363,7 +5394,7 @@ var init_extract = __esm({
5363
5394
  });
5364
5395
 
5365
5396
  // ../shared/src/mcp/comprehension/lexicon.ts
5366
- var TECHNICAL_TERMS, PRODUCT_TERMS, KNOWN_ACRONYMS, VAGUE_ACTIONS, MAX_SENTENCE_WORDS, MAX_SCREEN_WORDS, MAX_ACTIONS, EXPLAIN_BUDGET_SECONDS;
5397
+ var TECHNICAL_TERMS, PRODUCT_TERMS, KNOWN_ACRONYMS, VAGUE_ACTIONS, MAX_SENTENCE_WORDS, MAX_SCREEN_WORDS, MAX_ACTIONS, EXPLAIN_BUDGET_SECONDS, MAX_SLIDE_WORDS, MAX_SLIDE_LINES, SLIDE_NAME;
5367
5398
  var init_lexicon = __esm({
5368
5399
  "../shared/src/mcp/comprehension/lexicon.ts"() {
5369
5400
  "use strict";
@@ -5472,6 +5503,9 @@ var init_lexicon = __esm({
5472
5503
  MAX_SCREEN_WORDS = 60;
5473
5504
  MAX_ACTIONS = 3;
5474
5505
  EXPLAIN_BUDGET_SECONDS = 15;
5506
+ MAX_SLIDE_WORDS = 40;
5507
+ MAX_SLIDE_LINES = 5;
5508
+ SLIDE_NAME = /\b(slides?|deck|presentation)\b/i;
5475
5509
  }
5476
5510
  });
5477
5511
 
@@ -5546,7 +5580,7 @@ function estimateExplainSeconds(words2, insiderTermCount, actionCount) {
5546
5580
  const choosing = Math.max(0, actionCount - 1) * 3;
5547
5581
  return Math.round(speaking + defining + choosing);
5548
5582
  }
5549
- function fifteenSecondFindings(words2, actions, seconds) {
5583
+ function fifteenSecondFindings(words2, actions, seconds, surface) {
5550
5584
  const findings = [];
5551
5585
  if (seconds > EXPLAIN_BUDGET_SECONDS) {
5552
5586
  findings.push({
@@ -5557,7 +5591,7 @@ function fifteenSecondFindings(words2, actions, seconds) {
5557
5591
  fix: "Cut to one idea and one next step. Move the rest behind a link or a later screen."
5558
5592
  });
5559
5593
  }
5560
- if (words2 > MAX_SCREEN_WORDS) {
5594
+ if (surface === "screen" && words2 > MAX_SCREEN_WORDS) {
5561
5595
  findings.push({
5562
5596
  test: "fifteen-second",
5563
5597
  severity: "serious",
@@ -5618,7 +5652,30 @@ function screenshotFindings(copy, words2) {
5618
5652
  }
5619
5653
  return findings;
5620
5654
  }
5655
+ function slideFindings(words2, body) {
5656
+ const findings = [];
5657
+ if (words2 > MAX_SLIDE_WORDS) {
5658
+ findings.push({
5659
+ test: "slide",
5660
+ severity: "serious",
5661
+ problem: `The slide shows ${words2} words, so the audience reads instead of listening.`,
5662
+ evidence: `${words2} words of visible copy`,
5663
+ fix: `Keep a slide under about ${MAX_SLIDE_WORDS} words: a title that states the point and a few short lines.`
5664
+ });
5665
+ }
5666
+ if (body.length > MAX_SLIDE_LINES) {
5667
+ findings.push({
5668
+ test: "slide",
5669
+ severity: "serious",
5670
+ problem: `The slide stacks ${body.length} lines of copy, which usually means more than one idea.`,
5671
+ evidence: body.join(" | "),
5672
+ fix: "Give each idea its own slide, with the point in the title."
5673
+ });
5674
+ }
5675
+ return findings;
5676
+ }
5621
5677
  function analyzeComprehension(input) {
5678
+ const surface = input.surface ?? (SLIDE_NAME.test(input.screen) ? "slide" : "screen");
5622
5679
  const copy = resolveCopy(input);
5623
5680
  const known = new Set((input.knownTerms ?? []).map((term) => term.trim().toLowerCase()));
5624
5681
  const visible = [copy.headline ?? "", ...copy.body, ...copy.actions].join(". ");
@@ -5627,13 +5684,16 @@ function analyzeComprehension(input) {
5627
5684
  const estimatedExplainSeconds = estimateExplainSeconds(words2, insiderTerms.length, copy.actions.length);
5628
5685
  const findings = [
5629
5686
  ...momTestFindings(visible, known, insiderTerms),
5630
- ...fifteenSecondFindings(words2, copy.actions, estimatedExplainSeconds),
5631
- ...screenshotFindings(copy, words2)
5687
+ ...fifteenSecondFindings(words2, copy.actions, estimatedExplainSeconds, surface),
5688
+ ...screenshotFindings(copy, words2),
5689
+ ...surface === "slide" ? slideFindings(words2, copy.body) : []
5632
5690
  ];
5691
+ const blocking = findings.some((finding3) => finding3.severity !== "minor");
5633
5692
  const longestSentence = sentences(visible).sort((a, b) => countWords(b) - countWords(a))[0] ?? "";
5634
5693
  return {
5635
5694
  screen: input.screen,
5636
- verdict: findings.some((finding3) => finding3.severity !== "minor") ? "revise" : "pass",
5695
+ surface,
5696
+ verdict: blocking ? "revise" : surface === "slide" ? "needs-screenshot" : "pass",
5637
5697
  findings,
5638
5698
  copy: { headline: copy.headline, body: copy.body, actions: copy.actions },
5639
5699
  stats: {
@@ -5677,7 +5737,8 @@ function formatStats(report) {
5677
5737
  "",
5678
5738
  `- Visible copy: ${words2} words across ${actionCount} action${actionCount === 1 ? "" : "s"}.`,
5679
5739
  `- Insider words found: ${insiderTerms.length > 0 ? insiderTerms.join(", ") : "none"}.`,
5680
- `- Rough explain time: about ${estimatedExplainSeconds} seconds. This is a heuristic budget from word count, insider words and choices, not a measurement of real users.`
5740
+ `- Rough explain time: about ${estimatedExplainSeconds} seconds. This is a heuristic budget from word count, insider words and choices, not a measurement of real users.`,
5741
+ "- It read text only: the copy passed in, or the text in the markup. It cannot see screenshots, text inside images, or visual hierarchy."
5681
5742
  ].join("\n");
5682
5743
  }
5683
5744
  function formatComprehensionReport(report) {
@@ -5685,8 +5746,11 @@ function formatComprehensionReport(report) {
5685
5746
  (totals, finding3) => ({ ...totals, [finding3.severity]: totals[finding3.severity] + 1 }),
5686
5747
  { critical: 0, serious: 0, minor: 0 }
5687
5748
  );
5688
- const verdict = report.verdict === "pass" ? "Pass. Nothing blocking. Read the copy once more out loud before you present it." : `Revise before presenting. ${counts.critical} critical, ${counts.serious} serious, ${counts.minor} minor.`;
5689
- const next = report.verdict === "pass" ? "Say in one sentence what a first-time user can do on this screen. If that sentence needs a second sentence to make sense, simplify the screen." : "Rewrite the copy so it survives all three tests, then call check-comprehension again on the new copy. Do not present the screen while a critical or serious finding stands.";
5749
+ const { line: verdict, next } = report.verdict === "revise" ? {
5750
+ line: `Revise before presenting. ${counts.critical} critical, ${counts.serious} serious, ${counts.minor} minor.`,
5751
+ next: "Rewrite the copy so it survives every test, then call check-comprehension again on the new copy. Do not present the screen while a critical or serious finding stands."
5752
+ } : VERDICTS[report.verdict];
5753
+ const slideSection = report.surface === "slide" ? ["", formatTestSection(report, "slide")] : [];
5690
5754
  return [
5691
5755
  `# Comprehension check: ${report.screen}`,
5692
5756
  "",
@@ -5697,6 +5761,7 @@ function formatComprehensionReport(report) {
5697
5761
  formatTestSection(report, "fifteen-second"),
5698
5762
  "",
5699
5763
  formatTestSection(report, "screenshot"),
5764
+ ...slideSection,
5700
5765
  "",
5701
5766
  formatStats(report),
5702
5767
  "",
@@ -5705,16 +5770,27 @@ function formatComprehensionReport(report) {
5705
5770
  next
5706
5771
  ].join("\n");
5707
5772
  }
5708
- var TEST_TITLES, SEVERITY_ORDER;
5773
+ var TEST_TITLES, SEVERITY_ORDER, VERDICTS;
5709
5774
  var init_format = __esm({
5710
5775
  "../shared/src/mcp/comprehension/format.ts"() {
5711
5776
  "use strict";
5712
5777
  TEST_TITLES = {
5713
5778
  mom: "Mom test: would someone outside the team understand this?",
5714
5779
  "fifteen-second": "15 second test: can you explain the screen in 15 seconds?",
5715
- screenshot: "Screenshot test: with zero context, is it clear what this does?"
5780
+ screenshot: "Screenshot test: with zero context, is it clear what this does?",
5781
+ slide: "Slide test: one idea, and few enough words to hear the speaker?"
5716
5782
  };
5717
5783
  SEVERITY_ORDER = { critical: 0, serious: 1, minor: 2 };
5784
+ VERDICTS = {
5785
+ pass: {
5786
+ line: "Pass. Nothing blocking. Read the copy once more out loud before you present it.",
5787
+ next: "Say in one sentence what a first-time user can do on this screen. If that sentence needs a second sentence to make sense, simplify the screen."
5788
+ },
5789
+ "needs-screenshot": {
5790
+ line: "The text passes. This is not approval: a slide passes only after you review its rendered screenshot.",
5791
+ next: "Render the slide at its real size and look at the screenshot. Confirm it carries one idea, the title reads first, and one element holds the eye. Split or restyle the slide if any of the three fails."
5792
+ }
5793
+ };
5718
5794
  }
5719
5795
  });
5720
5796
 
@@ -5739,6 +5815,8 @@ function registerComprehensionTools({ server, provider, track, userId }) {
5739
5815
 
5740
5816
  Review rules catch what is broken. This catches what is unclear: copy that makes sense to the people who built the product and to nobody else.
5741
5817
 
5818
+ It reads text only: the copy you pass, or the text it extracts from your markup. It does not see what renders: not screenshots, text inside images, or visual hierarchy. A pass is a text result, not visual approval.
5819
+
5742
5820
  Three tests, run on the screen's own words:
5743
5821
  1. Mom test. Would a person outside the team understand this? Flags insider vocabulary, unexplained acronyms and sentences too long to follow.
5744
5822
  2. 15 second test. If the screen takes longer than 15 seconds to explain, it is too complex. Flags word count, stacked concepts and competing actions.
@@ -5748,6 +5826,8 @@ Pass either the copy or the markup:
5748
5826
  - check-comprehension({ screen: "Lesson end card", headline: "Lesson complete", body: ["Claim the June badge by collecting more quest points from your daily, friend and weekend quests."], actions: ["Continue"] })
5749
5827
  - check-comprehension({ screen: "Billing page", code: "<section>\u2026the JSX you wrote\u2026</section>" })
5750
5828
 
5829
+ For a slide, pass surface: "slide" (a screen named slide, deck or presentation gets it by default) and call once per slide. A slide adds a density test, and it never passes on text: approve it only after you review its rendered screenshot.
5830
+
5751
5831
  Use knownTerms for words your audience genuinely knows, so the check stops flagging them. Fix every critical and serious finding, then call this again on the rewritten copy.`,
5752
5832
  inputSchema: z9.object({
5753
5833
  screen: z9.string().trim().min(1).max(120).describe("What this screen is, e.g. 'lesson end card', 'billing page'."),
@@ -5756,6 +5836,7 @@ Use knownTerms for words your audience genuinely knows, so the check stops flagg
5756
5836
  actions: z9.array(z9.string().trim().min(1).max(120)).max(20).optional().describe("Visible button and link labels, e.g. ['Claim badge', 'Not now']."),
5757
5837
  code: z9.string().max(6e4).optional().describe("JSX, TSX or HTML for the screen. Copy is extracted from it when headline/body/actions are omitted."),
5758
5838
  knownTerms: z9.array(z9.string().trim().min(1).max(60)).max(50).optional().describe("Words this audience already knows, so they are not flagged as insider language."),
5839
+ surface: z9.enum(["screen", "slide"]).optional().describe('"slide" adds the slide density test and withholds a pass until a screenshot review. Defaults to "slide" when the screen name says slide, deck or presentation.'),
5759
5840
  context: CONTEXT_ARG
5760
5841
  }),
5761
5842
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -13885,6 +13966,32 @@ Examples:
13885
13966
  - "fintech startup" \u2192 Stripe design system
13886
13967
  - "playful education app" \u2192 Duolingo design system
13887
13968
  - "minimal developer tools" \u2192 Linear design system`;
13969
+ const formatCatalogMatches = (results) => results.map(
13970
+ (r) => [
13971
+ // Markdown link so the NAME is clickable in the host (Claude Code,
13972
+ // ChatGPT, claude.ai all render markdown links). Keep it intact when
13973
+ // presenting so the user can open the preview in one click.
13974
+ `### [${r.title}](${APP_BASE_URL}/design-systems/${r.id})`,
13975
+ r.description,
13976
+ `_${r.personality.join(", ")}_ \xB7 id: \`${r.id}\``
13977
+ ].join("\n")
13978
+ ).join("\n\n");
13979
+ const askForBaseChoice = async (prompt) => {
13980
+ const heading = "# Choose a starting point first\n\nNothing was generated.";
13981
+ try {
13982
+ const results = await provider.searchDesignSystems(prompt, 4);
13983
+ if (results.length === 0) {
13984
+ return `${heading} No catalog system matched this prompt. Ask the user whether to go fully custom, then call create-design-system again with \`userConfirmedCustom: true\`.`;
13985
+ }
13986
+ return `${heading} Show the user these options, keeping each name as its clickable link, and ask which to start from or whether to go fully custom. Then call create-design-system again with the chosen \`id\` as \`baseDesignSystemId\`, or with \`userConfirmedCustom: true\` if the user chose fully custom. Do not pick for the user.
13987
+
13988
+ ---
13989
+
13990
+ ${formatCatalogMatches(results)}`;
13991
+ } catch {
13992
+ return `${heading} The catalog search failed, so call \`find-design-system\` with the app description, show the user the top matches, and ask which to start from or whether to go fully custom.`;
13993
+ }
13994
+ };
13888
13995
  const makeSearchHandler = (toolName, includeBuildRules = false) => async ({ query }) => {
13889
13996
  const start = Date.now();
13890
13997
  const finish = (response, topResults, isError = false, errorMessage) => {
@@ -13912,27 +14019,20 @@ ${includeBuildRules ? BUILD_TARGET_RULE : DESIGN_TRADEOFF_RULE}`
13912
14019
  topResults
13913
14020
  );
13914
14021
  }
13915
- const formatted = results.map(
13916
- (r) => [
13917
- // Markdown link so the NAME is clickable in the host (Claude Code,
13918
- // ChatGPT, claude.ai all render markdown links). Keep it intact when
13919
- // presenting so the user can open the preview in one click.
13920
- `### [${r.title}](${APP_BASE_URL}/design-systems/${r.id})`,
13921
- `${r.description}`,
13922
- `_${r.personality.join(", ")}_ \xB7 id: \`${r.id}\``
13923
- ].join("\n")
13924
- ).join("\n\n");
14022
+ const productPagePointer = COMMERCIAL_QUERY.test(query) ? `
14023
+
14024
+ ${PRODUCT_PAGE_POINTER}` : "";
13925
14025
  return finish(
13926
14026
  {
13927
14027
  content: [{
13928
14028
  type: "text",
13929
14029
  text: `# Catalog matches
13930
14030
 
13931
- Keep any already selected design system and its assigned icons; do not reopen that choice unless requested. If none is selected, present these as options, keeping each name as its clickable markdown link (no scores) so they can open the preview. Explain each option's expected buyer fit and trade-offs using known project context; do not claim conversion effects without evidence. Recommend by buyer, never by the product's material or subject (a steel tool does not need a steel-dark UI). For ecommerce and DTC product pages, default to light, warm, photography-led systems unless the brand already owns a dark identity. Ask which to start their brand from, or to go fully custom, then pass the chosen \`id\` as \`baseDesignSystemId\` to create-design-system.
14031
+ Keep any already selected design system and its assigned icons; do not reopen that choice unless requested. If none is selected, present these as options, keeping each name as its clickable markdown link (no scores) so they can open the preview. Explain each option's expected buyer fit and trade-offs using known project context; do not claim conversion effects without evidence. Recommend by buyer, never by the product's material or subject (a steel tool does not need a steel-dark UI). For ecommerce and DTC product pages, default to light, warm, photography-led systems unless the brand already owns a dark identity. Ask which to start their brand from, or to go fully custom, then pass the chosen \`id\` as \`baseDesignSystemId\` to create-design-system, or pass \`userConfirmedCustom: true\` if they choose fully custom.${productPagePointer}
13932
14032
 
13933
14033
  ---
13934
14034
 
13935
- ${formatted}
14035
+ ${formatCatalogMatches(results)}
13936
14036
 
13937
14037
  ---
13938
14038
 
@@ -14624,7 +14724,7 @@ _${installNote}_`,
14624
14724
 
14625
14725
  ${DESIGN_TRADEOFF_RULE}
14626
14726
 
14627
- **Use this for a new custom brand, even when the user never mentions design or branding** (a design system IS the app's brand). Keep an already selected system and its assigned icons unless the user requests a different direction; a redesign alone does not require regenerating the brand. When no system is selected, first call \`find-design-system\` with the app description, show the user the top ~4 matches, and ask which to start from (passed here as \`baseDesignSystemId\`) or whether to go fully custom. Then derive the \`prompt\` from the app (name, domain, vibe), generate, show it with \`get-design-system-status\`, and build with that brand. Also use it when the user explicitly wants a **custom** design system instead of installing an existing one via \`get-design-system-kit\`.
14727
+ **Use this for a new custom brand, even when the user never mentions design or branding** (a design system IS the app's brand). Keep an already selected system and its assigned icons unless the user requests a different direction; a redesign alone does not require regenerating the brand. When no system is selected, first call \`find-design-system\` with the app description, show the user the top ~4 matches, and ask which to start from (passed here as \`baseDesignSystemId\`) or whether to go fully custom (passed here as \`userConfirmedCustom: true\`). A call with neither, and no \`brandCss\`, generates nothing: it returns those matches for you to show the user. Then derive the \`prompt\` from the app (name, domain, vibe), generate, show it with \`get-design-system-status\`, and build with that brand. Also use it when the user explicitly wants a **custom** design system instead of installing an existing one via \`get-design-system-kit\`.
14628
14728
 
14629
14729
  **When the user has an existing brand** (their site, Figma file, or stylesheet): run \`extract-design-system\` or \`extract-from-url\` first and pass the resulting token sheet (the fenced \`\`\`css block, verbatim: the \`:root { ... }\` and optional \`.dark { ... }\` blocks) as \`brandCss\`. The generation is then pinned to that brand: no catalog aesthetic is retrieved, the sheet's colors, fonts and radii override everything, and later design-system switches are refused. Copy the block exactly; do not retype or trim it.
14630
14730
 
@@ -14637,6 +14737,7 @@ Share the project URL with the user right away so they can watch the generation
14637
14737
  inputSchema: z14.object({
14638
14738
  prompt: z14.string().min(1).max(1e4).describe("Describe the design system the user wants. E.g. 'A warm, minimal design system for a personal finance app, soft pastels, rounded corners' or 'A brutalist developer tool aesthetic with sharp edges and monospace type'."),
14639
14739
  baseDesignSystemId: z14.string().optional().describe("Optional. The catalog id the USER picked from the `find-design-system` matches you showed them, used to bias generation toward that aesthetic (e.g. a theme tweak on top of Linear). Leave empty only when the user chose fully custom."),
14740
+ userConfirmedCustom: z14.boolean().optional().describe("Set true only when the user saw the find-design-system matches and chose fully custom. Without it, baseDesignSystemId or brandCss, the tool returns the matches and generates nothing."),
14640
14741
  brandCss: z14.string().max(5e4).optional().describe("Optional. The user's extracted brand token sheet, verbatim: the fenced css block from extract-design-system / extract-from-url (`:root { --background: ...; }` plus optional `.dark { ... }`). When set, generation builds FROM this brand instead of a catalog look-alike, and baseDesignSystemId is ignored (a catalog base would leak its identity past the brand tokens).")
14641
14742
  }),
14642
14743
  // Each call starts a new generation and returns a new project id, so it
@@ -14650,7 +14751,10 @@ Share the project URL with the user right away so they can watch the generation
14650
14751
  openWorldHint: true
14651
14752
  }
14652
14753
  },
14653
- async ({ prompt, baseDesignSystemId, brandCss }) => tracked("create-design-system", { query: prompt, hasBase: Boolean(baseDesignSystemId), hasBrand: Boolean(brandCss) }, async () => {
14754
+ async ({ prompt, baseDesignSystemId, brandCss, userConfirmedCustom }) => tracked("create-design-system", { query: prompt, hasBase: Boolean(baseDesignSystemId), hasBrand: Boolean(brandCss), userConfirmedCustom: Boolean(userConfirmedCustom) }, async () => {
14755
+ if (!baseDesignSystemId && !brandCss && !userConfirmedCustom) {
14756
+ return { content: [{ type: "text", text: await askForBaseChoice(prompt) }] };
14757
+ }
14654
14758
  const startUrl = `${APP_BASE_URL}/api/projects/anonymous`;
14655
14759
  let projectId;
14656
14760
  const requestHeaders = { "Content-Type": "application/json" };
@@ -14890,7 +14994,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
14890
14994
  })
14891
14995
  );
14892
14996
  }
14893
- var SHADCN_INIT_IF_MISSING, SAFE_COMPONENT_NAME_RE, DS_INDEX_SCHEMA;
14997
+ var SHADCN_INIT_IF_MISSING, SAFE_COMPONENT_NAME_RE, DS_INDEX_SCHEMA, COMMERCIAL_QUERY, PRODUCT_PAGE_POINTER;
14894
14998
  var init_design_system_tools = __esm({
14895
14999
  "../shared/src/mcp/tools/design-system-tools.ts"() {
14896
15000
  "use strict";
@@ -14914,6 +15018,8 @@ var init_design_system_tools = __esm({
14914
15018
  DS_INDEX_SCHEMA = z14.object({
14915
15019
  baseComponents: z14.array(z14.object({ name: z14.string() })).optional()
14916
15020
  });
15021
+ COMMERCIAL_QUERY = /\b(product pages?|pdp|shops?|(?<!app )stores?|storefront|e-?commerce|webshop|dtc|d2c|shopify|checkout|buy box)\b|[$€£]\s?\d/i;
15022
+ PRODUCT_PAGE_POINTER = 'This looks like a product page or a store. Before you build, call `get-ui-principle({ topic: "product-pages" })` for section order by buyer objection, the squint test and buyer-matched tone. For a catalog or checkout, also call `get-ux-principle` with topic `commerce-catalog` or `commerce-checkout`.';
14917
15023
  }
14918
15024
  });
14919
15025
 
@@ -22989,7 +23095,7 @@ function columnToSchema(column, factory) {
22989
23095
  }
22990
23096
  return schema;
22991
23097
  }
22992
- function numberColumnToSchema(column, z37, coerce) {
23098
+ function numberColumnToSchema(column, z38, coerce) {
22993
23099
  let unsigned = column.getSQLType().includes("unsigned");
22994
23100
  let min;
22995
23101
  let max;
@@ -23057,20 +23163,20 @@ function numberColumnToSchema(column, z37, coerce) {
23057
23163
  min = Number.MIN_SAFE_INTEGER;
23058
23164
  max = Number.MAX_SAFE_INTEGER;
23059
23165
  }
23060
- let schema = coerce === true || coerce?.number ? z37.coerce.number() : z37.number();
23166
+ let schema = coerce === true || coerce?.number ? z38.coerce.number() : z38.number();
23061
23167
  schema = schema.min(min).max(max);
23062
23168
  return integer2 ? schema.int() : schema;
23063
23169
  }
23064
- function bigintColumnToSchema(column, z37, coerce) {
23170
+ function bigintColumnToSchema(column, z38, coerce) {
23065
23171
  const unsigned = column.getSQLType().includes("unsigned");
23066
23172
  const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
23067
23173
  const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
23068
- const schema = coerce === true || coerce?.bigint ? z37.coerce.bigint() : z37.bigint();
23174
+ const schema = coerce === true || coerce?.bigint ? z38.coerce.bigint() : z38.bigint();
23069
23175
  return schema.min(min).max(max);
23070
23176
  }
23071
- function stringColumnToSchema(column, z37, coerce) {
23177
+ function stringColumnToSchema(column, z38, coerce) {
23072
23178
  if (isColumnType(column, ["PgUUID"])) {
23073
- return z37.string().uuid();
23179
+ return z38.string().uuid();
23074
23180
  }
23075
23181
  let max;
23076
23182
  let regex;
@@ -23102,7 +23208,7 @@ function stringColumnToSchema(column, z37, coerce) {
23102
23208
  regex = /^[01]+$/;
23103
23209
  max = column.dimensions;
23104
23210
  }
23105
- let schema = coerce === true || coerce?.string ? z37.coerce.string() : z37.string();
23211
+ let schema = coerce === true || coerce?.string ? z38.coerce.string() : z38.string();
23106
23212
  schema = regex ? schema.regex(regex) : schema;
23107
23213
  return max && fixed ? schema.length(max) : max ? schema.max(max) : schema;
23108
23214
  }
@@ -23198,15 +23304,74 @@ var init_drizzle_zod = __esm({
23198
23304
  }
23199
23305
  });
23200
23306
 
23201
- // ../shared/src/schema.ts
23307
+ // ../shared/src/agent-feedback.ts
23202
23308
  import { z as z16 } from "zod";
23203
- var vector2, messageRoleEnum, messageTypeEnum, designSystemStatusEnum, onboardingPathValues, onboardingPathSchema, onboardingPathSqlValues, mcpIssueCategoryValues, mcpIssueStatusValues, mcpIssueImprovementStatusValues, mcpIssueImprovementVerdictValues, mcpIssueImprovementStatusSchema, mcpIssueImprovementVerdictSchema, mcpIssueImprovementStatusSqlValues, mcpIssueImprovementVerdictSqlValues, designConfigSchema, projectMemoryDataSchema, projects, messages, projectMemory, fragments, usage, scrapeBlock, resourceAccess, user, signupAlertDeliveries, billingAlertDeliveries, billingEmailDeliveries, TEAM_RULES_MAX_BYTES, teamRules, billingSubscription, githubInstallationClaim, githubInstallation, featureAccess, session, account, verification, jwks, oauthClient, oauthRefreshToken, oauthAccessToken, oauthConsent, designSystems, designSystemComponents, foundationalDocs, apiKeys, emailCampaignSends, emailCampaignSteps, projectEvents, iconLibraries, dsSearchQueries, mcpToolCalls, mcpClientConnections, evalVerdicts, generationInputs, mcpIssues, mcpIssueImprovements, projectsRelations, projectMemoryRelations, mcpIssuesRelations, mcpIssueImprovementsRelations, messagesRelations, fragmentsRelations, designSystemsRelations, designSystemComponentsRelations, insertProjectSchema, selectProjectSchema, insertMessageSchema, selectMessageSchema, insertFragmentSchema, selectFragmentSchema, insertProjectMemorySchema, selectProjectMemorySchema, insertDesignSystemSchema, selectDesignSystemSchema, insertDesignSystemComponentSchema, selectDesignSystemComponentSchema, insertFoundationalDocSchema, selectFoundationalDocSchema, insertIconLibrarySchema, selectIconLibrarySchema, insertApiKeySchema, selectApiKeySchema, insertUserSchema, selectUserSchema, insertSignupAlertDeliverySchema, selectSignupAlertDeliverySchema, insertEmailCampaignSendSchema, selectEmailCampaignSendSchema, insertEmailCampaignStepSchema, selectEmailCampaignStepSchema, insertMcpIssueImprovementSchema, selectMcpIssueImprovementSchema, pluginAuthNonces;
23309
+ var agentFeedbackKindValues, TEXT_MAX, SURFACE_MAX, SUMMARY_MAX, AGENT_FEEDBACK_CLIENT_MAX, FIELD_DESCRIPTIONS, optionalText, agentFeedbackInputSchema, AGENT_FEEDBACK_CONTRACT;
23310
+ var init_agent_feedback = __esm({
23311
+ "../shared/src/agent-feedback.ts"() {
23312
+ "use strict";
23313
+ agentFeedbackKindValues = [
23314
+ "bug",
23315
+ "missing-feature",
23316
+ "wrong-output",
23317
+ "docs-gap",
23318
+ "other"
23319
+ ];
23320
+ TEXT_MAX = 4e3;
23321
+ SURFACE_MAX = 200;
23322
+ SUMMARY_MAX = 500;
23323
+ AGENT_FEEDBACK_CLIENT_MAX = 100;
23324
+ FIELD_DESCRIPTIONS = {
23325
+ kind: "What kind of problem this is.",
23326
+ surface: "The endpoint path or MCP tool name you called.",
23327
+ summary: "One sentence: what is wrong or missing.",
23328
+ attempted: "What you tried to do, and the request you sent.",
23329
+ expected: "What you expected to get back.",
23330
+ actual: "What you got back instead.",
23331
+ client: "Your agent name and version."
23332
+ };
23333
+ optionalText = (description) => z16.string().trim().max(TEXT_MAX).optional().describe(description);
23334
+ agentFeedbackInputSchema = z16.object({
23335
+ kind: z16.enum(agentFeedbackKindValues).describe(FIELD_DESCRIPTIONS.kind),
23336
+ surface: z16.string().trim().min(1).max(SURFACE_MAX).describe(FIELD_DESCRIPTIONS.surface),
23337
+ summary: z16.string().trim().min(1).max(SUMMARY_MAX).describe(FIELD_DESCRIPTIONS.summary),
23338
+ attempted: optionalText(FIELD_DESCRIPTIONS.attempted),
23339
+ expected: optionalText(FIELD_DESCRIPTIONS.expected),
23340
+ actual: optionalText(FIELD_DESCRIPTIONS.actual),
23341
+ client: z16.string().trim().max(AGENT_FEEDBACK_CLIENT_MAX).optional().describe(FIELD_DESCRIPTIONS.client)
23342
+ });
23343
+ AGENT_FEEDBACK_CONTRACT = {
23344
+ method: "POST",
23345
+ path: "/api/v1/feedback",
23346
+ purpose: "Tell Better Design about a bug or a missing feature you hit while calling this API. A person reads every report.",
23347
+ fields: {
23348
+ kind: { required: true, description: FIELD_DESCRIPTIONS.kind, oneOf: agentFeedbackKindValues },
23349
+ surface: { required: true, description: FIELD_DESCRIPTIONS.surface, maxLength: SURFACE_MAX },
23350
+ summary: { required: true, description: FIELD_DESCRIPTIONS.summary, maxLength: SUMMARY_MAX },
23351
+ attempted: { required: false, description: FIELD_DESCRIPTIONS.attempted, maxLength: TEXT_MAX },
23352
+ expected: { required: false, description: FIELD_DESCRIPTIONS.expected, maxLength: TEXT_MAX },
23353
+ actual: { required: false, description: FIELD_DESCRIPTIONS.actual, maxLength: TEXT_MAX },
23354
+ client: { required: false, description: FIELD_DESCRIPTIONS.client, maxLength: AGENT_FEEDBACK_CLIENT_MAX }
23355
+ },
23356
+ notes: [
23357
+ "Send it with your Better Design API key: Authorization: Bearer <key>.",
23358
+ "Do not include secrets or personal data. Known secret shapes are removed before storage.",
23359
+ "Sending feedback never spends credit."
23360
+ ]
23361
+ };
23362
+ }
23363
+ });
23364
+
23365
+ // ../shared/src/schema.ts
23366
+ import { z as z17 } from "zod";
23367
+ var vector2, messageRoleEnum, messageTypeEnum, designSystemStatusEnum, onboardingPathValues, onboardingPathSchema, onboardingPathSqlValues, mcpIssueCategoryValues, mcpIssueStatusValues, mcpIssueImprovementStatusValues, mcpIssueImprovementVerdictValues, mcpIssueImprovementStatusSchema, mcpIssueImprovementVerdictSchema, mcpIssueImprovementStatusSqlValues, mcpIssueImprovementVerdictSqlValues, designConfigSchema, projectMemoryDataSchema, projects, messages, projectMemory, fragments, usage, scrapeBlock, resourceAccess, user, signupAlertDeliveries, billingAlertDeliveries, billingEmailDeliveries, TEAM_RULES_MAX_BYTES, teamRules, agentFeedbackStatusValues, agentFeedback, billingSubscription, githubInstallationClaim, githubInstallation, featureAccess, session, account, verification, jwks, oauthClient, oauthRefreshToken, oauthAccessToken, oauthConsent, designSystems, designSystemComponents, foundationalDocs, apiKeys, emailCampaignSends, emailCampaignSteps, projectEvents, iconLibraries, dsSearchQueries, mcpToolCalls, mcpClientConnections, evalVerdicts, generationInputs, mcpIssues, mcpIssueImprovements, projectsRelations, projectMemoryRelations, mcpIssuesRelations, mcpIssueImprovementsRelations, messagesRelations, fragmentsRelations, designSystemsRelations, designSystemComponentsRelations, insertProjectSchema, selectProjectSchema, insertMessageSchema, selectMessageSchema, insertFragmentSchema, selectFragmentSchema, insertProjectMemorySchema, selectProjectMemorySchema, insertDesignSystemSchema, selectDesignSystemSchema, insertDesignSystemComponentSchema, selectDesignSystemComponentSchema, insertFoundationalDocSchema, selectFoundationalDocSchema, insertIconLibrarySchema, selectIconLibrarySchema, insertApiKeySchema, selectApiKeySchema, insertUserSchema, selectUserSchema, insertSignupAlertDeliverySchema, selectSignupAlertDeliverySchema, insertEmailCampaignSendSchema, selectEmailCampaignSendSchema, insertEmailCampaignStepSchema, selectEmailCampaignStepSchema, insertMcpIssueImprovementSchema, selectMcpIssueImprovementSchema, pluginAuthNonces;
23204
23368
  var init_schema2 = __esm({
23205
23369
  "../shared/src/schema.ts"() {
23206
23370
  "use strict";
23207
23371
  init_pg_core();
23208
23372
  init_drizzle_orm();
23209
23373
  init_drizzle_zod();
23374
+ init_agent_feedback();
23210
23375
  vector2 = customType({
23211
23376
  dataType() {
23212
23377
  return "vector(768)";
@@ -23234,7 +23399,7 @@ var init_schema2 = __esm({
23234
23399
  "skip",
23235
23400
  "legacy"
23236
23401
  ];
23237
- onboardingPathSchema = z16.enum(onboardingPathValues);
23402
+ onboardingPathSchema = z17.enum(onboardingPathValues);
23238
23403
  onboardingPathSqlValues = sql.raw(
23239
23404
  onboardingPathValues.map((path6) => `'${path6}'`).join(", ")
23240
23405
  );
@@ -23257,10 +23422,10 @@ var init_schema2 = __esm({
23257
23422
  "not_improved",
23258
23423
  "inconclusive"
23259
23424
  ];
23260
- mcpIssueImprovementStatusSchema = z16.enum(
23425
+ mcpIssueImprovementStatusSchema = z17.enum(
23261
23426
  mcpIssueImprovementStatusValues
23262
23427
  );
23263
- mcpIssueImprovementVerdictSchema = z16.enum(
23428
+ mcpIssueImprovementVerdictSchema = z17.enum(
23264
23429
  mcpIssueImprovementVerdictValues
23265
23430
  );
23266
23431
  mcpIssueImprovementStatusSqlValues = sql.raw(
@@ -23269,38 +23434,38 @@ var init_schema2 = __esm({
23269
23434
  mcpIssueImprovementVerdictSqlValues = sql.raw(
23270
23435
  mcpIssueImprovementVerdictValues.map((verdict) => `'${verdict}'`).join(", ")
23271
23436
  );
23272
- designConfigSchema = z16.object({
23273
- colors: z16.object({
23274
- primary: z16.string(),
23275
- secondary: z16.string(),
23276
- accent: z16.string(),
23277
- muted: z16.string(),
23278
- destructive: z16.string(),
23279
- background: z16.string(),
23280
- foreground: z16.string(),
23281
- card: z16.string(),
23282
- cardForeground: z16.string(),
23283
- popover: z16.string(),
23284
- popoverForeground: z16.string(),
23285
- border: z16.string(),
23286
- input: z16.string(),
23287
- ring: z16.string()
23437
+ designConfigSchema = z17.object({
23438
+ colors: z17.object({
23439
+ primary: z17.string(),
23440
+ secondary: z17.string(),
23441
+ accent: z17.string(),
23442
+ muted: z17.string(),
23443
+ destructive: z17.string(),
23444
+ background: z17.string(),
23445
+ foreground: z17.string(),
23446
+ card: z17.string(),
23447
+ cardForeground: z17.string(),
23448
+ popover: z17.string(),
23449
+ popoverForeground: z17.string(),
23450
+ border: z17.string(),
23451
+ input: z17.string(),
23452
+ ring: z17.string()
23288
23453
  }).partial(),
23289
- radius: z16.string(),
23290
- font: z16.object({
23291
- sans: z16.string(),
23292
- mono: z16.string()
23454
+ radius: z17.string(),
23455
+ font: z17.object({
23456
+ sans: z17.string(),
23457
+ mono: z17.string()
23293
23458
  }).partial(),
23294
- icons: z16.object({
23295
- libraryId: z16.string(),
23296
- variant: z16.string().nullable()
23459
+ icons: z17.object({
23460
+ libraryId: z17.string(),
23461
+ variant: z17.string().nullable()
23297
23462
  }).partial()
23298
23463
  }).partial();
23299
- projectMemoryDataSchema = z16.object({
23300
- lockedIn: z16.array(z16.string()),
23301
- rejected: z16.array(z16.string()),
23302
- tone: z16.string().nullable(),
23303
- notes: z16.string().nullable()
23464
+ projectMemoryDataSchema = z17.object({
23465
+ lockedIn: z17.array(z17.string()),
23466
+ rejected: z17.array(z17.string()),
23467
+ tone: z17.string().nullable(),
23468
+ notes: z17.string().nullable()
23304
23469
  });
23305
23470
  projects = pgTable("projects", {
23306
23471
  id: uuid("id").primaryKey().defaultRandom(),
@@ -23580,6 +23745,43 @@ var init_schema2 = __esm({
23580
23745
  )
23581
23746
  ]
23582
23747
  );
23748
+ agentFeedbackStatusValues = [
23749
+ "open",
23750
+ "accepted",
23751
+ "rejected",
23752
+ "shipped"
23753
+ ];
23754
+ agentFeedback = pgTable(
23755
+ "agent_feedback",
23756
+ {
23757
+ id: uuid("id").primaryKey().defaultRandom(),
23758
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
23759
+ apiKeyId: text("api_key_id").references(() => apiKeys.id, { onDelete: "set null" }),
23760
+ kind: text("kind", { enum: agentFeedbackKindValues }).notNull(),
23761
+ // The endpoint path or MCP tool name the agent was calling.
23762
+ surface: text("surface").notNull(),
23763
+ summary: text("summary").notNull(),
23764
+ attempted: text("attempted"),
23765
+ expected: text("expected"),
23766
+ actual: text("actual"),
23767
+ // The agent's own name for itself, such as "claude-code/2.1".
23768
+ client: text("client"),
23769
+ // SHA-256 of kind, surface and the normalized summary, so identical
23770
+ // reports group in the admin queue without a model call.
23771
+ fingerprint: text("fingerprint").notNull(),
23772
+ status: text("status", { enum: agentFeedbackStatusValues }).notNull().default("open"),
23773
+ pullRequestUrl: text("pull_request_url"),
23774
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
23775
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
23776
+ },
23777
+ (t) => [
23778
+ index("agent_feedback_status_created_idx").on(t.status, t.createdAt),
23779
+ index("agent_feedback_fingerprint_idx").on(t.fingerprint),
23780
+ // Deleting a user or an API key looks up its reports through these.
23781
+ index("agent_feedback_user_id_idx").on(t.userId),
23782
+ index("agent_feedback_api_key_id_idx").on(t.apiKeyId)
23783
+ ]
23784
+ );
23583
23785
  billingSubscription = pgTable(
23584
23786
  "billing_subscription",
23585
23787
  {
@@ -24386,7 +24588,7 @@ var init_schema2 = __esm({
24386
24588
  });
24387
24589
 
24388
24590
  // ../shared/src/mcp/tools/design-config-tools.ts
24389
- import { z as z17 } from "zod";
24591
+ import { z as z18 } from "zod";
24390
24592
  function registerDesignConfigTools({
24391
24593
  server,
24392
24594
  tracked,
@@ -24437,7 +24639,7 @@ function registerDesignConfigTools({
24437
24639
  {
24438
24640
  title: "Get a Project's Tuned Design Config",
24439
24641
  description: `Read the user-tuned design config (exact color tokens, radius, fonts, icon library) of a generated project. Call this BEFORE building or restyling UI for the project, and again after the user tunes values on the project page, so you apply their exact numbers instead of guessing from adjectives.`,
24440
- inputSchema: z17.object({
24642
+ inputSchema: z18.object({
24441
24643
  projectId: PROJECT_ID_ARG,
24442
24644
  context: CONTEXT_ARG
24443
24645
  }),
@@ -24466,7 +24668,7 @@ function registerDesignConfigTools({
24466
24668
  {
24467
24669
  title: "Set a Project's Design Config",
24468
24670
  description: `Write exact design-token values (colors, radius, fonts, icon library) onto a generated project. Use this when the user states concrete values ("make primary oklch(0.62 0.11 45)", "radius 12px"). It merges over the current config and persists, so the change survives restarts and every later get-design-config returns it. Saved config is publicly readable by anyone with the fragment config URL. For vague direction ("warmer", "more playful"), iterate with the user toward concrete values first; do not invent numbers on their behalf.`,
24469
- inputSchema: z17.object({
24671
+ inputSchema: z18.object({
24470
24672
  projectId: PROJECT_ID_ARG,
24471
24673
  config: CONFIG_ARG,
24472
24674
  context: CONTEXT_ARG
@@ -24517,7 +24719,7 @@ var init_design_config_tools = __esm({
24517
24719
  "use strict";
24518
24720
  init_schema2();
24519
24721
  init_shared();
24520
- PROJECT_ID_ARG = z17.string().min(1).describe("The project id returned by create-design-system.");
24722
+ PROJECT_ID_ARG = z18.string().min(1).describe("The project id returned by create-design-system.");
24521
24723
  CONFIG_ARG = designConfigSchema.describe(
24522
24724
  "Partial design config to merge over the current one. Only pass the fields the user changed. Colors are oklch() strings (e.g. 'oklch(0.62 0.11 45)'); radius is a CSS length (e.g. '0.75rem'); fonts are family names."
24523
24725
  );
@@ -24525,16 +24727,16 @@ var init_design_config_tools = __esm({
24525
24727
  });
24526
24728
 
24527
24729
  // ../shared/src/mcp/tools/jig-tools.ts
24528
- import { z as z18 } from "zod";
24730
+ import { z as z19 } from "zod";
24529
24731
  function registerJigTools({ server, tracked }) {
24530
24732
  server.registerTool(
24531
24733
  "get-jig-guide",
24532
24734
  {
24533
24735
  title: "Get a Jig for Parameter Work",
24534
24736
  description: `Return the setup for a "jig", the tool you build to make the real thing precisely. Two kinds, both here. Use kind "tune-in-place" (default) when you are about to hard-code taste-based values in UI you are building: animation duration/easing/springs/stagger, particles, canvas physics, blur/glow/grain, gradient stops, shadow recipes, parallax speeds. It returns a control panel to wire to those parameters so the USER tunes by eye and you hard-code what they copy back. Use kind "standalone-tool" when the user wants a reusable custom tool instead: a generator, an image effect, a shader playground, an asset maker. That returns the Toolcraft starter kit (MIT), which ships a canvas and an export flow, with optional layers and a timeline. Tuning setups: React recommends DialKit (MIT, no re-render lag) with a dependency-free fallback; "vanilla" and "react-native" get dependency-free scaffolds.`,
24535
- inputSchema: z18.object({
24536
- kind: z18.enum(["tune-in-place", "standalone-tool"]).default("tune-in-place").describe("What the user needs. 'tune-in-place' (default): a control panel for parameters in the UI you are building. 'standalone-tool': a separate reusable design tool, scaffolded with Toolcraft."),
24537
- framework: z18.enum(["react", "vanilla", "react-native"]).optional().describe("Which tune-in-place setup to return. 'react' (default) for React/Next web projects; 'vanilla' for plain DOM or canvas work; 'react-native' for RN/Expo apps. Ignored when kind is 'standalone-tool'."),
24737
+ inputSchema: z19.object({
24738
+ kind: z19.enum(["tune-in-place", "standalone-tool"]).default("tune-in-place").describe("What the user needs. 'tune-in-place' (default): a control panel for parameters in the UI you are building. 'standalone-tool': a separate reusable design tool, scaffolded with Toolcraft."),
24739
+ framework: z19.enum(["react", "vanilla", "react-native"]).optional().describe("Which tune-in-place setup to return. 'react' (default) for React/Next web projects; 'vanilla' for plain DOM or canvas work; 'react-native' for RN/Expo apps. Ignored when kind is 'standalone-tool'."),
24538
24740
  context: CONTEXT_ARG
24539
24741
  }),
24540
24742
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -25366,7 +25568,7 @@ var init_brand_sources = __esm({
25366
25568
  });
25367
25569
 
25368
25570
  // ../shared/src/mcp/tools/extract-tools.ts
25369
- import { z as z19 } from "zod";
25571
+ import { z as z20 } from "zod";
25370
25572
  function authHeaders2(authContext) {
25371
25573
  if (!authContext) return {};
25372
25574
  const bearer = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
@@ -25403,15 +25605,15 @@ JSON.stringify(${buildRoleSampleScript()})
25403
25605
  - kind "tokens": a design-tokens JSON export ($value entries, W3C format or the Figma variables API shape with 0..1 rgb floats).
25404
25606
 
25405
25607
  Common vendor names (Shopify's --color-button family) are mapped into empty semantic slots automatically and the mapping is reported in the output.`,
25406
- inputSchema: z19.object({
25407
- css: z19.string().trim().min(1).max(2e5).optional().describe("Full contents of the project's globals.css (or any CSS holding the design tokens / CSS custom properties). Optional when `sources` is given."),
25408
- sources: z19.array(z19.object({
25409
- name: z19.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
25410
- kind: z19.enum(["css", "computed", "tokens"]).optional().describe("Omit to infer: JSON with $value entries is tokens, JSON keyed by UI roles is computed, anything else is CSS."),
25411
- content: z19.string().trim().min(1).max(2e5)
25608
+ inputSchema: z20.object({
25609
+ css: z20.string().trim().min(1).max(2e5).optional().describe("Full contents of the project's globals.css (or any CSS holding the design tokens / CSS custom properties). Optional when `sources` is given."),
25610
+ sources: z20.array(z20.object({
25611
+ name: z20.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
25612
+ kind: z20.enum(["css", "computed", "tokens"]).optional().describe("Omit to infer: JSON with $value entries is tokens, JSON keyed by UI roles is computed, anything else is CSS."),
25613
+ content: z20.string().trim().min(1).max(2e5)
25412
25614
  })).max(10).optional().describe("Any mix of brand sources, merged in order (later sources win). See the tool description for how to gather each kind."),
25413
- name: z19.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
25414
- maxTokens: z19.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25615
+ name: z20.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
25616
+ maxTokens: z20.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25415
25617
  }),
25416
25618
  annotations: READ_ONLY_TOOL_ANNOTATIONS
25417
25619
  },
@@ -25442,10 +25644,10 @@ Common vendor names (Shopify's --color-button family) are mapped into empty sema
25442
25644
  Use this when the user names their site ("match uclips.eu", "use our website's style") or hands you any public URL whose look should be reused. Server side, Better Design fetches the page's stylesheets AND renders the page in a headless browser to sample what role elements actually look like (body, heading, link, button, input, card), then merges both, rendered truth last. That reads the brand even when the site's variable names are unrecognizable.
25443
25645
 
25444
25646
  Slow by nature (a real browser loads the page): expect 10 to 45 seconds. For CSS the user already has locally, prefer extract-design-system, which is instant.`,
25445
- inputSchema: z19.object({
25446
- url: z19.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
25447
- name: z19.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
25448
- maxTokens: z19.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25647
+ inputSchema: z20.object({
25648
+ url: z20.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
25649
+ name: z20.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
25650
+ maxTokens: z20.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25449
25651
  }),
25450
25652
  // Reads any public page the user names, so the set of hosts it reaches is
25451
25653
  // the open web, not Better Design's catalog. It stores nothing. [BD-316]
@@ -25495,7 +25697,7 @@ var init_extract_tools = __esm({
25495
25697
  });
25496
25698
 
25497
25699
  // ../shared/src/mcp/tools/illustration-tools.ts
25498
- import { z as z20 } from "zod";
25700
+ import { z as z21 } from "zod";
25499
25701
  function fail(message) {
25500
25702
  return {
25501
25703
  content: [{ type: "text", text: message }],
@@ -25541,11 +25743,11 @@ How to call it:
25541
25743
  4. Stop when \`faults\` is empty.
25542
25744
 
25543
25745
  Markup carrying script, event handlers, remote references, or styles that reach outside the drawing is refused rather than cleaned, so the drawing you fix is the drawing you sent.`,
25544
- inputSchema: z20.object({
25545
- svg: z20.string().min(1).describe("The complete svg element you drew."),
25546
- intent: z20.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
25547
- animated: z20.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
25548
- reviewToken: z20.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
25746
+ inputSchema: z21.object({
25747
+ svg: z21.string().min(1).describe("The complete svg element you drew."),
25748
+ intent: z21.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
25749
+ animated: z21.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
25750
+ reviewToken: z21.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
25549
25751
  }),
25550
25752
  outputSchema: REVIEW_OUTPUT_SCHEMA,
25551
25753
  // A review without a valid reviewToken passes the paywall, so it spends
@@ -25624,16 +25826,16 @@ var init_illustration_tools = __esm({
25624
25826
  init_shared();
25625
25827
  REVIEW_OUTPUT_SCHEMA = {
25626
25828
  /** Empty when nothing is visibly wrong. */
25627
- faults: z20.array(z20.string()),
25829
+ faults: z21.array(z21.string()),
25628
25830
  /** Group ids in paint order, the parts an animation can target. */
25629
- layers: z20.array(z20.string()),
25831
+ layers: z21.array(z21.string()),
25630
25832
  /** False when the markup was refused; faults says why. */
25631
- reviewed: z20.boolean(),
25833
+ reviewed: z21.boolean(),
25632
25834
  /** True when the drawing could not be rendered, so only structure was checked. */
25633
- unseen: z20.boolean(),
25835
+ unseen: z21.boolean(),
25634
25836
  /** Pass this back to review the same drawing again without spending another credit. */
25635
- reviewToken: z20.string(),
25636
- message: z20.string()
25837
+ reviewToken: z21.string(),
25838
+ message: z21.string()
25637
25839
  };
25638
25840
  }
25639
25841
  });
@@ -26995,7 +27197,7 @@ ul.evidence { padding-inline-start: 1rem; }
26995
27197
  });
26996
27198
 
26997
27199
  // ../shared/src/mcp/tools/inspection-tools.ts
26998
- import { z as z21 } from "zod";
27200
+ import { z as z22 } from "zod";
26999
27201
  function coversRecommendedWidth(states, width) {
27000
27202
  return states.some(
27001
27203
  (state) => Math.abs(state.viewport.width - width) <= WIDTH_TOLERANCE
@@ -27051,36 +27253,38 @@ function registerInspectionTools({ server, provider, tracked }) {
27051
27253
 
27052
27254
  This is the runtime counterpart to get-review-rules. It automatically loads the live visual-design rubric, but it does not guess pixel values from a screenshot. Exact claims come from DOM geometry.
27053
27255
 
27256
+ It measures geometry only. A score of 100 is not visual approval: it cannot judge hierarchy, density, or whether the design works, so look at a screenshot before you call a screen done.
27257
+
27054
27258
  Workflow:
27055
27259
  1. Call with no states to receive a read-only browser capture script in structuredContent.captureScript. Use detail: "full" if your client reads only text; it includes the script in text. That call measures nothing.
27056
27260
  2. Evaluate that script in each rendered state you need to review: at minimum 1440, 1728 and 390 wide. Also capture named hover, focus, selected, and open states when they change a control's surface or layout.
27057
27261
  3. Call again with the captured states and the project's spacing scale when known. The layout counts as measured only when this second call returns status \`completed\`; reading the snapshot yourself is not a measurement. The report and summary start with the highest-priority correction. Read structuredContent.findings or use detail: "full" for every returned finding, measurement, and fix. Structured evidence retains captured geometry and optional screenshots for report renderers.
27058
27262
 
27059
27263
  The capture reads visible layout geometry, computed spacing, corner radii, surface styles, the number of line boxes each text block rendered, and short accessible labels for interactive controls. It does not collect source code, cookies, form values, or network data. Add data-bd-inspect-label, data-bd-symmetry, data-bd-group, data-bd-compare-size, data-bd-align-group, and data-bd-align-edge attributes when the intended relationship cannot be inferred safely.`,
27060
- inputSchema: z21.object({
27061
- states: z21.array(spacingStateSchema).min(1).max(6).optional().describe("Rendered layout states from the capture script. Omit on the first call to receive the script."),
27062
- spacingScale: z21.array(finiteNumber.positive()).min(2).max(32).optional().describe("Project spacing tokens in CSS pixels. Defaults to 4, 8, 12, 16, 24, 32, 48, 64, 96, 128."),
27264
+ inputSchema: z22.object({
27265
+ states: z22.array(spacingStateSchema).min(1).max(6).optional().describe("Rendered layout states from the capture script. Omit on the first call to receive the script."),
27266
+ spacingScale: z22.array(finiteNumber.positive()).min(2).max(32).optional().describe("Project spacing tokens in CSS pixels. Defaults to 4, 8, 12, 16, 24, 32, 48, 64, 96, 128."),
27063
27267
  tolerance: finiteNumber.min(0).max(4).optional().describe("Pixel tolerance for grouping measurements. Defaults to 1px."),
27064
- detail: z21.enum(["summary", "full"]).default("summary").describe("Defaults to a short summary alongside the inline report. Use full for the capture script or all findings and measurements in text-only clients."),
27268
+ detail: z22.enum(["summary", "full"]).default("summary").describe("Defaults to a short summary alongside the inline report. Use full for the capture script or all findings and measurements in text-only clients."),
27065
27269
  context: CONTEXT_ARG
27066
27270
  }),
27067
- outputSchema: z21.object({
27068
- status: z21.enum(["needs-capture", "completed"]),
27069
- score: z21.number().optional(),
27070
- summary: z21.object({
27071
- critical: z21.number(),
27072
- serious: z21.number(),
27073
- moderate: z21.number(),
27074
- total: z21.number(),
27075
- truncated: z21.number().optional()
27271
+ outputSchema: z22.object({
27272
+ status: z22.enum(["needs-capture", "completed"]),
27273
+ score: z22.number().optional(),
27274
+ summary: z22.object({
27275
+ critical: z22.number(),
27276
+ serious: z22.number(),
27277
+ moderate: z22.number(),
27278
+ total: z22.number(),
27279
+ truncated: z22.number().optional()
27076
27280
  }).optional(),
27077
- findings: z21.array(findingSchema).optional(),
27078
- capturedWidths: z21.array(z21.number()).optional(),
27079
- missingWidths: z21.array(z21.number()).optional(),
27080
- states: z21.array(overlayStateSchema).optional(),
27081
- rubric: z21.array(z21.object({ id: z21.string(), title: z21.string() })).optional(),
27082
- rubricStatus: z21.enum(["loaded", "unavailable"]).optional(),
27083
- captureScript: z21.string().optional()
27281
+ findings: z22.array(findingSchema).optional(),
27282
+ capturedWidths: z22.array(z22.number()).optional(),
27283
+ missingWidths: z22.array(z22.number()).optional(),
27284
+ states: z22.array(overlayStateSchema).optional(),
27285
+ rubric: z22.array(z22.object({ id: z22.string(), title: z22.string() })).optional(),
27286
+ rubricStatus: z22.enum(["loaded", "unavailable"]).optional(),
27287
+ captureScript: z22.string().optional()
27084
27288
  }),
27085
27289
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
27086
27290
  _meta: {
@@ -27140,7 +27344,8 @@ ${SPACING_CAPTURE_SCRIPT}
27140
27344
  detail === "full" ? `Spacing score: ${result.score}/100.` : "",
27141
27345
  coverageLine,
27142
27346
  findingsText,
27143
- result.summary.truncated ? `${result.summary.truncated} additional findings exceeded the analysis limit. Inspect smaller sections before treating this review as complete.` : ""
27347
+ result.summary.truncated ? `${result.summary.truncated} additional findings exceeded the analysis limit. Inspect smaller sections before treating this review as complete.` : "",
27348
+ GEOMETRY_ONLY
27144
27349
  ].filter(Boolean).join("\n\n");
27145
27350
  return {
27146
27351
  content: [{ type: "text", text: text3 }],
@@ -27157,7 +27362,7 @@ ${SPACING_CAPTURE_SCRIPT}
27157
27362
  )
27158
27363
  );
27159
27364
  }
27160
- var finiteNumber, rectSchema, edgesSchema, cornerRadiusAxesSchema, cornerRadiusSchema, cornerRadiiSchema, styleSchema, elementSchema, screenshotSchema, spacingStateSchema, evidenceSchema, findingSchema, overlayStateSchema, RECOMMENDED_VIEWPORT_WIDTHS, WIDTH_TOLERANCE;
27365
+ var finiteNumber, rectSchema, edgesSchema, cornerRadiusAxesSchema, cornerRadiusSchema, cornerRadiiSchema, styleSchema, elementSchema, screenshotSchema, spacingStateSchema, evidenceSchema, findingSchema, overlayStateSchema, RECOMMENDED_VIEWPORT_WIDTHS, WIDTH_TOLERANCE, GEOMETRY_ONLY;
27161
27366
  var init_inspection_tools = __esm({
27162
27367
  "../shared/src/mcp/tools/inspection-tools.ts"() {
27163
27368
  "use strict";
@@ -27165,67 +27370,67 @@ var init_inspection_tools = __esm({
27165
27370
  init_finding_copy();
27166
27371
  init_spacing_inspection_html();
27167
27372
  init_shared();
27168
- finiteNumber = z21.number().finite();
27169
- rectSchema = z21.object({
27373
+ finiteNumber = z22.number().finite();
27374
+ rectSchema = z22.object({
27170
27375
  x: finiteNumber,
27171
27376
  y: finiteNumber,
27172
27377
  width: finiteNumber.nonnegative(),
27173
27378
  height: finiteNumber.nonnegative()
27174
27379
  });
27175
- edgesSchema = z21.object({
27380
+ edgesSchema = z22.object({
27176
27381
  top: finiteNumber,
27177
27382
  right: finiteNumber,
27178
27383
  bottom: finiteNumber,
27179
27384
  left: finiteNumber
27180
27385
  });
27181
- cornerRadiusAxesSchema = z21.object({
27386
+ cornerRadiusAxesSchema = z22.object({
27182
27387
  horizontal: finiteNumber.nonnegative(),
27183
27388
  vertical: finiteNumber.nonnegative()
27184
27389
  });
27185
- cornerRadiusSchema = z21.union([
27390
+ cornerRadiusSchema = z22.union([
27186
27391
  cornerRadiusAxesSchema,
27187
27392
  finiteNumber.nonnegative().transform((value) => ({
27188
27393
  horizontal: value,
27189
27394
  vertical: value
27190
27395
  }))
27191
27396
  ]);
27192
- cornerRadiiSchema = z21.object({
27397
+ cornerRadiiSchema = z22.object({
27193
27398
  topLeft: cornerRadiusSchema,
27194
27399
  topRight: cornerRadiusSchema,
27195
27400
  bottomRight: cornerRadiusSchema,
27196
27401
  bottomLeft: cornerRadiusSchema
27197
27402
  });
27198
- styleSchema = z21.object({
27199
- display: z21.string().max(60).optional(),
27200
- position: z21.string().max(60).optional(),
27201
- flexDirection: z21.string().max(60).optional(),
27202
- flexWrap: z21.string().max(60).optional(),
27203
- alignItems: z21.string().max(60).optional(),
27204
- justifyContent: z21.string().max(60).optional(),
27403
+ styleSchema = z22.object({
27404
+ display: z22.string().max(60).optional(),
27405
+ position: z22.string().max(60).optional(),
27406
+ flexDirection: z22.string().max(60).optional(),
27407
+ flexWrap: z22.string().max(60).optional(),
27408
+ alignItems: z22.string().max(60).optional(),
27409
+ justifyContent: z22.string().max(60).optional(),
27205
27410
  rowGap: finiteNumber.optional(),
27206
27411
  columnGap: finiteNumber.optional(),
27207
27412
  padding: edgesSchema.optional(),
27208
27413
  margin: edgesSchema.optional(),
27209
- overflowX: z21.string().max(60).optional(),
27210
- overflowY: z21.string().max(60).optional(),
27414
+ overflowX: z22.string().max(60).optional(),
27415
+ overflowY: z22.string().max(60).optional(),
27211
27416
  borderRadius: cornerRadiiSchema.optional(),
27212
- backgroundColor: z21.string().max(160).optional(),
27213
- backgroundImage: z21.string().max(1e3).optional(),
27214
- boxShadow: z21.string().max(1e3).optional()
27417
+ backgroundColor: z22.string().max(160).optional(),
27418
+ backgroundImage: z22.string().max(1e3).optional(),
27419
+ boxShadow: z22.string().max(1e3).optional()
27215
27420
  });
27216
- elementSchema = z21.object({
27217
- id: z21.string().min(1).max(300),
27218
- selector: z21.string().max(500).optional(),
27219
- tag: z21.string().max(80).optional(),
27220
- role: z21.string().max(80).optional(),
27221
- label: z21.string().max(160).optional(),
27222
- parentId: z21.string().max(300).optional(),
27223
- groupId: z21.string().max(300).optional(),
27224
- alignmentGroup: z21.string().max(300).optional(),
27225
- alignmentEdge: z21.enum(["left", "right", "top", "bottom", "center-x", "center-y"]).optional(),
27226
- expectedSymmetry: z21.enum(["horizontal", "vertical", "both"]).optional(),
27227
- compareSize: z21.enum(["width", "height", "both"]).optional(),
27228
- interactive: z21.boolean().optional(),
27421
+ elementSchema = z22.object({
27422
+ id: z22.string().min(1).max(300),
27423
+ selector: z22.string().max(500).optional(),
27424
+ tag: z22.string().max(80).optional(),
27425
+ role: z22.string().max(80).optional(),
27426
+ label: z22.string().max(160).optional(),
27427
+ parentId: z22.string().max(300).optional(),
27428
+ groupId: z22.string().max(300).optional(),
27429
+ alignmentGroup: z22.string().max(300).optional(),
27430
+ alignmentEdge: z22.enum(["left", "right", "top", "bottom", "center-x", "center-y"]).optional(),
27431
+ expectedSymmetry: z22.enum(["horizontal", "vertical", "both"]).optional(),
27432
+ compareSize: z22.enum(["width", "height", "both"]).optional(),
27433
+ interactive: z22.boolean().optional(),
27229
27434
  rect: rectSchema,
27230
27435
  style: styleSchema.optional(),
27231
27436
  lineCount: finiteNumber.int().positive().optional(),
@@ -27234,44 +27439,45 @@ var init_inspection_tools = __esm({
27234
27439
  scrollWidth: finiteNumber.nonnegative().optional(),
27235
27440
  scrollHeight: finiteNumber.nonnegative().optional()
27236
27441
  });
27237
- screenshotSchema = z21.string().max(5e6).regex(/^data:image\/(?:png|jpeg|webp);base64,/, "Use a PNG, JPEG, or WebP data URL.");
27238
- spacingStateSchema = z21.object({
27239
- name: z21.string().min(1).max(100),
27240
- viewport: z21.object({
27442
+ screenshotSchema = z22.string().max(5e6).regex(/^data:image\/(?:png|jpeg|webp);base64,/, "Use a PNG, JPEG, or WebP data URL.");
27443
+ spacingStateSchema = z22.object({
27444
+ name: z22.string().min(1).max(100),
27445
+ viewport: z22.object({
27241
27446
  width: finiteNumber.positive().max(2e4),
27242
27447
  height: finiteNumber.positive().max(2e4),
27243
27448
  devicePixelRatio: finiteNumber.positive().max(10).optional()
27244
27449
  }),
27245
- elements: z21.array(elementSchema).max(1500),
27450
+ elements: z22.array(elementSchema).max(1500),
27246
27451
  screenshotDataUrl: screenshotSchema.optional().describe("Optional browser screenshot as a data URL. It remains in the structured evidence for report renderers and is not persisted.")
27247
27452
  });
27248
- evidenceSchema = z21.object({
27249
- label: z21.string(),
27250
- value: z21.union([z21.number(), z21.string()]),
27251
- unit: z21.enum(["px", "count"]).optional()
27453
+ evidenceSchema = z22.object({
27454
+ label: z22.string(),
27455
+ value: z22.union([z22.number(), z22.string()]),
27456
+ unit: z22.enum(["px", "count"]).optional()
27252
27457
  });
27253
- findingSchema = z21.object({
27254
- id: z21.string(),
27255
- ruleId: z21.string(),
27256
- state: z21.string(),
27257
- severity: z21.enum(["critical", "serious", "moderate"]),
27258
- message: z21.string(),
27259
- fix: z21.string(),
27260
- elementIds: z21.array(z21.string()),
27261
- evidence: z21.array(evidenceSchema)
27458
+ findingSchema = z22.object({
27459
+ id: z22.string(),
27460
+ ruleId: z22.string(),
27461
+ state: z22.string(),
27462
+ severity: z22.enum(["critical", "serious", "moderate"]),
27463
+ message: z22.string(),
27464
+ fix: z22.string(),
27465
+ elementIds: z22.array(z22.string()),
27466
+ evidence: z22.array(evidenceSchema)
27262
27467
  });
27263
- overlayStateSchema = z21.object({
27264
- name: z21.string(),
27265
- viewport: z21.object({
27266
- width: z21.number(),
27267
- height: z21.number(),
27268
- devicePixelRatio: z21.number().optional()
27468
+ overlayStateSchema = z22.object({
27469
+ name: z22.string(),
27470
+ viewport: z22.object({
27471
+ width: z22.number(),
27472
+ height: z22.number(),
27473
+ devicePixelRatio: z22.number().optional()
27269
27474
  }),
27270
- elements: z21.array(z21.object({ id: z21.string(), label: z21.string().optional(), rect: rectSchema })),
27271
- screenshotDataUrl: z21.string().optional()
27475
+ elements: z22.array(z22.object({ id: z22.string(), label: z22.string().optional(), rect: rectSchema })),
27476
+ screenshotDataUrl: z22.string().optional()
27272
27477
  });
27273
27478
  RECOMMENDED_VIEWPORT_WIDTHS = [390, 1440, 1728];
27274
27479
  WIDTH_TOLERANCE = 40;
27480
+ GEOMETRY_ONLY = "This measured geometry only. A clean result or a score of 100 is not visual approval: it cannot judge hierarchy, density or whether the design works.";
27275
27481
  }
27276
27482
  });
27277
27483
 
@@ -27310,7 +27516,7 @@ var init_inspection_resource = __esm({
27310
27516
  });
27311
27517
 
27312
27518
  // ../shared/src/mcp/tools/dashboard-review-tools.ts
27313
- import { z as z22 } from "zod";
27519
+ import { z as z23 } from "zod";
27314
27520
  function finding(id, severity, message, fix) {
27315
27521
  return { id, severity, message, fix };
27316
27522
  }
@@ -27817,11 +28023,11 @@ function registerDashboardReviewTools({ server, track }) {
27817
28023
  Pass the complete page module source, the unchanged fixture, the factual interpretation, and the installed Sidebar, StatCard, and Table source files. Never summarize, reconstruct, or submit only the overview component. The tool derives the overview contract from the fixture. It fails incomplete source, rewritten copy, unsupported claims, weak hierarchy, missing or under-composed installed components, raw aside/table markup, the wrong scan order, action color on every rate, and stepper rails on short flows.
27818
28024
 
27819
28025
  FAIL is blocking and returns an MCP tool error. Fix every finding and call the tool again until PASS. Then run check-comprehension and inspect-spacing, and call finalize-dashboard-review. Only its VALID source-bound receipt permits presentation.`,
27820
- inputSchema: z22.object({
27821
- screen: z22.string().trim().min(1).max(120).describe("What this dashboard is, e.g. 'funnel report overview'."),
27822
- source: z22.string().min(1).max(12e4).describe("Complete TSX or JSX source for the dashboard page."),
28026
+ inputSchema: z23.object({
28027
+ screen: z23.string().trim().min(1).max(120).describe("What this dashboard is, e.g. 'funnel report overview'."),
28028
+ source: z23.string().min(1).max(12e4).describe("Complete TSX or JSX source for the dashboard page."),
27823
28029
  fixture: dashboardFixtureSchema.describe("The request's unchanged fixture object. The first step is the overview; remaining steps are dependent task screens."),
27824
- interpretation: z22.string().trim().min(1).max(2e3).describe("Exact supplied or factually derived takeaway that must appear after records."),
28030
+ interpretation: z23.string().trim().min(1).max(2e3).describe("Exact supplied or factually derived takeaway that must appear after records."),
27825
28031
  componentSources: dashboardComponentSourcesSchema.optional().describe("Installed Sidebar, StatCard, and Table source files for compound-composition review."),
27826
28032
  context: CONTEXT_ARG
27827
28033
  }),
@@ -27869,34 +28075,34 @@ var init_dashboard_review_tools = __esm({
27869
28075
  "use strict";
27870
28076
  init_review_capture();
27871
28077
  init_shared();
27872
- dashboardFixtureSchema = z22.object({
27873
- stats: z22.array(z22.object({
27874
- label: z22.string().trim().min(1).max(300),
27875
- value: z22.string().trim().min(1).max(300)
28078
+ dashboardFixtureSchema = z23.object({
28079
+ stats: z23.array(z23.object({
28080
+ label: z23.string().trim().min(1).max(300),
28081
+ value: z23.string().trim().min(1).max(300)
27876
28082
  })).max(100).optional(),
27877
- records: z22.array(z22.object({
27878
- title: z22.string().trim().min(1).max(300),
27879
- detail: z22.string().trim().min(1).max(1e3),
27880
- meta: z22.string().trim().min(1).max(300),
27881
- status: z22.string().trim().min(1).max(300)
28083
+ records: z23.array(z23.object({
28084
+ title: z23.string().trim().min(1).max(300),
28085
+ detail: z23.string().trim().min(1).max(1e3),
28086
+ meta: z23.string().trim().min(1).max(300),
28087
+ status: z23.string().trim().min(1).max(300)
27882
28088
  })).max(100).optional(),
27883
- steps: z22.array(z22.object({
27884
- title: z22.string().trim().min(1).max(300),
27885
- description: z22.string().trim().min(1).max(1e3),
27886
- action: z22.string().trim().min(1).max(200)
28089
+ steps: z23.array(z23.object({
28090
+ title: z23.string().trim().min(1).max(300),
28091
+ description: z23.string().trim().min(1).max(1e3),
28092
+ action: z23.string().trim().min(1).max(200)
27887
28093
  })).min(2).max(50)
27888
28094
  });
27889
- dashboardComponentSourcesSchema = z22.object({
27890
- sidebar: z22.string().min(1).max(12e4),
27891
- statCard: z22.string().min(1).max(12e4),
27892
- table: z22.string().min(1).max(12e4)
28095
+ dashboardComponentSourcesSchema = z23.object({
28096
+ sidebar: z23.string().min(1).max(12e4),
28097
+ statCard: z23.string().min(1).max(12e4),
28098
+ table: z23.string().min(1).max(12e4)
27893
28099
  });
27894
28100
  }
27895
28101
  });
27896
28102
 
27897
28103
  // ../shared/src/mcp/tools/final-ui-review-tools.ts
27898
28104
  import { createHash as createHash2 } from "crypto";
27899
- import { z as z23 } from "zod";
28105
+ import { z as z24 } from "zod";
27900
28106
  function digest(value) {
27901
28107
  return createHash2("sha256").update(value).digest("hex");
27902
28108
  }
@@ -27907,7 +28113,9 @@ function comprehensionFindings(input) {
27907
28113
  const report = analyzeComprehension({
27908
28114
  screen: input.screen,
27909
28115
  code: input.visibleSource,
27910
- knownTerms: input.knownTerms
28116
+ knownTerms: input.knownTerms,
28117
+ // A dashboard is a screen even when its name mentions slides. [BD-593]
28118
+ surface: "screen"
27911
28119
  });
27912
28120
  return report.findings.flatMap(
27913
28121
  (item, index2) => item.severity === "minor" ? [] : [
@@ -28308,15 +28516,15 @@ function registerFinalUiReviewTools({
28308
28516
  description: `Issue a source-bound receipt for a completed dashboard.
28309
28517
 
28310
28518
  Submit the public HTTPS preview plus exact source, unchanged data, and installed components. Better Design's own browser captures the deployed page at 390, 1440, and 1728 pixels and content-addresses the live deployment. No trusted browser capture means incomplete. The review blocks weak hierarchy, under-composed components, hidden fixture fields, unclear copy, spacing defects, unsupported interpretations, or source edits.`,
28311
- inputSchema: z23.object({
28312
- screen: z23.string().trim().min(1).max(120),
28313
- previewUrl: z23.string().url().max(2048).refine((value) => new URL(value).protocol === "https:", {
28519
+ inputSchema: z24.object({
28520
+ screen: z24.string().trim().min(1).max(120),
28521
+ previewUrl: z24.string().url().max(2048).refine((value) => new URL(value).protocol === "https:", {
28314
28522
  message: "Use a public HTTPS preview URL."
28315
28523
  }).describe("Public HTTPS URL for the exact deployed dashboard route."),
28316
- source: z23.string().min(1).max(24e4).describe(
28524
+ source: z24.string().min(1).max(24e4).describe(
28317
28525
  "Complete TSX or JSX page source, including its default export."
28318
28526
  ),
28319
- visibleSource: z23.string().min(1).max(12e4).describe(
28527
+ visibleSource: z24.string().min(1).max(12e4).describe(
28320
28528
  "Exact visible screen component copied unchanged from source."
28321
28529
  ),
28322
28530
  fixture: dashboardFixtureSchema.describe(
@@ -28325,7 +28533,7 @@ Submit the public HTTPS preview plus exact source, unchanged data, and installed
28325
28533
  componentSources: dashboardComponentSourcesSchema.describe(
28326
28534
  "Exact installed Sidebar, StatCard, and Table component source files."
28327
28535
  ),
28328
- knownTerms: z23.array(z23.string().trim().min(1).max(60)).max(50).optional(),
28536
+ knownTerms: z24.array(z24.string().trim().min(1).max(60)).max(50).optional(),
28329
28537
  context: CONTEXT_ARG
28330
28538
  }),
28331
28539
  annotations: READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS
@@ -28396,7 +28604,7 @@ var init_final_ui_review_tools = __esm({
28396
28604
  });
28397
28605
 
28398
28606
  // ../shared/src/mcp/tools/widget-tools.ts
28399
- import { z as z24 } from "zod";
28607
+ import { z as z25 } from "zod";
28400
28608
  function hasIntake(args) {
28401
28609
  return Boolean(args.widgetType || INTAKE_FIELDS.some(({ key }) => args[key]));
28402
28610
  }
@@ -28519,16 +28727,16 @@ var init_widget_tools = __esm({
28519
28727
  WIDGET_GUIDE_TOPIC = "better-widgets";
28520
28728
  WIDGET_GUIDE_TOOL = "get-widget-guide";
28521
28729
  MINIMUM_CONTRACT_HEADING = "## Minimum build contract";
28522
- WIDGET_GUIDE_INPUT_SCHEMA = z24.object({
28523
- widgetType: z24.enum(BETTER_WIDGET_TYPES).optional().describe("Widget kind: calculator, estimator, converter, generator, quiz, or checklist."),
28524
- userGoal: z24.string().trim().min(1).max(500).optional().describe("Who will use the widget and the decision or task it helps them complete."),
28525
- inputs: z24.string().trim().min(1).max(1e3).optional().describe("Required inputs, including units, allowed ranges, and defaults."),
28526
- interaction: z24.string().trim().min(1).max(750).optional().describe("How readers tune inputs and see feedback, including presets, custom controls, direct manipulation, and exact-value entry."),
28527
- transformation: z24.string().trim().min(1).max(1500).optional().describe("Formula, scoring rule, conversion, or transformation that produces the result."),
28528
- result: z24.string().trim().min(1).max(750).optional().describe("What the result shows, explains, and lets the user do next."),
28529
- hostFramework: z24.string().trim().min(1).max(300).optional().describe("The host framework and the page or component that will contain the widget."),
28530
- visualContext: z24.string().trim().min(1).max(750).optional().describe("Existing design tokens, components, or brand reference the widget must match."),
28531
- maxTokens: z24.number().min(2e3).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
28730
+ WIDGET_GUIDE_INPUT_SCHEMA = z25.object({
28731
+ widgetType: z25.enum(BETTER_WIDGET_TYPES).optional().describe("Widget kind: calculator, estimator, converter, generator, quiz, or checklist."),
28732
+ userGoal: z25.string().trim().min(1).max(500).optional().describe("Who will use the widget and the decision or task it helps them complete."),
28733
+ inputs: z25.string().trim().min(1).max(1e3).optional().describe("Required inputs, including units, allowed ranges, and defaults."),
28734
+ interaction: z25.string().trim().min(1).max(750).optional().describe("How readers tune inputs and see feedback, including presets, custom controls, direct manipulation, and exact-value entry."),
28735
+ transformation: z25.string().trim().min(1).max(1500).optional().describe("Formula, scoring rule, conversion, or transformation that produces the result."),
28736
+ result: z25.string().trim().min(1).max(750).optional().describe("What the result shows, explains, and lets the user do next."),
28737
+ hostFramework: z25.string().trim().min(1).max(300).optional().describe("The host framework and the page or component that will contain the widget."),
28738
+ visualContext: z25.string().trim().min(1).max(750).optional().describe("Existing design tokens, components, or brand reference the widget must match."),
28739
+ maxTokens: z25.number().min(2e3).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
28532
28740
  context: CONTEXT_ARG
28533
28741
  });
28534
28742
  INTAKE_FIELDS = [
@@ -28582,7 +28790,7 @@ The completed response combines a scoped build brief with Better Design's stable
28582
28790
  });
28583
28791
 
28584
28792
  // ../shared/src/mcp/feature-map/schema.ts
28585
- import { z as z25 } from "zod";
28793
+ import { z as z26 } from "zod";
28586
28794
  function oneLine(text3) {
28587
28795
  return text3.replace(/\s+/g, " ").trim();
28588
28796
  }
@@ -28617,38 +28825,38 @@ var init_schema3 = __esm({
28617
28825
  "use strict";
28618
28826
  FEATURE_MAP_PATH = ".better-design/feature-map.json";
28619
28827
  FEATURE_MAP_LIMITS = { features: 200, flows: 100, steps: 50, paths: 200, name: 120 };
28620
- NAME = z25.string().trim().min(1).max(FEATURE_MAP_LIMITS.name);
28621
- PATHS = z25.array(z25.string().trim().min(1).max(500)).max(FEATURE_MAP_LIMITS.paths).default([]);
28622
- featureMapStepSchema = z25.object({
28828
+ NAME = z26.string().trim().min(1).max(FEATURE_MAP_LIMITS.name);
28829
+ PATHS = z26.array(z26.string().trim().min(1).max(500)).max(FEATURE_MAP_LIMITS.paths).default([]);
28830
+ featureMapStepSchema = z26.object({
28623
28831
  name: NAME,
28624
28832
  /** URL path such as `/settings/billing`. Absent for a dialog or an email. */
28625
- route: z25.string().trim().max(300).optional(),
28833
+ route: z26.string().trim().max(300).optional(),
28626
28834
  /** The file that renders the step, such as the route's page file. */
28627
- screen: z25.string().trim().max(500).optional(),
28835
+ screen: z26.string().trim().max(500).optional(),
28628
28836
  /** What the user does here, such as "confirms the cancellation". */
28629
- action: z25.string().trim().max(300).optional(),
28837
+ action: z26.string().trim().max(300).optional(),
28630
28838
  components: PATHS,
28631
28839
  /** API routes, server actions, hooks and queries the step reads or writes. */
28632
28840
  data: PATHS
28633
28841
  });
28634
- featureMapFlowSchema = z25.object({
28842
+ featureMapFlowSchema = z26.object({
28635
28843
  id: NAME,
28636
28844
  name: NAME,
28637
- description: z25.string().trim().max(500).optional(),
28638
- steps: z25.array(featureMapStepSchema).min(1).max(FEATURE_MAP_LIMITS.steps)
28845
+ description: z26.string().trim().max(500).optional(),
28846
+ steps: z26.array(featureMapStepSchema).min(1).max(FEATURE_MAP_LIMITS.steps)
28639
28847
  });
28640
- featureMapFeatureSchema = z25.object({
28848
+ featureMapFeatureSchema = z26.object({
28641
28849
  id: NAME,
28642
28850
  name: NAME,
28643
- description: z25.string().trim().max(500).optional(),
28644
- flows: z25.array(featureMapFlowSchema).max(FEATURE_MAP_LIMITS.flows),
28851
+ description: z26.string().trim().max(500).optional(),
28852
+ flows: z26.array(featureMapFlowSchema).max(FEATURE_MAP_LIMITS.flows),
28645
28853
  /** Components that belong to the feature but to no single step. */
28646
28854
  components: PATHS
28647
28855
  });
28648
- featureMapSchema = z25.object({
28649
- version: z25.literal(1),
28650
- product: z25.string().trim().max(120).optional(),
28651
- features: z25.array(featureMapFeatureSchema).max(FEATURE_MAP_LIMITS.features)
28856
+ featureMapSchema = z26.object({
28857
+ version: z26.literal(1),
28858
+ product: z26.string().trim().max(120).optional(),
28859
+ features: z26.array(featureMapFeatureSchema).max(FEATURE_MAP_LIMITS.features)
28652
28860
  });
28653
28861
  }
28654
28862
  });
@@ -28926,7 +29134,7 @@ var init_read = __esm({
28926
29134
  });
28927
29135
 
28928
29136
  // ../shared/src/mcp/feature-map/update.ts
28929
- import { z as z26 } from "zod";
29137
+ import { z as z27 } from "zod";
28930
29138
  function find(items, ref) {
28931
29139
  const r = ref.trim();
28932
29140
  const index2 = items.findIndex((i) => i.id === r || i.name.toLowerCase() === r.toLowerCase());
@@ -28989,21 +29197,21 @@ var init_update2 = __esm({
28989
29197
  "../shared/src/mcp/feature-map/update.ts"() {
28990
29198
  "use strict";
28991
29199
  init_schema3();
28992
- NAME2 = z26.string().trim().min(1).max(120);
28993
- REF = z26.string().trim().min(1).max(120).describe("The id or exact name of an existing feature or flow.");
29200
+ NAME2 = z27.string().trim().min(1).max(120);
29201
+ REF = z27.string().trim().min(1).max(120).describe("The id or exact name of an existing feature or flow.");
28994
29202
  FLOW_INPUT = featureMapFlowSchema.omit({ id: true });
28995
- featureMapChangeSchema = z26.discriminatedUnion("op", [
28996
- z26.object({
28997
- op: z26.literal("add-feature"),
29203
+ featureMapChangeSchema = z27.discriminatedUnion("op", [
29204
+ z27.object({
29205
+ op: z27.literal("add-feature"),
28998
29206
  name: NAME2,
28999
- description: z26.string().trim().max(500).optional(),
29000
- flows: z26.array(FLOW_INPUT).max(100).optional()
29207
+ description: z27.string().trim().max(500).optional(),
29208
+ flows: z27.array(FLOW_INPUT).max(100).optional()
29001
29209
  }),
29002
- z26.object({ op: z26.literal("rename-feature"), feature: REF, name: NAME2 }),
29003
- z26.object({ op: z26.literal("remove-feature"), feature: REF }),
29004
- z26.object({ op: z26.literal("set-flow"), feature: REF, flow: FLOW_INPUT }),
29005
- z26.object({ op: z26.literal("rename-flow"), feature: REF, flow: REF, name: NAME2 }),
29006
- z26.object({ op: z26.literal("remove-flow"), feature: REF, flow: REF })
29210
+ z27.object({ op: z27.literal("rename-feature"), feature: REF, name: NAME2 }),
29211
+ z27.object({ op: z27.literal("remove-feature"), feature: REF }),
29212
+ z27.object({ op: z27.literal("set-flow"), feature: REF, flow: FLOW_INPUT }),
29213
+ z27.object({ op: z27.literal("rename-flow"), feature: REF, flow: REF, name: NAME2 }),
29214
+ z27.object({ op: z27.literal("remove-flow"), feature: REF, flow: REF })
29007
29215
  ]);
29008
29216
  idTaken = (items, id, except2) => items.some((item, i) => item.id === id && i !== except2);
29009
29217
  toFlow = (f) => ({ id: slugify(f.name), ...f });
@@ -29022,7 +29230,7 @@ var init_feature_map = __esm({
29022
29230
  });
29023
29231
 
29024
29232
  // ../shared/src/mcp/tools/feature-map-tools.ts
29025
- import { z as z27 } from "zod";
29233
+ import { z as z28 } from "zod";
29026
29234
  function registerFeatureMapTools({ server, tracked }) {
29027
29235
  server.registerTool(
29028
29236
  "build-feature-map",
@@ -29033,9 +29241,9 @@ function registerFeatureMapTools({ server, tracked }) {
29033
29241
  Run \`git ls-files\` in the repository and pass every path as \`files\`. Routes become screens (Next.js app and pages routers, Remix and React Router flat routes, SvelteKit, Nuxt), each chain of nested routes becomes a flow (/settings/billing/cancel is Settings \u2192 Billing \u2192 Cancel), and components, API routes, hooks and server actions join the step whose name they share.
29034
29242
 
29035
29243
  The result is a first draft. Write it to ${FEATURE_MAP_PATH}, show the user the flows, and ask which steps are missing: a dialog, an email or a background job has no route, so add those with update-feature-map (op "set-flow").`,
29036
- inputSchema: z27.object({
29037
- files: z27.array(z27.string().max(500)).min(1).max(5e4).describe("Every file path in the repository, relative to its root, as printed by `git ls-files`."),
29038
- product: z27.string().trim().max(120).optional().describe("The product's name, stored at the top of the map."),
29244
+ inputSchema: z28.object({
29245
+ files: z28.array(z28.string().max(500)).min(1).max(5e4).describe("Every file path in the repository, relative to its root, as printed by `git ls-files`."),
29246
+ product: z28.string().trim().max(120).optional().describe("The product's name, stored at the top of the map."),
29039
29247
  context: CONTEXT_ARG
29040
29248
  }),
29041
29249
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -29065,11 +29273,11 @@ ${serializeFeatureMap(map).trimEnd()}
29065
29273
  description: `Find the flows a change touches, with the screen, route, components and data files of every step. ${WHEN_TO_USE}
29066
29274
 
29067
29275
  Pass \`flow\` ("cancel account"), \`feature\` ("billing") or \`file\` (a path you are about to edit) to narrow it. With no query it lists every flow.`,
29068
- inputSchema: z27.object({
29276
+ inputSchema: z28.object({
29069
29277
  map: MAP_ARG,
29070
- feature: z27.string().trim().max(120).optional().describe("A feature id or part of its name."),
29071
- flow: z27.string().trim().max(120).optional().describe("A flow id or part of its name."),
29072
- file: z27.string().trim().max(500).optional().describe("A file path or route; returns every flow that uses it."),
29278
+ feature: z28.string().trim().max(120).optional().describe("A feature id or part of its name."),
29279
+ flow: z28.string().trim().max(120).optional().describe("A flow id or part of its name."),
29280
+ file: z28.string().trim().max(500).optional().describe("A file path or route; returns every flow that uses it."),
29073
29281
  context: CONTEXT_ARG
29074
29282
  }),
29075
29283
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -29095,7 +29303,7 @@ Changes, one per call:
29095
29303
  - { op: "remove-flow", feature, flow }
29096
29304
 
29097
29305
  \`feature\` and \`flow\` take an id or an exact name. Write the returned file to ${FEATURE_MAP_PATH}.`,
29098
- inputSchema: z27.object({
29306
+ inputSchema: z28.object({
29099
29307
  map: MAP_ARG,
29100
29308
  change: featureMapChangeSchema,
29101
29309
  context: CONTEXT_ARG
@@ -29117,7 +29325,7 @@ var init_feature_map_tools = __esm({
29117
29325
  "use strict";
29118
29326
  init_feature_map();
29119
29327
  init_shared();
29120
- MAP_ARG = z27.string().min(1).max(2e6).describe(`The exact contents of ${FEATURE_MAP_PATH}.`);
29328
+ MAP_ARG = z28.string().min(1).max(2e6).describe(`The exact contents of ${FEATURE_MAP_PATH}.`);
29121
29329
  text2 = (body, isError = false) => ({
29122
29330
  content: [{ type: "text", text: body }],
29123
29331
  isError
@@ -29139,7 +29347,7 @@ var init_feature_map_tools = __esm({
29139
29347
  });
29140
29348
 
29141
29349
  // ../shared/src/mcp/inspiration.ts
29142
- import { z as z28 } from "zod";
29350
+ import { z as z29 } from "zod";
29143
29351
  function describeSet(set) {
29144
29352
  const heading = `## ${set.title}${set.category ? ` (${set.category})` : ""}`;
29145
29353
  if (!set.notes) return heading;
@@ -29160,40 +29368,40 @@ var init_inspiration = __esm({
29160
29368
  "use strict";
29161
29369
  INSPIRATION_TYPES = ["app-store-screenshots"];
29162
29370
  MAX_INSPIRATION_LIMIT = 6;
29163
- noteSchema = z28.object({
29164
- slug: z28.string(),
29371
+ noteSchema = z29.object({
29372
+ slug: z29.string(),
29165
29373
  /** The day the note was written, as YYYY-MM-DD. A listing can change after it. */
29166
- writtenAt: z28.string(),
29167
- headline: z28.string(),
29168
- order: z28.string(),
29169
- framing: z28.string(),
29170
- color: z28.string()
29374
+ writtenAt: z29.string(),
29375
+ headline: z29.string(),
29376
+ order: z29.string(),
29377
+ framing: z29.string(),
29378
+ color: z29.string()
29171
29379
  });
29172
- inspirationNotesFileSchema = z28.object({
29173
- model: z28.string(),
29174
- notes: z28.array(noteSchema)
29380
+ inspirationNotesFileSchema = z29.object({
29381
+ model: z29.string(),
29382
+ notes: z29.array(noteSchema)
29175
29383
  });
29176
- indexFileSchema = z28.object({
29177
- key: z28.string(),
29178
- type: z28.string(),
29179
- width: z28.number(),
29180
- height: z28.number()
29384
+ indexFileSchema = z29.object({
29385
+ key: z29.string(),
29386
+ type: z29.string(),
29387
+ width: z29.number(),
29388
+ height: z29.number()
29181
29389
  });
29182
- indexEntrySchema = z28.object({
29183
- title: z28.string(),
29184
- slug: z28.string(),
29185
- category: z28.string().nullish(),
29186
- files: z28.array(indexFileSchema)
29390
+ indexEntrySchema = z29.object({
29391
+ title: z29.string(),
29392
+ slug: z29.string(),
29393
+ category: z29.string().nullish(),
29394
+ files: z29.array(indexFileSchema)
29187
29395
  });
29188
- inspirationIndexSchema = z28.object({
29189
- entries: z28.array(indexEntrySchema)
29396
+ inspirationIndexSchema = z29.object({
29397
+ entries: z29.array(indexEntrySchema)
29190
29398
  });
29191
29399
  NOTE_DATE = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
29192
29400
  }
29193
29401
  });
29194
29402
 
29195
29403
  // ../shared/src/mcp/tools/inspiration-tools.ts
29196
- import { z as z29 } from "zod";
29404
+ import { z as z30 } from "zod";
29197
29405
  function describeSets(result) {
29198
29406
  const lines = result.sets.map((set, i) => {
29199
29407
  const category = set.category ? `, ${set.category}` : "";
@@ -29221,10 +29429,10 @@ function registerInspirationTools({ server, tracked }, readInspiration2) {
29221
29429
  Use it before designing App Store or Google Play screenshots: pass \`type: "app-store-screenshots"\`, and a \`category\` such as "Health & Fitness" when the user's app has one. Each set arrives as its title and category, followed by its first screenshots.
29222
29430
 
29223
29431
  Then propose two or three directions drawn from what the sets do (headline style, slide order, device framing, color), and let the user choose before you build anything.`,
29224
- inputSchema: z29.object({
29225
- type: z29.enum(INSPIRATION_TYPES).describe('The kind of reference to send. Today: "app-store-screenshots".'),
29226
- category: z29.string().trim().min(1).max(60).optional().describe('Optional App Store category, for example "Productivity" or "Health & Fitness". Ignores case.'),
29227
- limit: z29.number().int().min(1).max(MAX_INSPIRATION_LIMIT).optional().describe(`How many sets to send, 1 to ${MAX_INSPIRATION_LIMIT}. Defaults to 3.`),
29432
+ inputSchema: z30.object({
29433
+ type: z30.enum(INSPIRATION_TYPES).describe('The kind of reference to send. Today: "app-store-screenshots".'),
29434
+ category: z30.string().trim().min(1).max(60).optional().describe('Optional App Store category, for example "Productivity" or "Health & Fitness". Ignores case.'),
29435
+ limit: z30.number().int().min(1).max(MAX_INSPIRATION_LIMIT).optional().describe(`How many sets to send, 1 to ${MAX_INSPIRATION_LIMIT}. Defaults to 3.`),
29228
29436
  context: CONTEXT_ARG
29229
29437
  }),
29230
29438
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -29261,8 +29469,62 @@ var init_inspiration_tools = __esm({
29261
29469
  }
29262
29470
  });
29263
29471
 
29472
+ // ../shared/src/mcp/tools/feedback-tools.ts
29473
+ function createFeedbackSender(apiFetch, apiKey) {
29474
+ return async (report) => {
29475
+ const res = await apiFetch(`${APP_BASE_URL}/api/v1/feedback`, {
29476
+ method: "POST",
29477
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
29478
+ body: JSON.stringify(report),
29479
+ signal: AbortSignal.timeout(15e3)
29480
+ });
29481
+ const body = await res.json().catch(() => ({}));
29482
+ if (!res.ok || !body.id) throw new Error(body.message ?? `HTTP ${res.status}`);
29483
+ return { id: body.id, status: body.status ?? "open", message: body.message ?? "" };
29484
+ };
29485
+ }
29486
+ function registerFeedbackTools({ server, tracked }, sendFeedback) {
29487
+ if (!sendFeedback) return;
29488
+ server.registerTool(
29489
+ "send-feedback",
29490
+ {
29491
+ title: "Send Feedback",
29492
+ description: `Tell Better Design about a bug, a missing feature or wrong output you hit while using its tools or API. A person reads every report and approves any fix.
29493
+
29494
+ Send one report per problem, once you have finished what the user asked. Say what you tried, what you expected and what came back. Leave out secrets, personal data and the user's private code. Sending a report never spends credit.`,
29495
+ inputSchema: reportSchema,
29496
+ // Each call stores one new report and publishes nothing. See
29497
+ // `docs/mcp-tool-annotations.md`.
29498
+ annotations: {
29499
+ readOnlyHint: false,
29500
+ destructiveHint: false,
29501
+ idempotentHint: false,
29502
+ openWorldHint: false
29503
+ }
29504
+ },
29505
+ // Usage telemetry records the kind only. The report itself goes to the queue.
29506
+ async (report) => tracked("send-feedback", { kind: report.kind }, async () => {
29507
+ const info = server.server.getClientVersion();
29508
+ const client = info ? `${info.name}/${info.version}`.slice(0, AGENT_FEEDBACK_CLIENT_MAX) : void 0;
29509
+ const receipt = await sendFeedback({ ...report, client });
29510
+ return {
29511
+ content: [{ type: "text", text: `Report ${receipt.id} sent. ${receipt.message}` }]
29512
+ };
29513
+ })
29514
+ );
29515
+ }
29516
+ var reportSchema;
29517
+ var init_feedback_tools = __esm({
29518
+ "../shared/src/mcp/tools/feedback-tools.ts"() {
29519
+ "use strict";
29520
+ init_agent_feedback();
29521
+ init_shared();
29522
+ reportSchema = agentFeedbackInputSchema.omit({ client: true });
29523
+ }
29524
+ });
29525
+
29264
29526
  // ../shared/src/mcp/tools.ts
29265
- function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId, captureDashboardBrowser2, readInspiration2) {
29527
+ function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId, captureDashboardBrowser2, readInspiration2, sendFeedback) {
29266
29528
  const { track, tracked } = createTracking(server, onToolUsage);
29267
29529
  const deps = {
29268
29530
  server,
@@ -29289,6 +29551,7 @@ function registerAllTools(server, provider, onToolUsage, authContext, options =
29289
29551
  registerFeatureMapTools(deps);
29290
29552
  registerTeamRulesTools(deps);
29291
29553
  registerInspirationTools(deps, readInspiration2);
29554
+ registerFeedbackTools(deps, sendFeedback);
29292
29555
  registerPreviewResource(deps);
29293
29556
  registerInspectionResource(deps);
29294
29557
  }
@@ -29313,6 +29576,7 @@ var init_tools = __esm({
29313
29576
  init_feature_map_tools();
29314
29577
  init_team_rules_tools();
29315
29578
  init_inspiration_tools();
29579
+ init_feedback_tools();
29316
29580
  init_shared();
29317
29581
  }
29318
29582
  });
@@ -29322,7 +29586,7 @@ var MCP_IMPLEMENTATION_VERSION;
29322
29586
  var init_version2 = __esm({
29323
29587
  "../shared/src/mcp/version.ts"() {
29324
29588
  "use strict";
29325
- MCP_IMPLEMENTATION_VERSION = "0.33.0";
29589
+ MCP_IMPLEMENTATION_VERSION = "0.35.0";
29326
29590
  }
29327
29591
  });
29328
29592
 
@@ -29355,6 +29619,7 @@ __export(mcp_exports, {
29355
29619
  analyzeDashboardStructure: () => analyzeDashboardStructure,
29356
29620
  buildShopifyPrimitiveSet: () => buildShopifyPrimitiveSet,
29357
29621
  canonicalMcpToolName: () => canonicalMcpToolName,
29622
+ createFeedbackSender: () => createFeedbackSender,
29358
29623
  createMcpServer: () => createMcpServer,
29359
29624
  declareAdvertisedSchemaDialect: () => declareAdvertisedSchemaDialect,
29360
29625
  deriveDashboardInterpretation: () => deriveDashboardInterpretation,
@@ -29388,7 +29653,8 @@ function createMcpServer(options) {
29388
29653
  userId,
29389
29654
  captureDashboardBrowser: captureDashboardBrowser2,
29390
29655
  directoryListing = false,
29391
- readInspiration: readInspiration2
29656
+ readInspiration: readInspiration2,
29657
+ sendFeedback
29392
29658
  } = options;
29393
29659
  const server = new McpServer(
29394
29660
  { name, version: version2 },
@@ -29416,7 +29682,8 @@ function createMcpServer(options) {
29416
29682
  apiFetch,
29417
29683
  userId,
29418
29684
  captureDashboardBrowser2,
29419
- readInspiration2
29685
+ readInspiration2,
29686
+ sendFeedback
29420
29687
  );
29421
29688
  return server;
29422
29689
  }
@@ -29433,6 +29700,7 @@ var init_mcp = __esm({
29433
29700
  init_advertised_schema();
29434
29701
  init_tool_names();
29435
29702
  init_principle_tools();
29703
+ init_feedback_tools();
29436
29704
  init_instructions();
29437
29705
  init_session_guidance();
29438
29706
  init_shared();
@@ -29461,6 +29729,7 @@ __export(api_client_exports, {
29461
29729
  searchIconLibraries: () => searchIconLibraries,
29462
29730
  searchIcons: () => searchIcons,
29463
29731
  searchPrinciples: () => searchPrinciples,
29732
+ sendAgentFeedback: () => sendAgentFeedback,
29464
29733
  trackMcpInstalled: () => trackMcpInstalled,
29465
29734
  trackToolUsage: () => trackToolUsage
29466
29735
  });
@@ -29500,6 +29769,9 @@ function readInspiration(input) {
29500
29769
  if (input.limit !== void 0) params.set("limit", String(input.limit));
29501
29770
  return request(`/v1/inspiration?${params}`, { timeoutMs: 3e4 });
29502
29771
  }
29772
+ function sendAgentFeedback(report) {
29773
+ return request("/v1/feedback", { method: "POST", body: report });
29774
+ }
29503
29775
  async function searchDesignSystems(query, limit = 5) {
29504
29776
  return request(
29505
29777
  `/v1/design-systems/search?q=${encodeURIComponent(query)}&limit=${limit}`
@@ -31476,7 +31748,7 @@ var init_engine = __esm({
31476
31748
  });
31477
31749
 
31478
31750
  // ../shared/src/lint/config.ts
31479
- import { z as z30 } from "zod";
31751
+ import { z as z31 } from "zod";
31480
31752
  function mergeReviewConfig(override) {
31481
31753
  if (!override) return DEFAULT_REVIEW_CONFIG;
31482
31754
  return {
@@ -31534,47 +31806,47 @@ var init_config = __esm({
31534
31806
  semanticTokens: { enabled: true }
31535
31807
  }
31536
31808
  };
31537
- reviewFlowStepSchema = z30.discriminatedUnion("action", [
31538
- z30.object({ action: z30.literal("click"), selector: z30.string() }),
31539
- z30.object({ action: z30.literal("type"), selector: z30.string(), value: z30.string() }),
31540
- z30.object({ action: z30.literal("press"), key: z30.string() }),
31541
- z30.object({ action: z30.literal("wait"), milliseconds: z30.number().finite().nonnegative() })
31809
+ reviewFlowStepSchema = z31.discriminatedUnion("action", [
31810
+ z31.object({ action: z31.literal("click"), selector: z31.string() }),
31811
+ z31.object({ action: z31.literal("type"), selector: z31.string(), value: z31.string() }),
31812
+ z31.object({ action: z31.literal("press"), key: z31.string() }),
31813
+ z31.object({ action: z31.literal("wait"), milliseconds: z31.number().finite().nonnegative() })
31542
31814
  ]);
31543
- visualReviewConfigSchema = z30.object({
31544
- enabled: z30.boolean().optional(),
31545
- model: z30.object({
31546
- enabled: z30.boolean().optional(),
31547
- maxScreenshots: z30.number().int().nonnegative().optional()
31815
+ visualReviewConfigSchema = z31.object({
31816
+ enabled: z31.boolean().optional(),
31817
+ model: z31.object({
31818
+ enabled: z31.boolean().optional(),
31819
+ maxScreenshots: z31.number().int().nonnegative().optional()
31548
31820
  }).optional(),
31549
- routes: z30.array(z30.string()).optional(),
31550
- viewports: z30.array(z30.enum(["desktop", "mobile", "reflow"])).optional(),
31551
- flows: z30.array(z30.object({
31552
- name: z30.string(),
31553
- persona: z30.string(),
31554
- path: z30.string().optional(),
31555
- steps: z30.array(reviewFlowStepSchema)
31821
+ routes: z31.array(z31.string()).optional(),
31822
+ viewports: z31.array(z31.enum(["desktop", "mobile", "reflow"])).optional(),
31823
+ flows: z31.array(z31.object({
31824
+ name: z31.string(),
31825
+ persona: z31.string(),
31826
+ path: z31.string().optional(),
31827
+ steps: z31.array(reviewFlowStepSchema)
31556
31828
  })).optional(),
31557
- componentGallery: z30.object({
31558
- enabled: z30.boolean().optional(),
31559
- componentSelector: z30.string().optional(),
31560
- canvasSelector: z30.string().optional(),
31561
- labelSelector: z30.string().optional(),
31562
- maxComponents: z30.number().int().nonnegative().optional()
31829
+ componentGallery: z31.object({
31830
+ enabled: z31.boolean().optional(),
31831
+ componentSelector: z31.string().optional(),
31832
+ canvasSelector: z31.string().optional(),
31833
+ labelSelector: z31.string().optional(),
31834
+ maxComponents: z31.number().int().nonnegative().optional()
31563
31835
  }).optional()
31564
31836
  });
31565
- reviewConfigOverrideSchema = z30.object({
31566
- designSystem: z30.string().optional(),
31837
+ reviewConfigOverrideSchema = z31.object({
31838
+ designSystem: z31.string().optional(),
31567
31839
  visual: visualReviewConfigSchema.optional(),
31568
- rules: z30.object({
31569
- elements: z30.record(z30.string(), z30.object({ use: z30.string(), from: z30.string() })).optional(),
31570
- tokens: z30.record(z30.string(), z30.string()).optional(),
31571
- wcag: z30.object({
31572
- enabled: z30.boolean().optional(),
31573
- severity: z30.enum(["all", "critical+serious"]).optional()
31840
+ rules: z31.object({
31841
+ elements: z31.record(z31.string(), z31.object({ use: z31.string(), from: z31.string() })).optional(),
31842
+ tokens: z31.record(z31.string(), z31.string()).optional(),
31843
+ wcag: z31.object({
31844
+ enabled: z31.boolean().optional(),
31845
+ severity: z31.enum(["all", "critical+serious"]).optional()
31574
31846
  }).optional(),
31575
- slop: z30.object({ enabled: z30.boolean().optional() }).optional(),
31576
- copy: z30.object({ enabled: z30.boolean().optional() }).optional(),
31577
- semanticTokens: z30.object({ enabled: z30.boolean().optional() }).optional()
31847
+ slop: z31.object({ enabled: z31.boolean().optional() }).optional(),
31848
+ copy: z31.object({ enabled: z31.boolean().optional() }).optional(),
31849
+ semanticTokens: z31.object({ enabled: z31.boolean().optional() }).optional()
31578
31850
  }).optional()
31579
31851
  });
31580
31852
  }
@@ -31950,7 +32222,7 @@ __export(review_tool_exports, {
31950
32222
  isReviewableSourcePath: () => isReviewableSourcePath,
31951
32223
  registerReviewTool: () => registerReviewTool
31952
32224
  });
31953
- import { z as z31 } from "zod";
32225
+ import { z as z32 } from "zod";
31954
32226
  function isReviewableSourcePath(path6) {
31955
32227
  return /\.(tsx|jsx|ts|js|css)$/i.test(path6);
31956
32228
  }
@@ -31974,11 +32246,11 @@ get-review-rules gives you the judgment rules to apply yourself. This tool is th
31974
32246
  This source reviewer cannot judge rendered hierarchy or semantic color roles. After it passes, inspect the rendered dashboard and confirm title \u2192 metrics \u2192 records \u2192 interpretation, distinct action and status colors, and that a stepper begins only after a task starts.
31975
32247
 
31976
32248
  Fix every critical and serious finding, then call it again on the fixed source. Findings feed the same loop as a human code review: read, fix, re-check.`,
31977
- inputSchema: z31.object({
31978
- files: z31.array(
31979
- z31.object({
31980
- path: z31.string().trim().min(1).max(400).describe("Repo-relative file path, e.g. components/signup-form.tsx."),
31981
- content: z31.string().max(MAX_FILE_CHARS).describe("The file's full source.")
32249
+ inputSchema: z32.object({
32250
+ files: z32.array(
32251
+ z32.object({
32252
+ path: z32.string().trim().min(1).max(400).describe("Repo-relative file path, e.g. components/signup-form.tsx."),
32253
+ content: z32.string().max(MAX_FILE_CHARS).describe("The file's full source.")
31982
32254
  })
31983
32255
  ).min(1).max(MAX_FILES).describe("The .tsx, .jsx, .ts, .js, and .css files to review, as written."),
31984
32256
  context: CONTEXT_ARG
@@ -32039,7 +32311,7 @@ review-ui-code({ files: [{ path: "components/signup-form.tsx", content: "..." },
32039
32311
  });
32040
32312
 
32041
32313
  // ../shared/src/mcp/figma-prototype.ts
32042
- import { z as z32 } from "zod";
32314
+ import { z as z33 } from "zod";
32043
32315
  function escapeHtml(value) {
32044
32316
  return value.replace(/[&<>"]/g, (char2) => ESCAPES.get(char2) ?? char2);
32045
32317
  }
@@ -32073,63 +32345,63 @@ var colorSchema, paintSchema, sizingSchema, baseSchema, prototypeNodeSchema, pro
32073
32345
  var init_figma_prototype = __esm({
32074
32346
  "../shared/src/mcp/figma-prototype.ts"() {
32075
32347
  "use strict";
32076
- colorSchema = z32.object({ r: z32.number(), g: z32.number(), b: z32.number(), a: z32.number() });
32077
- paintSchema = z32.object({ variable: z32.string().optional(), color: colorSchema.optional() });
32078
- sizingSchema = z32.enum(["FIXED", "HUG", "FILL"]);
32079
- baseSchema = z32.object({
32080
- name: z32.string(),
32081
- width: z32.number(),
32082
- height: z32.number(),
32083
- x: z32.number(),
32084
- y: z32.number(),
32085
- sizing: z32.object({ horizontal: sizingSchema, vertical: sizingSchema }).optional(),
32086
- absolute: z32.boolean().optional(),
32087
- opacity: z32.number().optional()
32348
+ colorSchema = z33.object({ r: z33.number(), g: z33.number(), b: z33.number(), a: z33.number() });
32349
+ paintSchema = z33.object({ variable: z33.string().optional(), color: colorSchema.optional() });
32350
+ sizingSchema = z33.enum(["FIXED", "HUG", "FILL"]);
32351
+ baseSchema = z33.object({
32352
+ name: z33.string(),
32353
+ width: z33.number(),
32354
+ height: z33.number(),
32355
+ x: z33.number(),
32356
+ y: z33.number(),
32357
+ sizing: z33.object({ horizontal: sizingSchema, vertical: sizingSchema }).optional(),
32358
+ absolute: z33.boolean().optional(),
32359
+ opacity: z33.number().optional()
32088
32360
  });
32089
- prototypeNodeSchema = z32.lazy(() => z32.discriminatedUnion("kind", [
32361
+ prototypeNodeSchema = z33.lazy(() => z33.discriminatedUnion("kind", [
32090
32362
  baseSchema.extend({
32091
- kind: z32.literal("instance"),
32092
- auditKey: z32.string(),
32093
- systemId: z32.string(),
32094
- texts: z32.array(z32.string()),
32095
- mainTexts: z32.array(z32.string())
32363
+ kind: z33.literal("instance"),
32364
+ auditKey: z33.string(),
32365
+ systemId: z33.string(),
32366
+ texts: z33.array(z33.string()),
32367
+ mainTexts: z33.array(z33.string())
32096
32368
  }),
32097
32369
  baseSchema.extend({
32098
- kind: z32.literal("frame"),
32099
- layout: z32.object({
32100
- direction: z32.enum(["HORIZONTAL", "VERTICAL"]),
32101
- gap: z32.number(),
32102
- padding: z32.tuple([z32.number(), z32.number(), z32.number(), z32.number()]),
32103
- primary: z32.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]),
32104
- counter: z32.enum(["MIN", "CENTER", "MAX", "BASELINE"]),
32105
- wrap: z32.boolean().optional()
32370
+ kind: z33.literal("frame"),
32371
+ layout: z33.object({
32372
+ direction: z33.enum(["HORIZONTAL", "VERTICAL"]),
32373
+ gap: z33.number(),
32374
+ padding: z33.tuple([z33.number(), z33.number(), z33.number(), z33.number()]),
32375
+ primary: z33.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]),
32376
+ counter: z33.enum(["MIN", "CENTER", "MAX", "BASELINE"]),
32377
+ wrap: z33.boolean().optional()
32106
32378
  }).optional(),
32107
32379
  fill: paintSchema.optional(),
32108
- stroke: z32.object({ paint: paintSchema, weight: z32.number() }).optional(),
32109
- radius: z32.number().optional(),
32110
- clips: z32.boolean().optional(),
32111
- children: z32.array(prototypeNodeSchema)
32380
+ stroke: z33.object({ paint: paintSchema, weight: z33.number() }).optional(),
32381
+ radius: z33.number().optional(),
32382
+ clips: z33.boolean().optional(),
32383
+ children: z33.array(prototypeNodeSchema)
32112
32384
  }),
32113
32385
  baseSchema.extend({
32114
- kind: z32.literal("text"),
32115
- characters: z32.string(),
32116
- fontSize: z32.number(),
32117
- fontWeight: z32.number(),
32118
- lineHeight: z32.number().optional(),
32119
- letterSpacing: z32.number().optional(),
32120
- fontFamily: z32.string().optional(),
32121
- align: z32.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]),
32386
+ kind: z33.literal("text"),
32387
+ characters: z33.string(),
32388
+ fontSize: z33.number(),
32389
+ fontWeight: z33.number(),
32390
+ lineHeight: z33.number().optional(),
32391
+ letterSpacing: z33.number().optional(),
32392
+ fontFamily: z33.string().optional(),
32393
+ align: z33.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]),
32122
32394
  fill: paintSchema.optional()
32123
32395
  }),
32124
32396
  baseSchema.extend({
32125
- kind: z32.literal("shape"),
32126
- svg: z32.string(),
32127
- reason: z32.string()
32397
+ kind: z33.literal("shape"),
32398
+ svg: z33.string(),
32399
+ reason: z33.string()
32128
32400
  })
32129
32401
  ]));
32130
- prototypeRequestSchema = z32.object({
32131
- type: z32.literal("better-design-figma-prototype"),
32132
- id: z32.string(),
32402
+ prototypeRequestSchema = z33.object({
32403
+ type: z33.literal("better-design-figma-prototype"),
32404
+ id: z33.string(),
32133
32405
  frame: prototypeNodeSchema
32134
32406
  });
32135
32407
  ESCAPES = /* @__PURE__ */ new Map([["&", "&amp;"], ["<", "&lt;"], [">", "&gt;"], ['"', "&quot;"]]);
@@ -32315,7 +32587,7 @@ var init_figma_model_prompt = __esm({
32315
32587
  });
32316
32588
 
32317
32589
  // ../shared/src/mcp/figma-model.ts
32318
- import { z as z33 } from "zod";
32590
+ import { z as z34 } from "zod";
32319
32591
  function fenced(text3, language) {
32320
32592
  const match = text3.match(new RegExp("```" + language + "[^\\n]*\\n([\\s\\S]*?)```", "i"));
32321
32593
  return match ? match[1] : null;
@@ -32521,8 +32793,8 @@ var init_figma_model = __esm({
32521
32793
  init_figma_prototype();
32522
32794
  init_html_kit();
32523
32795
  init_preview_html();
32524
- unplacedSchema = z33.object({
32525
- unplaced: z33.array(z33.object({ name: z33.string(), reason: z33.string() }))
32796
+ unplacedSchema = z34.object({
32797
+ unplaced: z34.array(z34.object({ name: z34.string(), reason: z34.string() }))
32526
32798
  });
32527
32799
  UNSAFE_BLOCKS = /<(script|iframe|object|embed|noscript)\b[\s\S]*?(<\/\1[^>]*>|$)/gi;
32528
32800
  UNSAFE_TAGS = /<\/?(meta|link|base|form|style)\b[^>]*(>|$)/gi;
@@ -32623,7 +32895,7 @@ var init_figma_model_handoff = __esm({
32623
32895
  });
32624
32896
 
32625
32897
  // ../shared/src/mcp/figma-model-providers.ts
32626
- import { z as z34 } from "zod";
32898
+ import { z as z35 } from "zod";
32627
32899
  function serverLabel(url) {
32628
32900
  try {
32629
32901
  const parsed = new URL(url);
@@ -32716,19 +32988,19 @@ var init_figma_model_providers = __esm({
32716
32988
  DEFAULT_LOCAL_MODEL = "gemma4:latest";
32717
32989
  DEFAULT_LOCAL_BASE_URL = "http://localhost:11434/v1";
32718
32990
  MAX_OUTPUT_TOKENS = 16e3;
32719
- chatReplySchema = z34.object({
32720
- choices: z34.array(z34.object({
32721
- message: z34.object({ content: z34.string().nullable().optional() }),
32722
- finish_reason: z34.string().nullable().optional()
32991
+ chatReplySchema = z35.object({
32992
+ choices: z35.array(z35.object({
32993
+ message: z35.object({ content: z35.string().nullable().optional() }),
32994
+ finish_reason: z35.string().nullable().optional()
32723
32995
  })).min(1),
32724
- usage: z34.object({ prompt_tokens: z34.number(), completion_tokens: z34.number() }).optional()
32996
+ usage: z35.object({ prompt_tokens: z35.number(), completion_tokens: z35.number() }).optional()
32725
32997
  });
32726
- errorCodeSchema = z34.object({ code: z34.string() });
32998
+ errorCodeSchema = z35.object({ code: z35.string() });
32727
32999
  }
32728
33000
  });
32729
33001
 
32730
33002
  // ../shared/src/mcp/figma-relay.ts
32731
- import { z as z35 } from "zod";
33003
+ import { z as z36 } from "zod";
32732
33004
  function noPluginMessage() {
32733
33005
  return "No Figma plugin is connected. Open the Figma file, run Plugins, Development, Better Design, and leave the panel open.";
32734
33006
  }
@@ -32742,35 +33014,35 @@ var init_figma_relay = __esm({
32742
33014
  "use strict";
32743
33015
  init_figma_prototype();
32744
33016
  FIGMA_RELAY_PORT = 4397;
32745
- relayRequestSchema = z35.object({
32746
- id: z35.string(),
33017
+ relayRequestSchema = z36.object({
33018
+ id: z36.string(),
32747
33019
  /** A frame name as it reads in Figma. Omitted means the current selection. */
32748
- frame: z35.string().optional(),
33020
+ frame: z36.string().optional(),
32749
33021
  /**
32750
33022
  * `model` asks for the serialized frame and its PNG instead of a rendered
32751
33023
  * page, so a model can rebuild a frame that holds no library component.
32752
33024
  * Omitted means the library path, unchanged. [BD-866]
32753
33025
  */
32754
- mode: z35.enum(["library", "model"]).optional()
33026
+ mode: z36.enum(["library", "model"]).optional()
32755
33027
  });
32756
- relayResultSchema = z35.object({
32757
- id: z35.string(),
32758
- html: z35.string().optional(),
32759
- error: z35.string().optional(),
32760
- components: z35.number().optional(),
32761
- report: z35.array(z35.object({ name: z35.string(), reason: z35.string() })).default([]),
32762
- frame: z35.string().optional(),
33028
+ relayResultSchema = z36.object({
33029
+ id: z36.string(),
33030
+ html: z36.string().optional(),
33031
+ error: z36.string().optional(),
33032
+ components: z36.number().optional(),
33033
+ report: z36.array(z36.object({ name: z36.string(), reason: z36.string() })).default([]),
33034
+ frame: z36.string().optional(),
32763
33035
  /** The frames the plugin found, so a missing name can say what is there. */
32764
- frames: z35.array(z35.string()).optional(),
33036
+ frames: z36.array(z36.string()).optional(),
32765
33037
  /** Model mode: the serialized frame, for the model to rebuild. */
32766
33038
  tree: prototypeNodeSchema.optional(),
32767
33039
  /** Model mode: the frame's PNG export, base64. */
32768
- png: z35.string().optional()
33040
+ png: z36.string().optional()
32769
33041
  });
32770
- relayHelloSchema = z35.object({
32771
- file: z35.string(),
33042
+ relayHelloSchema = z36.object({
33043
+ file: z36.string(),
32772
33044
  /** The design system the file holds, when the plugin knows it. */
32773
- system: z35.string().optional()
33045
+ system: z36.string().optional()
32774
33046
  });
32775
33047
  POLL_HOLD_MS = 25e3;
32776
33048
  REQUEST_TIMEOUT_MS = 3e5;
@@ -32938,7 +33210,7 @@ __export(figma_prototype_tool_exports, {
32938
33210
  });
32939
33211
  import { mkdir as mkdir2, readFile, stat, writeFile as writeFile2 } from "fs/promises";
32940
33212
  import { dirname as dirname3, resolve as resolve2 } from "path";
32941
- import { z as z36 } from "zod";
33213
+ import { z as z37 } from "zod";
32942
33214
  async function ensureRelay() {
32943
33215
  if (relay) return relay;
32944
33216
  starting ??= startFigmaRelay().then((started) => {
@@ -32998,30 +33270,30 @@ function registerFigmaPrototypeTool(server) {
32998
33270
  "yourself: the tool returns a prompt file and the frame's image, you write the answer to a file, and",
32999
33271
  "you call this tool again with frameId and answerPath so it checks the page and reports what is missing."
33000
33272
  ].join(" "),
33001
- inputSchema: z36.object({
33002
- frame: z36.string().optional().describe("The frame's name as it reads in Figma. Omit to use the current selection."),
33003
- outputPath: z36.string().optional().describe("Where to write the HTML. Relative paths resolve against the working directory."),
33004
- mode: z36.enum(["library", "model"]).optional().describe(
33273
+ inputSchema: z37.object({
33274
+ frame: z37.string().optional().describe("The frame's name as it reads in Figma. Omit to use the current selection."),
33275
+ outputPath: z37.string().optional().describe("Where to write the HTML. Relative paths resolve against the working directory."),
33276
+ mode: z37.enum(["library", "model"]).optional().describe(
33005
33277
  "library (the default) maps components from a Better Design library. model asks a model to rebuild a frame from any file."
33006
33278
  ),
33007
- system: z36.string().optional().describe("Model mode: the catalog design system to build with, as find-design-system names it."),
33008
- provider: z36.enum(["agent", "openai-compatible"]).optional().describe(
33279
+ system: z37.string().optional().describe("Model mode: the catalog design system to build with, as find-design-system names it."),
33280
+ provider: z37.enum(["agent", "openai-compatible"]).optional().describe(
33009
33281
  "Model mode: agent (the default) hands the frame to you to rebuild; openai-compatible calls a local server, Ollama by default."
33010
33282
  ),
33011
- model: z36.string().optional().describe(`Model mode with openai-compatible: the local model id. Defaults to ${DEFAULT_LOCAL_MODEL}.`),
33012
- frameId: z36.string().optional().describe("Model mode, second call: the frameId the first call returned."),
33013
- answerPath: z36.string().optional().describe("Model mode, second call: the file holding your answer, as the prompt file asks.")
33283
+ model: z37.string().optional().describe(`Model mode with openai-compatible: the local model id. Defaults to ${DEFAULT_LOCAL_MODEL}.`),
33284
+ frameId: z37.string().optional().describe("Model mode, second call: the frameId the first call returned."),
33285
+ answerPath: z37.string().optional().describe("Model mode, second call: the file holding your answer, as the prompt file asks.")
33014
33286
  }),
33015
- outputSchema: z36.object({
33016
- status: z36.enum(["completed", "failed", "needs-answer"]),
33017
- message: z36.string(),
33018
- frameId: z36.string().optional(),
33019
- promptPath: z36.string().optional(),
33020
- imagePath: z36.string().optional(),
33021
- path: z36.string().optional(),
33022
- components: z36.number().optional(),
33023
- report: z36.array(z36.object({ name: z36.string(), reason: z36.string() })).optional(),
33024
- model: z36.string().optional()
33287
+ outputSchema: z37.object({
33288
+ status: z37.enum(["completed", "failed", "needs-answer"]),
33289
+ message: z37.string(),
33290
+ frameId: z37.string().optional(),
33291
+ promptPath: z37.string().optional(),
33292
+ imagePath: z37.string().optional(),
33293
+ path: z37.string().optional(),
33294
+ components: z37.number().optional(),
33295
+ report: z37.array(z37.object({ name: z37.string(), reason: z37.string() })).optional(),
33296
+ model: z37.string().optional()
33025
33297
  }),
33026
33298
  annotations: { readOnlyHint: false, openWorldHint: true }
33027
33299
  },
@@ -33244,6 +33516,7 @@ async function startServer() {
33244
33516
  const {
33245
33517
  captureDashboardBrowser: captureDashboardBrowser2,
33246
33518
  readInspiration: readInspiration2,
33519
+ sendAgentFeedback: sendAgentFeedback2,
33247
33520
  trackToolUsage: trackToolUsage2,
33248
33521
  trackMcpInstalled: trackMcpInstalled2
33249
33522
  } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
@@ -33270,7 +33543,8 @@ async function startServer() {
33270
33543
  version: MCP_IMPLEMENTATION_VERSION2,
33271
33544
  onToolUsage,
33272
33545
  captureDashboardBrowser: captureDashboardBrowser2,
33273
- readInspiration: readInspiration2
33546
+ readInspiration: readInspiration2,
33547
+ sendFeedback: sendAgentFeedback2
33274
33548
  });
33275
33549
  registerReviewTool2(server, onToolUsage);
33276
33550
  registerFigmaPrototypeTool2(server);