raven-mcp 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/index.js +215 -1
- package/dist/index.js.map +1 -1
- package/dist/page-checks.js +46 -0
- package/dist/page-checks.js.map +1 -1
- package/dist/taste.d.ts +93 -0
- package/dist/taste.js +754 -0
- package/dist/taste.js.map +1 -0
- package/package.json +1 -1
- package/src/data/patterns/dropdown-menu.json +150 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A design knowledge MCP server that Claude can query when generating UI. Eight la
|
|
|
11
11
|
Raven gives Claude access to a comprehensive design knowledge base:
|
|
12
12
|
|
|
13
13
|
- **Principles** — Nielsen's 10 Heuristics, all 21 Laws of UX, Gestalt principles, WCAG accessibility, typography rules, color theory, mobile UX, D4D framework, UX writing, service design, brand, color-systems (palette-size discipline), and spacing-systems (base-unit grid + scale limits)
|
|
14
|
-
- **Patterns** — Proven UI patterns for signup flows, pricing pages, navigation, forms, landing pages, dashboards, modals, empty/error/loading states, CTAs, social proof, mobile conversion — plus content patterns (error messages, empty-state copy, notifications, form validation) and service patterns (service blueprinting, human handoff, signup-as-service, omnichannel continuity, moments of truth)
|
|
14
|
+
- **Patterns** — Proven UI patterns for signup flows, pricing pages, navigation, dropdown/select menus, forms, landing pages, dashboards, modals, empty/error/loading states, CTAs, social proof, mobile conversion — plus content patterns (error messages, empty-state copy, notifications, form validation) and service patterns (service blueprinting, human handoff, signup-as-service, omnichannel continuity, moments of truth)
|
|
15
15
|
- **Content systems** — Voice & tone guides from publicly documented brand systems: Mailchimp, GOV.UK, Shopify Polaris, and Atlassian
|
|
16
16
|
- **Research** — Qualitative, quantitative, and usability methods with do/don't protocols and checklists. Metrics frameworks: HEART, AARRR/Pirate, North Star Metric, conversion funnel, RICE, OKRs.
|
|
17
17
|
- **Service design** — Service blueprinting (with HTML blueprint generation — current vs. ideal state), human-handoff patterns, signup-as-service, omnichannel continuity, moments of truth / recovery, and the GOV.UK Service Standard
|
|
@@ -63,8 +63,9 @@ cd raven-mcp && npm install && npm run build
|
|
|
63
63
|
| `get_design_system` | Get tokens for a specific design system |
|
|
64
64
|
| `compose_system` | Mix tokens from different systems |
|
|
65
65
|
| `get_brand_system` | Get a full system styled like a well-known brand |
|
|
66
|
-
| `audit_page` | Audit HTML/CSS against Raven's quality standards — pass `html` for static audit, or `url` to render headless with optional `scroll_settle` (scroll to bottom + settle reveals) and `viewport` parameters; `containerMaxWidth` makes container checks token-aware. Pass `compact: true` to return only scores, violations, and fix_priority (drops embedded base64 screenshots) when the full payload is too large. |
|
|
67
|
-
| `
|
|
66
|
+
| `audit_page` | Audit HTML/CSS against Raven's quality standards — pass `html` for static audit, or `url` to render headless with optional `scroll_settle` (scroll to bottom + settle reveals) and `viewport` parameters; `containerMaxWidth` makes container checks token-aware. Also flags inline SVG icons that hardcode a color instead of using `currentColor`/a token. Pass `compact: true` to return only scores, violations, and fix_priority (drops embedded base64 screenshots) when the full payload is too large. |
|
|
67
|
+
| `score_page` | Return a per-category (0–10) design score for a page — typography, accessibility, spacing, color, responsive layout, design tokens, structure — derived from the same checks as `audit_page`, plus the overall score/grade, the weakest category, and categories Raven does not mechanically assess (brand, conversion, motion) |
|
|
68
|
+
| `audit_layout` | Evaluate visual rhythm, alignment, and optical balance; detects orphan-stretch (a lonely last-row grid/flex card stretching far wider than siblings) |
|
|
68
69
|
| `audit_responsive_visibility` | Render a URL at multiple breakpoints and flag content elements that are visible on desktop but hidden on mobile (display:none/opacity:0/zero-size) — categorises each as likely-oversight (content vanishing on mobile) vs intentional (decorative) |
|
|
69
70
|
| `audit_contrast` | Compute WCAG contrast ratios for every text element on a rendered page and report AA (4.5:1 / 3:1 large) and AAA pass-fail per element, with delta-to-pass for failures |
|
|
70
71
|
| `suggest_contrast_fix` | Given failing WCAG color pairs, return the minimal fg/bg change that clears the AA/AAA target — concrete passing values to fix `audit_contrast` failures |
|
|
@@ -73,6 +74,8 @@ cd raven-mcp && npm install && npm run build
|
|
|
73
74
|
| `audit_typography` | Typographic-**scale** report over rendered DOM text nodes (or a supplied snapshot) — detects the dominant modular-scale ratio and flags off-scale sizes, checks line-height consistency vs the body rhythm, and flags weight ladders >4 weights or non-standard values. Goes beyond `audit_page`'s pass/fail typography checks |
|
|
74
75
|
| `audit_tap_targets` | WCAG 2.5.5 / Apple 44pt **web** tap-target audit — enumerates every interactive element (rendered URL or snapshot) and emits a per-element fix table: selector, role, text, measured w/h, per-axis pixel deficit, and a concrete CSS fix, sorted worst-first |
|
|
75
76
|
| `audit_device_frame` | Flag cropped content in device-mockup frames — `frames` (container box + intrinsic media + object-fit, or a DevTools snippet) detects object-fit:cover crop loss when frame AR ≠ media AR; `clips` (first/last frame PNGs) detects baked-in pan/zoom (Ken Burns); `edge_frames` (PNGs) flags content truncated at a frame edge |
|
|
77
|
+
| `audit_video_playback` | Render a page and observe whether each `<video>` actually advances — samples currentTime, readyState, error codes, and autoplay-block state, then classifies each clip into playing | paused | stalled | empty | error with evidence. Catches black/non-playing videos that static frame-capture audits miss. Pass `url` to render and observe, or `dom_snapshot` for deterministic offline classification |
|
|
78
|
+
| `audit_consistency` | Corpus/multi-page audit — compares ≥2 pages and flags cross-page divergence in content-container width and hero heading tier, inferring the canonical (modal) value from the corpus when no token is supplied — catching relational defects that single-page audits miss |
|
|
76
79
|
| `audit_swiftui` | Audit SwiftUI source against Apple HIG — Dynamic Type, semantic colors, 44pt targets, 4/8pt spacing, AccentColor |
|
|
77
80
|
| `audit_ios_screen` | Score a rendered iOS screen from an accessibility/view-hierarchy snapshot — 44pt targets + contrast + rhythm, in points |
|
|
78
81
|
| `audit_ios_privacy` | Audit Info.plist (or Expo app.json) /PRIVACY.md/entitlements/source — usage-string honesty, ATS, Android permissions, bundled secrets, undisclosed default data-egress |
|
|
@@ -101,6 +104,11 @@ cd raven-mcp && npm install && npm run build
|
|
|
101
104
|
| `list_generation_jobs` | List local creative generation jobs |
|
|
102
105
|
| `plan_creative_campaign` | Plan a multi-asset campaign and optionally create draft generation jobs |
|
|
103
106
|
| `score_creative` | Score a prompt/script/concept for hook, benefit clarity, product signal, CTA, channel fit, audience fit, and brand fit |
|
|
107
|
+
| `create_taste_profile` | Create a named taste profile — a portable design-judgment ruleset (rule_id, clause, category, severity, negative prompt, owner) + precedent corpus, from explicit rules and/or a DESIGN.md-style markdown doc — persisted locally under `~/.raven/taste/` (`RAVEN_TASTE_HOME` override) |
|
|
108
|
+
| `get_taste_profile` | Load a stored taste profile's full rule catalog and precedent corpus |
|
|
109
|
+
| `list_taste_profiles` | List locally stored taste profiles with rule/corpus counts |
|
|
110
|
+
| `label_finding` | Append a human accept/revise/reject precedent to a profile's corpus — the growth loop; append-only, and accept-verdicts suppress that pattern in future audits |
|
|
111
|
+
| `audit_taste` | Judge HTML, copy text, or a live URL against a taste profile — deterministic detectors for gradients, glow/neon, second accent hue, and banned words; `owner: raven` rules route through Raven's existing page/contrast/tap-target engines; every finding cites a rule_id + concrete evidence (undetectable clauses are reported as `not_assessed`, never guessed); verdict BLOCK / WARN / PASS |
|
|
104
112
|
| `raven_reflect` | Summarize your local Raven usage log to find patterns + gaps |
|
|
105
113
|
|
|
106
114
|
## Creative studio
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { join, dirname } from "path";
|
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { capturePage, CaptureUnavailableError, annotateVideoArtifacts, verifyFindings } from "./capture.js";
|
|
9
9
|
import { runPageChecks } from "./page-checks.js";
|
|
10
|
+
import { scorePage } from "./score-page.js";
|
|
10
11
|
import { auditUrl } from "./audit-url.js";
|
|
11
12
|
import { captureResponsiveVisibility } from "./responsive.js";
|
|
12
13
|
import { auditContrastUrl, auditContrastSnapshot, suggestContrastFix } from "./contrast.js";
|
|
@@ -21,6 +22,10 @@ import { auditDeviceFrames, auditClipMotion, auditFrameEdges } from "./device-fr
|
|
|
21
22
|
import { auditTypographyUrl, auditTypographySnapshot } from "./typography.js";
|
|
22
23
|
import { auditTapTargetsUrl, auditTapTargetsSnapshot } from "./tap-targets.js";
|
|
23
24
|
import { compactAuditPage, compactEvaluation, compactAuditUrl } from "./compact.js";
|
|
25
|
+
import { auditVideoPlaybackUrl, auditVideoPlaybackSnapshot } from "./video-playback.js";
|
|
26
|
+
import { auditConsistency } from "./audit-consistency.js";
|
|
27
|
+
import { detectOrphanStretch } from "./layout-orphans.js";
|
|
28
|
+
import { createTasteProfile, getTasteProfile, listTasteProfiles, labelFinding, auditTaste } from "./taste.js";
|
|
24
29
|
// ── Path setup ──────────────────────────────────────────────────────
|
|
25
30
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
31
|
var PKG_ROOT = join(__dirname, "..");
|
|
@@ -926,6 +931,31 @@ function extractInsight(toolName, input, output) {
|
|
|
926
931
|
case "raven_reflect":
|
|
927
932
|
insight = { action: toolName };
|
|
928
933
|
break;
|
|
934
|
+
case "create_taste_profile": {
|
|
935
|
+
var tasteText = output?.content?.[0]?.text || "";
|
|
936
|
+
var tasteOut = JSON.parse(tasteText);
|
|
937
|
+
insight = { name: safeStr(input?.name, 32), rules: tasteOut.rules, corpus: tasteOut.corpus };
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
case "get_taste_profile":
|
|
941
|
+
case "list_taste_profiles":
|
|
942
|
+
insight = { action: toolName, name: safeStr(input?.name, 32) };
|
|
943
|
+
break;
|
|
944
|
+
case "label_finding":
|
|
945
|
+
insight = { profile: safeStr(input?.profile, 32), verdict: input?.verdict, rule: safeStr(input?.violated_rule, 48) };
|
|
946
|
+
break;
|
|
947
|
+
case "audit_taste": {
|
|
948
|
+
var atText = output?.content?.[0]?.text || "";
|
|
949
|
+
var atOut = JSON.parse(atText);
|
|
950
|
+
insight = {
|
|
951
|
+
profile: safeStr(input?.profile, 32),
|
|
952
|
+
verdict: atOut.verdict,
|
|
953
|
+
findings: (atOut.findings || []).map(function (f) { return f.rule_id; }),
|
|
954
|
+
suppressed: (atOut.suppressed || []).length,
|
|
955
|
+
not_assessed: (atOut.not_assessed || []).length
|
|
956
|
+
};
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
929
959
|
default:
|
|
930
960
|
insight = {};
|
|
931
961
|
}
|
|
@@ -2194,6 +2224,28 @@ server.tool("audit_page", "Audit HTML/CSS against Raven's design quality standar
|
|
|
2194
2224
|
}]
|
|
2195
2225
|
};
|
|
2196
2226
|
});
|
|
2227
|
+
// ── Tool 11b: score_page ───────────────────────────────────────────────────
|
|
2228
|
+
server.tool("score_page", "Score an HTML/CSS page across 7 design categories (Structure, Typography, Color & palette, Spacing & rhythm, Accessibility, Responsive layout, Design tokens), each rated 0–10. Scores are derived deterministically from the same checks as audit_page — no browser required. Also returns the same overall 0–100 score and A–D grade audit_page produces, the weakest category, and the three categories Raven does not mechanically assess (brand, conversion, motion) with guidance on which tools to use for those.", {
|
|
2229
|
+
html: z.string().min(1).describe("The full HTML content of the page to score."),
|
|
2230
|
+
strict: z.boolean().optional().describe("Strict mode — count warnings as failures in the overall score. Default: false."),
|
|
2231
|
+
containerMaxWidth: z.number().optional().describe("Your design system's canonical content-container width in px (e.g. 1152). Forwarded to the responsive/max-width check.")
|
|
2232
|
+
}, async function ({ html, strict, containerMaxWidth }) {
|
|
2233
|
+
if (html === undefined || html === null || html.trim() === "") {
|
|
2234
|
+
return {
|
|
2235
|
+
content: [{
|
|
2236
|
+
type: "text",
|
|
2237
|
+
text: "Provide html (the page's HTML/CSS) to score."
|
|
2238
|
+
}]
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
const result = scorePage(html, { strict, containerMaxWidth });
|
|
2242
|
+
return {
|
|
2243
|
+
content: [{
|
|
2244
|
+
type: "text",
|
|
2245
|
+
text: JSON.stringify(result, null, 2)
|
|
2246
|
+
}]
|
|
2247
|
+
};
|
|
2248
|
+
});
|
|
2197
2249
|
server.tool("audit_asset_integrity", "Detect PNG exports whose content is sliced/cut off at the bottom edge (e.g. a Figma export that ended mid-form). Dimension/ratio checks cannot catch cut content inside a correctly-sized file; this measures per-pixel luminance variance in the bottom strip — uniform background = clean, high-variance UI content running into the edge = likely-sliced. Accepts filesystem paths to PNGs.", { image_paths: z.array(z.string()).describe("Filesystem paths to PNG files to check for sliced/cut-off bottom content.") }, async function ({ image_paths }) {
|
|
2198
2250
|
var results = await auditAssetIntegrity(image_paths || []);
|
|
2199
2251
|
return { content: [{ type: "text", text: JSON.stringify({ tool: "audit_asset_integrity", results: results }, null, 2) }] };
|
|
@@ -3236,6 +3288,7 @@ server.tool("audit_layout", "Evaluate visual rhythm from a rendered page's geome
|
|
|
3236
3288
|
else {
|
|
3237
3289
|
findings.push({ check: "optical-balance", status: "warn", message: "Layout is " + (leftWeight > rightWeight ? "left-heavy" : "right-heavy") + " — visual weight skewed by " + Math.round(balanceSkew * 100) + "%", fix: balanceSkew > 0.4 ? "Redistribute dense blocks (images, tables) toward center, or add counterweight on the lighter side." : "Minor imbalance — review whether it's intentional asymmetry or accidental." });
|
|
3238
3290
|
}
|
|
3291
|
+
var orphanStretch = detectOrphanStretch(elements);
|
|
3239
3292
|
return {
|
|
3240
3293
|
content: [{
|
|
3241
3294
|
type: "text",
|
|
@@ -3247,7 +3300,8 @@ server.tool("audit_layout", "Evaluate visual rhythm from a rendered page's geome
|
|
|
3247
3300
|
alignment: { total_columns: colCounts.size, shared_columns: sharedCols, singleton_columns: singletonCols, aligned_ratio: Number(alignmentRatio.toFixed(2)) },
|
|
3248
3301
|
gap_rhythm: vGaps.length >= 3 ? { samples: vGaps.length, median_px: Math.round(gapMedian), stdev_px: Math.round(gapStdev), coef_variation: Number(gapCV.toFixed(2)) } : { samples: vGaps.length, note: "not enough horizontally-overlapping vertical sibling pairs" },
|
|
3249
3302
|
balance: { left_weight: Math.round(leftWeight), right_weight: Math.round(rightWeight), skew_pct: Math.round(balanceSkew * 100) }
|
|
3250
|
-
}
|
|
3303
|
+
},
|
|
3304
|
+
orphan_stretch: orphanStretch
|
|
3251
3305
|
}, null, 2)
|
|
3252
3306
|
}]
|
|
3253
3307
|
};
|
|
@@ -4700,6 +4754,38 @@ server.tool("score_creative", "Score a creative prompt, script, or ad concept fo
|
|
|
4700
4754
|
var result = scoreCreativeText(params.creative_text, params.channel, brand, audience);
|
|
4701
4755
|
return { content: [{ type: "text", text: JSON.stringify({ ...result, channel: params.channel || null, audience: audience || null, brand_profile_id: brand ? brand.id : null }, null, 2) }] };
|
|
4702
4756
|
});
|
|
4757
|
+
// ── Cross-page consistency audit ─────────────────────────────────────
|
|
4758
|
+
// audit_consistency: takes ≥2 pages ({name, html}) and flags divergence in
|
|
4759
|
+
// content-container width and hero heading tier across the corpus. Addresses
|
|
4760
|
+
// GitHub issue #9 — the single-blob audit blind spot where each page passes
|
|
4761
|
+
// audit_page clean but the pages disagree with each other.
|
|
4762
|
+
server.tool("audit_consistency", "Audit multiple pages for cross-page consistency of content-container width and hero heading tier. Pass ≥2 pages ({name, html}) collected from different routes on the same site. Infers the canonical (modal) value from the corpus when no token is supplied, so you need not know the project's design token in advance. Flags the issue #9 single-blob blind spot: pages that each pass audit_page but silently disagree with each other on container width or hero size class. Returns per-page extraction (container_px, container_classes, hero_classes, signatures), consistency dimensions with reference values, outlier page names, issues[], score (100/50/0 → A/C/D), and a plain-text summary. Pure offline — no browser, no network.", {
|
|
4763
|
+
pages: z.array(z.object({
|
|
4764
|
+
name: z.string().min(1).describe("Page route or label (e.g. \"/\", \"/changelog\", \"get-started\")."),
|
|
4765
|
+
html: z.string().min(1).describe("Full HTML source for the page, including any <style> blocks.")
|
|
4766
|
+
})).min(2).describe("At least 2 pages to compare. Each entry is {name, html}."),
|
|
4767
|
+
container_token: z.number().optional().describe("Project's canonical container width in px (e.g. 1152). When supplied, container divergence is measured against this token rather than the corpus modal."),
|
|
4768
|
+
hero_token: z.string().optional().describe("Canonical hero heading class signature (e.g. \"text-display-xl\" or \"64\"). When supplied, hero divergence is measured against this token rather than the corpus modal.")
|
|
4769
|
+
}, async function (params) {
|
|
4770
|
+
if (!params.pages || params.pages.length < 2) {
|
|
4771
|
+
return {
|
|
4772
|
+
content: [{
|
|
4773
|
+
type: "text",
|
|
4774
|
+
text: "Provide at least 2 pages ({name, html}) to compare for cross-page consistency."
|
|
4775
|
+
}]
|
|
4776
|
+
};
|
|
4777
|
+
}
|
|
4778
|
+
var result = auditConsistency(params.pages, {
|
|
4779
|
+
container_token: params.container_token,
|
|
4780
|
+
hero_token: params.hero_token
|
|
4781
|
+
});
|
|
4782
|
+
return {
|
|
4783
|
+
content: [{
|
|
4784
|
+
type: "text",
|
|
4785
|
+
text: JSON.stringify(result, null, 2)
|
|
4786
|
+
}]
|
|
4787
|
+
};
|
|
4788
|
+
});
|
|
4703
4789
|
// ── Reflection ─────────────────────────────────────────────────────
|
|
4704
4790
|
// Summarize the local usage log so the user (or Claude acting on their
|
|
4705
4791
|
// behalf) can see what Raven is being asked to do, and what it's often
|
|
@@ -4812,6 +4898,134 @@ server.tool("raven_register", "Register your email to receive design updates and
|
|
|
4812
4898
|
};
|
|
4813
4899
|
}
|
|
4814
4900
|
});
|
|
4901
|
+
// ── Tool 57: audit_video_playback ─────────────────────────────────
|
|
4902
|
+
server.tool("audit_video_playback", "Render a page in headless Chromium and observe whether each <video> actually advances (samples currentTime before/after a play attempt), classifying every clip into playing|paused|stalled|empty|error with a reason. Catches black/non-playing videos that static audits miss — the most common real-world defect on marketing sites with video backgrounds. Pass url to render + observe, or dom_snapshot to classify pre-collected observations without a browser.", {
|
|
4903
|
+
url: z.string().optional().describe("URL to render and observe (http/https or file://). Requires headless chromium."),
|
|
4904
|
+
dom_snapshot: z.array(z.object({
|
|
4905
|
+
selector: z.string().describe("CSS selector identifying the video element"),
|
|
4906
|
+
hasSource: z.boolean().describe("True if currentSrc is non-empty OR the element has a src attribute or <source> child"),
|
|
4907
|
+
readyState: z.number().describe("HTMLMediaElement.readyState (0..4)"),
|
|
4908
|
+
networkState: z.number().describe("HTMLMediaElement.networkState (0..3; 3=NETWORK_NO_SOURCE)"),
|
|
4909
|
+
errorCode: z.number().describe("MediaError.code (0=none, 1=aborted, 2=network, 3=decode, 4=src-not-supported)"),
|
|
4910
|
+
paused: z.boolean().describe("True if the element is paused"),
|
|
4911
|
+
autoplayBlocked: z.boolean().describe("True if play() was rejected with NotAllowedError"),
|
|
4912
|
+
currentTimeStart: z.number().describe("currentTime recorded before the play attempt"),
|
|
4913
|
+
currentTimeEnd: z.number().describe("currentTime recorded after the observe window")
|
|
4914
|
+
})).optional().describe("Pre-collected video observations to classify without rendering (deterministic path)"),
|
|
4915
|
+
observeMs: z.number().optional().describe("Milliseconds to wait between currentTime samples after play() attempt. Default: 1000")
|
|
4916
|
+
}, async function ({ url, dom_snapshot, observeMs }) {
|
|
4917
|
+
if (url !== undefined && url !== null) {
|
|
4918
|
+
try {
|
|
4919
|
+
var vr = await auditVideoPlaybackUrl(url, { observeMs: observeMs });
|
|
4920
|
+
return { content: [{ type: "text", text: JSON.stringify(vr, null, 2) }] };
|
|
4921
|
+
}
|
|
4922
|
+
catch (error) {
|
|
4923
|
+
if (error instanceof CaptureUnavailableError) {
|
|
4924
|
+
return { content: [{ type: "text", text: "audit_video_playback url mode needs headless chromium. Run: npx playwright install chromium — or pass a dom_snapshot instead." }] };
|
|
4925
|
+
}
|
|
4926
|
+
throw error;
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
if (dom_snapshot !== undefined && dom_snapshot !== null) {
|
|
4930
|
+
// Same aggregator the browser path uses → identical VideoPlaybackResult
|
|
4931
|
+
// shape (url is null since no page was rendered).
|
|
4932
|
+
var snapResult = auditVideoPlaybackSnapshot(dom_snapshot, null);
|
|
4933
|
+
return { content: [{ type: "text", text: JSON.stringify(snapResult, null, 2) }] };
|
|
4934
|
+
}
|
|
4935
|
+
return { content: [{ type: "text", text: "Provide either url (render + observe) or dom_snapshot (classify supplied observations). See tool schema for dom_snapshot field shapes." }] };
|
|
4936
|
+
});
|
|
4937
|
+
// ── Taste Engine: profiles, growth loop, and taste audits ──────────────────
|
|
4938
|
+
server.tool("create_taste_profile", "Create (or overwrite) a named taste profile — a portable design-judgment ruleset + precedent corpus persisted locally under ~/.raven/taste/<name>.json (override dir with RAVEN_TASTE_HOME). Pass explicit rules[] (rule_id, clause_text, category, severity_default block|warn|nit, negative_prompt, owner taste|raven, delegate_to), and/or a DESIGN.md-style markdown doc to ingest (## headings = categories; '- ' bullets = rules; '(block)'/'(warn)'/'(nit)' severity markers; '(raven:<tool>)' delegates a rule to an existing Raven audit tool; 'Do NOT …' sentences become the rule's negative prompt). Ingest RULES-SHAPED docs only (actionable design constraints under category headings) — brand-story/mythology docs produce noise rules, not judgment. Local-first: nothing leaves the machine.", {
|
|
4939
|
+
name: z.string().min(1).describe("Profile name (becomes <name>.json; lowercase alnum/dash/underscore)."),
|
|
4940
|
+
rules: z.array(z.object({
|
|
4941
|
+
rule_id: z.string().describe("Stable unique id, e.g. COLOR-no-gradient."),
|
|
4942
|
+
clause_text: z.string().describe("The rule stated as a positive clause."),
|
|
4943
|
+
category: z.string().describe("color | typography | layout | spacing | voice | tokens | motion | …"),
|
|
4944
|
+
severity_default: z.enum(["block", "warn", "nit"]),
|
|
4945
|
+
negative_prompt: z.string().optional().describe("'Do NOT …' phrasing. A parenthesized comma-list becomes a deterministic banned-word scan ONLY when its sentence is about vocabulary (use/say/write/words/verbs/phrases…), e.g. 'Do NOT use persuasion verbs (proven, shipped, unlock)'. Descriptive example lists ('project facts (counts, scope)') are ignored."),
|
|
4946
|
+
owner: z.enum(["taste", "raven"]).optional().describe("raven = measured by an existing Raven audit tool named in delegate_to."),
|
|
4947
|
+
delegate_to: z.string().optional().describe("Raven tool that owns the measurement (required when owner is raven).")
|
|
4948
|
+
})).optional().describe("Explicit rule objects."),
|
|
4949
|
+
corpus: z.array(z.object({
|
|
4950
|
+
artifact: z.string(),
|
|
4951
|
+
verdict: z.enum(["accept", "revise", "reject"]),
|
|
4952
|
+
violated_rule: z.string(),
|
|
4953
|
+
severity: z.enum(["block", "warn", "nit"]).optional(),
|
|
4954
|
+
wrong: z.string(),
|
|
4955
|
+
right: z.string()
|
|
4956
|
+
})).optional().describe("Seed precedent records."),
|
|
4957
|
+
markdown: z.string().optional().describe("DESIGN.md-style markdown to ingest as rules.")
|
|
4958
|
+
}, async function ({ name, rules, corpus, markdown }) {
|
|
4959
|
+
var profile = createTasteProfile({ name: name, rules: rules, corpus: corpus, markdown: markdown });
|
|
4960
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "create_taste_profile", name: profile.name, rules: profile.rules.length, corpus: profile.corpus.length, home: process.env.RAVEN_TASTE_HOME ? "RAVEN_TASTE_HOME" : "~/.raven/taste" }, null, 2) }] };
|
|
4961
|
+
});
|
|
4962
|
+
server.tool("get_taste_profile", "Load a locally stored taste profile by name — returns its full rule catalog and precedent corpus.", { name: z.string().min(1).describe("Profile name.") }, async function ({ name }) {
|
|
4963
|
+
return { content: [{ type: "text", text: JSON.stringify(getTasteProfile(name), null, 2) }] };
|
|
4964
|
+
});
|
|
4965
|
+
server.tool("list_taste_profiles", "List locally stored taste profiles with rule/corpus counts and last-updated timestamps.", {}, async function () {
|
|
4966
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "list_taste_profiles", profiles: listTasteProfiles() }, null, 2) }] };
|
|
4967
|
+
});
|
|
4968
|
+
server.tool("label_finding", "Append a labeled precedent to a taste profile's corpus — the growth loop. Use when a human accepts/revises/rejects an audit_taste finding or labels a new wrong→right example. Append-only: existing records are never rewritten. accept-verdict precedents suppress matching findings in future audit_taste runs.", {
|
|
4969
|
+
profile: z.string().min(1).describe("Profile name."),
|
|
4970
|
+
artifact: z.string().describe("What was judged (path, URL, or short description)."),
|
|
4971
|
+
verdict: z.enum(["accept", "revise", "reject"]).describe("accept = the flagged pattern is fine (suppresses future matches); revise/reject = confirmed wrong."),
|
|
4972
|
+
violated_rule: z.string().describe("The rule_id the label concerns ('' if none). Must exist in the profile."),
|
|
4973
|
+
severity: z.enum(["block", "warn", "nit"]).optional().describe("Severity the human assigns."),
|
|
4974
|
+
wrong: z.string().describe("The wrong pattern — use a verbatim snippet so accept-suppression can match it."),
|
|
4975
|
+
right: z.string().describe("What right looks like.")
|
|
4976
|
+
}, async function ({ profile, artifact, verdict, violated_rule, severity, wrong, right }) {
|
|
4977
|
+
var res = labelFinding(profile, { artifact: artifact, verdict: verdict, violated_rule: violated_rule, severity: severity || "", wrong: wrong, right: right });
|
|
4978
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "label_finding", profile: res.profile, corpus_count: res.corpus_count, record: res.record }, null, 2) }] };
|
|
4979
|
+
});
|
|
4980
|
+
server.tool("audit_taste", "Judge a target against a taste profile. Pass html (static page/CSS), text (a copy block), or url (rendered headless; also runs delegated WCAG-contrast/tap-target measurements for owner:raven rules). owner:taste rules run deterministic detectors — gradients, glow/neon (large-blur colored shadows), second accent hue, banned-word lists from the rule's negative prompt; clauses with no deterministic detector are reported honestly under not_assessed instead of guessed. owner:raven rules route through Raven's existing audit engines (page checks, contrast, tap targets) and fold results in under the delegating rule_id. Every finding cites an existing rule_id + concrete evidence — the engine prefers silence over a speculative nit. accept-verdict corpus precedents suppress previously-approved patterns. Verdict: BLOCK (any block finding) / WARN (any warn) / PASS.", {
|
|
4981
|
+
profile: z.string().min(1).describe("Taste profile name (see list_taste_profiles)."),
|
|
4982
|
+
html: z.string().optional().describe("Full HTML/CSS of the page to judge."),
|
|
4983
|
+
text: z.string().optional().describe("A copy/text block to judge (voice/banned-word rules)."),
|
|
4984
|
+
url: z.string().optional().describe("Live URL — rendered headless with scroll-settle; enables delegated contrast/tap-target measurement.")
|
|
4985
|
+
}, async function ({ profile, html, text, url }) {
|
|
4986
|
+
if (typeof url === "string" && url.trim() === "")
|
|
4987
|
+
url = undefined;
|
|
4988
|
+
var providedInputs = [html !== undefined, text !== undefined, url !== undefined].filter(Boolean).length;
|
|
4989
|
+
if (providedInputs !== 1) {
|
|
4990
|
+
return { content: [{ type: "text", text: "Provide exactly one of html, text, or url to judge (got " + providedInputs + ")." }] };
|
|
4991
|
+
}
|
|
4992
|
+
var prof = getTasteProfile(profile);
|
|
4993
|
+
var targetHtml = html;
|
|
4994
|
+
var pageIssues = [];
|
|
4995
|
+
var delegates = new Set(prof.rules.filter(function (r) { return r.owner === "raven"; }).map(function (r) { return r.delegate_to; }));
|
|
4996
|
+
if (url) {
|
|
4997
|
+
try {
|
|
4998
|
+
var cap = await capturePage(url, { scroll_settle: true });
|
|
4999
|
+
targetHtml = cap.renderedHtml;
|
|
5000
|
+
if (delegates.has("audit_contrast")) {
|
|
5001
|
+
var c = await auditContrastUrl(url);
|
|
5002
|
+
for (var row of c.aa_failures)
|
|
5003
|
+
pageIssues.push({ rule: "contrast/aa", severity: "error", message: row.selector + " \"" + row.text.slice(0, 40) + "\" contrast " + row.ratio + ":1 < required " + row.required_aa + ":1 (fg " + row.foreground + " on bg " + row.background + ")", fix: "Adjust fg/bg to clear " + row.required_aa + ":1 (delta " + row.delta_to_aa + ")." });
|
|
5004
|
+
}
|
|
5005
|
+
if (delegates.has("audit_tap_targets")) {
|
|
5006
|
+
var t = await auditTapTargetsUrl(url);
|
|
5007
|
+
for (var trow of t.fix_table)
|
|
5008
|
+
pageIssues.push({ rule: "tap-targets/min-size", severity: "error", message: trow.selector + " (" + trow.role + ") " + trow.w + "x" + trow.h + "px below minimum", fix: trow.fix });
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
catch (error) {
|
|
5012
|
+
if (error instanceof CaptureUnavailableError) {
|
|
5013
|
+
return { content: [{ type: "text", text: "audit_taste url mode needs headless chromium. Run: npx playwright install chromium — or pass the page's html instead." }] };
|
|
5014
|
+
}
|
|
5015
|
+
throw error;
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
if (targetHtml && delegates.has("audit_page")) {
|
|
5019
|
+
var pc = runPageChecks(targetHtml);
|
|
5020
|
+
for (var iss of pc.issues)
|
|
5021
|
+
pageIssues.push({ rule: iss.rule, severity: iss.severity, message: iss.message, fix: iss.fix });
|
|
5022
|
+
}
|
|
5023
|
+
var result = auditTaste({ profile: prof, html: targetHtml, text: targetHtml === undefined ? text : undefined, page_issues: pageIssues.length > 0 ? pageIssues : undefined });
|
|
5024
|
+
var out = result;
|
|
5025
|
+
if (url)
|
|
5026
|
+
out = Object.assign({}, result, { target: "url", url: url });
|
|
5027
|
+
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
5028
|
+
});
|
|
4815
5029
|
// ── Start ───────────────────────────────────────────────────────────
|
|
4816
5030
|
async function main() {
|
|
4817
5031
|
var transport = new StdioServerTransport();
|