raven-mcp 1.6.1 → 1.7.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 CHANGED
@@ -18,6 +18,7 @@ Raven gives Claude access to a comprehensive design knowledge base:
18
18
  - **Brand & visual** — Logo usage (clear space, min sizes, variants, placement, restraint), gradient usage (hierarchy, palette, contrast, trend vs signature), imagery (consistency, representation, purpose), visual hierarchy, brand-as-system, and current (2026) visual-design trends
19
19
  - **Business** — Monetization models, retention strategies, onboarding optimization, growth mechanics, and product metrics frameworks
20
20
  - **Tokens** — Design system tokens for Stripe, Linear, and more
21
+ - **Creative studio** — Local-first brand profiles, asset references, character reference profiles, provider-agnostic image/video/3D/audio generation jobs, campaign plans, and transparent creative scoring. Raven does not ship media-provider credentials; set `RAVEN_CREATIVE_RUNNER` to route jobs to your own renderer.
21
22
 
22
23
  ## Install
23
24
 
@@ -54,7 +55,7 @@ cd raven-mcp && npm install && npm run build
54
55
  | `get_principles` | Get design principles relevant to a UI context |
55
56
  | `get_pattern` | Get proven patterns for a specific UI type |
56
57
  | `get_business_strategy` | Get business/monetization strategies |
57
- | `evaluate_design` | Evaluate a design description against principles |
58
+ | `evaluate_design` | Evaluate a design description against principles. Pass base64 PNG screenshots (`before_screenshot`/`after_screenshot`) for a structured before/after pixel diff with `fix_confirmed`, `changed_ratio`, and changed region. |
58
59
  | `search_knowledge` | Search across all principles, patterns, and strategies |
59
60
  | `get_checklist` | Get a pre-publish checklist for a UI type |
60
61
  | `get_d4d_framework` | Get Design for Delight framework templates |
@@ -62,8 +63,10 @@ cd raven-mcp && npm install && npm run build
62
63
  | `get_design_system` | Get tokens for a specific design system |
63
64
  | `compose_system` | Mix tokens from different systems |
64
65
  | `get_brand_system` | Get a full system styled like a well-known brand |
65
- | `audit_page` | Audit HTML/CSS against Raven's quality standards |
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 |
66
67
  | `audit_layout` | Evaluate visual rhythm, alignment, and optical balance |
68
+ | `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
+ | `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 |
67
70
  | `audit_swiftui` | Audit SwiftUI source against Apple HIG — Dynamic Type, semantic colors, 44pt targets, 4/8pt spacing, AccentColor |
68
71
  | `audit_ios_screen` | Score a rendered iOS screen from an accessibility/view-hierarchy snapshot — 44pt targets + contrast + rhythm, in points |
69
72
  | `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 |
@@ -80,8 +83,33 @@ cd raven-mcp && npm install && npm run build
80
83
  | `generate_service_blueprint` | Render a service blueprint as HTML — current state, or current vs. ideal side-by-side |
81
84
  | `get_brand_principles` | Get brand/visual principles — logo, gradient, imagery, hierarchy, brand-as-system |
82
85
  | `get_brand_trends` | Get current (2026) brand and visual-design trends with usage guidance |
86
+ | `list_creative_models` | Browse provider-agnostic creative model slots for image, video, 3D, audio, character consistency, and analysis |
87
+ | `list_creative_presets` | Browse creative presets: product photoshoot, marketplace cards, UGC ads, TV spots, social packs, storyboards, infographics |
88
+ | `create_brand_profile` | Create or update a local brand profile for brand-aware creative jobs |
89
+ | `get_brand_profile` | Read a local creative brand profile |
90
+ | `list_brand_profiles` | List local creative brand profiles |
91
+ | `register_creative_asset` | Register a local path or URL as a creative asset reference — no file bytes are uploaded by Raven |
92
+ | `create_character_profile` | Create a local character/identity reference profile from registered assets |
93
+ | `create_generation_job` | Create a provider-agnostic image, video, audio, 3D, campaign, or analysis job payload; optionally execute via `RAVEN_CREATIVE_RUNNER` |
94
+ | `get_generation_job` | Read a creative generation job and its provider payload/output state |
95
+ | `list_generation_jobs` | List local creative generation jobs |
96
+ | `plan_creative_campaign` | Plan a multi-asset campaign and optionally create draft generation jobs |
97
+ | `score_creative` | Score a prompt/script/concept for hook, benefit clarity, product signal, CTA, channel fit, audience fit, and brand fit |
83
98
  | `raven_reflect` | Summarize your local Raven usage log to find patterns + gaps |
84
99
 
100
+ ## Creative studio
101
+
102
+ Raven now covers the creative-production workflow around media generation without copying or depending on any closed vendor. The tools are orchestration primitives:
103
+
104
+ - Store brand kits locally with `create_brand_profile`.
105
+ - Register product photos, logos, references, or URLs with `register_creative_asset`.
106
+ - Create character/identity reference sets with `create_character_profile`.
107
+ - Generate provider-ready payloads with `create_generation_job`.
108
+ - Build full campaign shot lists with `plan_creative_campaign`.
109
+ - Score creative concepts with `score_creative`.
110
+
111
+ By default, jobs are saved as local draft payloads under `~/.raven/creative` (override with `RAVEN_CREATIVE_HOME`). To run real media generation, set `RAVEN_CREATIVE_RUNNER` to an executable that reads one job JSON object from stdin and returns JSON on stdout. That runner can call any provider you choose; Raven never stores API keys in source.
112
+
85
113
  ## iOS / SwiftUI audits
86
114
 
87
115
  Raven audits native iOS apps against the **Apple Human Interface Guidelines**, not web/CSS conventions. None of the web-only rules (`lang`, `title`, `flex-wrap`, `clamp`, `max-width`, CSS custom properties, bare hex) run on iOS input — and `get_checklist`/`get_principles` take `platform: "ios"` to return HIG items (Dynamic Type, 44pt targets, SF Symbols, safe areas, dark-mode parity, App Review privacy) instead of the web set.
@@ -103,6 +131,63 @@ Anyone building a React Native or Expo app gets the same treatment. RN renders t
103
131
 
104
132
  **One command:** `node scripts/rn-audit.mjs <app-dir> [--snapshot snap.json] [--md report.md]` discovers screens + `app.json` (reading `userInterfaceStyle` so dark-only apps aren't false-flagged) and runs everything.
105
133
 
134
+ ## Responsive visibility audits
135
+
136
+ `audit_responsive_visibility` renders a page at multiple breakpoints (default: 390px mobile, 768px tablet, 1440px desktop, 2160px ultra-wide) and flags content elements that are visible on desktop but hidden on mobile — catching the "vanishes on mobile" bug class. Each flagged element is categorised as **likely-oversight** (content that shouldn't be hidden) or **intentional** (decorative elements). Detects hiding via CSS (`hidden`, `display:none`, `opacity:0`, `visibility:hidden`) and responsive Tailwind classes (`hidden md:block`, etc.).
137
+
138
+ **Usage:**
139
+ - `audit_responsive_visibility(url)` — render at default breakpoints and flag mismatches.
140
+ - `audit_responsive_visibility(url, [390, 768, 1440])` — custom breakpoints.
141
+ - Optional `viewportHeight` (default: 900px) for tall content.
142
+
143
+ Returns flagged elements with selector, hiding class, visibility at each breakpoint, and category.
144
+
145
+ ## Contrast audits
146
+
147
+ `audit_contrast` computes WCAG contrast ratios for every text element on a rendered page, reporting AA (4.5:1 normal text, 3:1 large) and AAA (7:1 normal, 4.5:1 large) pass-fail. Useful for catching small-text / low-contrast pairs that a screenshot eyedropper would catch manually — Raven replaces the math.
148
+
149
+ **Usage:**
150
+ - `audit_contrast(url)` — render a live page and audit all text.
151
+ - `audit_contrast(dom_snapshot: [{ selector, color, bgColor, fontPx?, bold?, text? }])` — audit a pre-captured snapshot (useful for dynamic or cookie-protected pages).
152
+
153
+ Returns all text elements scored, failures highlighted with delta-to-pass, and a summary of AA/AAA failure count.
154
+
155
+ **WCAG math:** Contrast ratio uses linearised luminance (WCAG 2.1 § 1.4.3) — black-on-white is exactly 21, white-on-black is exactly 21. Large text (18.66pt+ bold or 24pt+) needs only 3:1 / 4.5:1 AAA; regular text needs 4.5:1 / 7:1.
156
+
157
+ ## Headless browser audits
158
+
159
+ `audit_page` can render a live URL in headless Chromium, scroll to settle reveal-on-scroll elements, and play preload=none videos before capturing — preventing false "blank section" reports caused by whileInView states that haven't fired yet.
160
+
161
+ **Usage:**
162
+ - **Static HTML mode** — pass `html` string for immediate static analysis (existing behavior, no change).
163
+ - **Rendered URL mode** — pass `url` (full HTTP/HTTPS URL). Raven launches Chromium, renders the page, optionally scrolls, and audits the live DOM.
164
+ - `scroll_settle: true` — scroll from top to bottom in viewport-height steps, then wait 300ms for `IntersectionObserver` / whileInView reveals to fire. Unloaded videos (preload="none") are played to detect if they render blank. This surfaces the real rendered state and avoids false positives on reveal-on-scroll or lazy-loaded content.
165
+ - `viewport: { w, h }` — set the render viewport (default: `{ w: 1440, h: 900 }`).
166
+
167
+ **Video artifacts detection:** If any `<video>` with `preload="none"` (or missing preload) renders with `readyState < 2` (i.e. would show a black box in a screenshot), Raven flags it as an `unloaded-video-artifact` in the result. This is **informational** — not a pass/fail — since preload=none is often intentional. On cookie-protected hosts, video requests may fail because iOS/Android media daemons don't send cookies; Raven notes this to help you troubleshoot (e.g. disable deployment protection, use a token-based bypass).
168
+
169
+ **Adversarial verification:** Set `adversarial_verify: true` to independently re-check each finding against the live DOM using a different method. Findings are tagged:
170
+ - `confirmed` — the finding is real on the live page (e.g. missing `<title>` in the rendered DOM)
171
+ - `likely-artifact` — the finding is an artifact of the static audit method (e.g. a `<video preload="none">` rendered blank, which is expected behavior, not a missing resource)
172
+ - `inconclusive` — the finding cannot be independently verified (e.g. aggregate rules like color-palette size)
173
+
174
+ The result includes `adversarial_verification: { debunked_count, confirmed_count, inconclusive_count }`, where debunked_count is the number of likely-artifacts. This surfaces false positives so you only fix real issues. Backwards-compatible: when `adversarial_verify` is absent or false, the output is identical to prior versions.
175
+
176
+ **Setup:** First time only, run `npx playwright install chromium` to download the browser binary. If the binary is missing when you call audit_page with `url`, you'll see a clear instruction to run the install command.
177
+
178
+ ## Before/after design diffs
179
+
180
+ `evaluate_design` can now accept base64-encoded PNG screenshots to measure whether a fix actually changed the rendered output.
181
+
182
+ **Usage:**
183
+ - Pass `before_screenshot` and `after_screenshot` (both base64 PNGs, with or without the `data:image/png;base64,` prefix).
184
+ - Raven returns `fix_confirmed: true` if the images differ by > 0.1% of pixels (accounting for jpeg/PNG decode variance).
185
+ - `changed_ratio` — exact fraction of pixels that changed (0–1).
186
+ - `changed_region` — bounding box `{ x, y, w, h }` of the changed pixels (null if no changes detected).
187
+ - `dimensions` — image-derived measurements (canvas size, brightness, color shift) as context, with the caveat that these are pixel-level proxies, not Raven principle scores.
188
+
189
+ When before/after screenshots are provided alongside a `description`, `evaluate_design` returns both the principle-based evaluation and the pixel diff. When screenshots are provided without a description, the evaluation gracefully skips the principle search and returns the diff only. Backwards-compatible: without screenshots, the tool behaves identically to prior versions.
190
+
106
191
  ## Release updates
107
192
 
108
193
  Raven ships new principles, patterns, and brand systems regularly. For one email per minor/major release (patches stay quiet):
@@ -0,0 +1,13 @@
1
+ export type AssetIntegrityResult = {
2
+ path: string;
3
+ bottom_variance: number;
4
+ verdict: "clean" | "likely-sliced";
5
+ confidence: number;
6
+ warnings: string[];
7
+ };
8
+ export type AssetIntegrityOptions = {
9
+ bottomFraction?: number;
10
+ minRows?: number;
11
+ varianceThreshold?: number;
12
+ };
13
+ export declare function auditAssetIntegrity(imagePaths: string[], opts?: AssetIntegrityOptions): Promise<AssetIntegrityResult[]>;
@@ -0,0 +1,104 @@
1
+ import { readFileSync } from "node:fs";
2
+ const DATA_URL_PREFIX = /^data:image\/png;base64,/i;
3
+ export async function auditAssetIntegrity(imagePaths, opts = {}) {
4
+ // @ts-ignore pngjs is intentionally optional; absence falls back to clean-with-warning.
5
+ const pngMod = (await import("pngjs").catch(() => null));
6
+ const PNG = pngMod?.PNG;
7
+ const readPng = PNG?.sync?.read;
8
+ return imagePaths.map(function (imagePath) {
9
+ let buffer;
10
+ try {
11
+ buffer = readFileSync(imagePath);
12
+ }
13
+ catch (error) {
14
+ return fallbackResult(imagePath, [
15
+ "Could not read file: " + errorMessage(error)
16
+ ]);
17
+ }
18
+ if (!readPng) {
19
+ return fallbackResult(imagePath, [
20
+ "pngjs not installed — cannot analyze asset integrity"
21
+ ]);
22
+ }
23
+ try {
24
+ return auditPng(imagePath, readPng(buffer), opts);
25
+ }
26
+ catch (error) {
27
+ return fallbackResult(imagePath, [
28
+ "Failed to decode PNG: " + errorMessage(error)
29
+ ]);
30
+ }
31
+ });
32
+ }
33
+ function stripPngPrefix(base64) {
34
+ return base64.replace(DATA_URL_PREFIX, "");
35
+ }
36
+ function fallbackResult(path, warnings) {
37
+ return {
38
+ path,
39
+ bottom_variance: 0,
40
+ verdict: "clean",
41
+ confidence: 0,
42
+ warnings
43
+ };
44
+ }
45
+ function auditPng(path, png, opts) {
46
+ const threshold = opts.varianceThreshold ?? 100;
47
+ const bottomFraction = opts.bottomFraction ?? 0.05;
48
+ const minRows = opts.minRows ?? 20;
49
+ const stripHeight = Math.min(png.height, Math.max(minRows, Math.round(png.height * bottomFraction)));
50
+ const bottomVariance = bottomLuminanceVariance(png, stripHeight);
51
+ const verdict = bottomVariance > threshold ? "likely-sliced" : "clean";
52
+ // Confidence is monotonic around the variance threshold: clean approaches 1 as
53
+ // variance approaches 0; likely-sliced starts at 0.5 and approaches 1 by 4x threshold.
54
+ const confidence = verdict === "clean"
55
+ ? clamp(1 - bottomVariance / threshold, 0, 1)
56
+ : Math.max(0.5, clamp(bottomVariance / (threshold * 4), 0, 1));
57
+ return {
58
+ path,
59
+ bottom_variance: bottomVariance,
60
+ verdict,
61
+ confidence,
62
+ warnings: []
63
+ };
64
+ }
65
+ function bottomLuminanceVariance(png, stripHeight) {
66
+ const startY = png.height - stripHeight;
67
+ const pixelCount = png.width * stripHeight;
68
+ if (pixelCount <= 0) {
69
+ return 0;
70
+ }
71
+ let lumaTotal = 0;
72
+ const luminances = [];
73
+ for (let y = startY; y < png.height; y += 1) {
74
+ for (let x = 0; x < png.width; x += 1) {
75
+ const index = (y * png.width + x) * 4;
76
+ const r = png.data[index];
77
+ const g = png.data[index + 1];
78
+ const b = png.data[index + 2];
79
+ const luminance = pixelLuminance(r, g, b);
80
+ luminances.push(luminance);
81
+ lumaTotal += luminance;
82
+ }
83
+ }
84
+ const mean = lumaTotal / pixelCount;
85
+ let squaredDeltaTotal = 0;
86
+ for (let i = 0; i < luminances.length; i += 1) {
87
+ const delta = luminances[i] - mean;
88
+ squaredDeltaTotal += delta * delta;
89
+ }
90
+ return squaredDeltaTotal / pixelCount;
91
+ }
92
+ function pixelLuminance(r, g, b) {
93
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
94
+ }
95
+ function clamp(value, min, max) {
96
+ return Math.min(max, Math.max(min, value));
97
+ }
98
+ function errorMessage(error) {
99
+ if (error instanceof Error) {
100
+ return error.message;
101
+ }
102
+ return String(error);
103
+ }
104
+ //# sourceMappingURL=asset-integrity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-integrity.js","sourceRoot":"","sources":["../src/asset-integrity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type AuditIssue = {
2
+ severity: "error" | "warning";
3
+ rule: string;
4
+ message: string;
5
+ fix: string;
6
+ };
7
+ export type ContainerAuditResult = {
8
+ pass?: string;
9
+ issue?: AuditIssue;
10
+ };
11
+ export declare function extractDeclaredMaxWidths(html: string): number[];
12
+ export declare function containerScaleWidths(html: string): number[];
13
+ export declare function auditContainerWidth(html: string, expectedPx?: number): ContainerAuditResult;
@@ -0,0 +1,129 @@
1
+ // ── Container-width auditing ────────────────────────────────────────
2
+ //
3
+ // The `responsive/max-width` check used to hardcode ~1200px: it passed if the
4
+ // HTML contained `max-width: 11xx/12xx px` and warned otherwise. That has two
5
+ // blind spots, both reported in issue #9:
6
+ //
7
+ // 1. It rewards *any* ~1200px max-width and is blind to a project's own
8
+ // canonical container token. A page throttled to 768px while the rest of
9
+ // the design system sits on, say, 1152px is not flagged — the divergence
10
+ // that actually breaks cross-page alignment is invisible.
11
+ // 2. `responsive/max-width` is the single most-fired warning in practice, yet
12
+ // it only ever nudges *toward* adding a max-width, never toward matching
13
+ // the system.
14
+ //
15
+ // When the caller passes `containerMaxWidth` (their canonical container token),
16
+ // this module reframes the rule to flag *divergence from that token* — too
17
+ // narrow OR too wide — instead of a generic 1200px heuristic. With no token
18
+ // passed, the original heuristic is preserved exactly, so existing callers are
19
+ // unaffected.
20
+ //
21
+ // Limitation (shared with the rest of audit_page): this reads *declared* CSS
22
+ // (`max-width: Npx` in <style>/inline). Utility-class-only markup (e.g. a
23
+ // Tailwind `max-w-3xl` with no compiled CSS in the blob) carries no declared
24
+ // max-width to read; pass the compiled stylesheet for those.
25
+ // Pull every *declared* `max-width: Npx` out of the markup. Media-query
26
+ // preludes (`@media (max-width: 768px) {`) are stripped first so a responsive
27
+ // breakpoint condition is never mistaken for a container constraint.
28
+ export function extractDeclaredMaxWidths(html) {
29
+ var stripped = html.replace(/@media[^{]*\{/g, "{");
30
+ var out = [];
31
+ var re = /max-width\s*:\s*(\d+(?:\.\d+)?)\s*px/g;
32
+ var m;
33
+ while ((m = re.exec(stripped)) !== null) {
34
+ var n = parseFloat(m[1]);
35
+ if (!isNaN(n) && n > 0)
36
+ out.push(n);
37
+ }
38
+ return out;
39
+ }
40
+ // Container-scale widths only. Floor at 700px so legitimate nested prose/
41
+ // reading measures (Tailwind max-w-2xl ≈ 672, max-w-prose ≈ 65ch, etc.) — which
42
+ // are correctly narrower than the page container by design — aren't mistaken
43
+ // for an off-system container throttle. The defect this catches is a *content
44
+ // container* (≥700px tier) that diverges from the system token.
45
+ var CONTAINER_FLOOR_PX = 700;
46
+ export function containerScaleWidths(html) {
47
+ return extractDeclaredMaxWidths(html).filter(function (w) {
48
+ return w >= CONTAINER_FLOOR_PX;
49
+ });
50
+ }
51
+ // Token tolerance: 5% of the token, floored at 16px, so 1152px ± ~58px still
52
+ // reads as "on token" but 768 vs 1152 does not.
53
+ function tokenTolerance(expectedPx) {
54
+ return Math.max(16, expectedPx * 0.05);
55
+ }
56
+ // Audit a page's content-container width. Token-aware when `expectedPx` is
57
+ // given; otherwise falls back to the original 1200px heuristic verbatim.
58
+ export function auditContainerWidth(html, expectedPx) {
59
+ if (typeof expectedPx === "number" && expectedPx > 0) {
60
+ var containers = containerScaleWidths(html);
61
+ var tol = tokenTolerance(expectedPx);
62
+ // Worst offender first: any container-scale width outside tolerance is a
63
+ // divergence, even when the token width ALSO appears elsewhere in the
64
+ // markup (e.g. a shared stylesheet defines the token, but the page itself
65
+ // overrides to an off-system width — exactly the issue-#9 case).
66
+ var divergent = containers
67
+ .filter(function (w) {
68
+ return Math.abs(w - expectedPx) > tol;
69
+ })
70
+ .sort(function (a, b) {
71
+ return Math.abs(b - expectedPx) - Math.abs(a - expectedPx);
72
+ });
73
+ if (divergent.length > 0) {
74
+ var worst = divergent[0];
75
+ var dir = worst < expectedPx ? "narrower" : "wider";
76
+ var deltaPx = Math.round(Math.abs(worst - expectedPx));
77
+ return {
78
+ issue: {
79
+ severity: "warning",
80
+ rule: "responsive/max-width",
81
+ message: "Content container max-width " +
82
+ worst +
83
+ "px diverges from your " +
84
+ expectedPx +
85
+ "px container token (" +
86
+ dir +
87
+ " by " +
88
+ deltaPx +
89
+ "px). Off-system container widths break cross-page alignment.",
90
+ fix: "Use the project's canonical container token (" +
91
+ expectedPx +
92
+ "px) for top-level content. If the content should read narrower, nest it inside the " +
93
+ expectedPx +
94
+ "px container rather than replacing the container's max-width."
95
+ }
96
+ };
97
+ }
98
+ var onToken = containers.filter(function (w) {
99
+ return Math.abs(w - expectedPx) <= tol;
100
+ });
101
+ if (onToken.length > 0) {
102
+ return { pass: "Content container matches the " + expectedPx + "px container token" };
103
+ }
104
+ return {
105
+ issue: {
106
+ severity: "warning",
107
+ rule: "responsive/max-width",
108
+ message: "No content container max-width detected — expected your " +
109
+ expectedPx +
110
+ "px container token.",
111
+ fix: "Add max-width: " + expectedPx + "px; margin: 0 auto to top-level content containers."
112
+ }
113
+ };
114
+ }
115
+ // ── No token passed: preserve the original heuristic exactly.
116
+ var hasMaxWidth = /max-width\s*:\s*(1[12]\d{2}|1200)\s*px/.test(html);
117
+ if (hasMaxWidth)
118
+ return { pass: "Content has max-width constraint" };
119
+ return {
120
+ issue: {
121
+ severity: "warning",
122
+ rule: "responsive/max-width",
123
+ message: "No 1200px max-width constraint detected on content containers",
124
+ fix: "Add max-width: 1200px; margin: 0 auto to content containers. " +
125
+ "Pass containerMaxWidth to audit_page to check against your design system's own container token instead."
126
+ }
127
+ };
128
+ }
129
+ //# sourceMappingURL=audit-container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-container.js","sourceRoot":"","sources":["../src/audit-container.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ export declare class CaptureUnavailableError extends Error {
2
+ constructor(message?: string);
3
+ }
4
+ export type VideoArtifact = {
5
+ selector: string;
6
+ preload: string;
7
+ renderedBlank: boolean;
8
+ reason: "unloaded-video-artifact";
9
+ };
10
+ export type CaptureResult = {
11
+ url: string;
12
+ renderedHtml: string;
13
+ screenshotBase64: string;
14
+ viewport: {
15
+ w: number;
16
+ h: number;
17
+ };
18
+ scrolledToBottom: boolean;
19
+ videoArtifacts: VideoArtifact[];
20
+ warnings: string[];
21
+ };
22
+ export type Interaction = {
23
+ selector: string;
24
+ event: "hover" | "click" | "focus";
25
+ delay_ms: number;
26
+ };
27
+ export type CaptureOptions = {
28
+ interactions?: Interaction[];
29
+ scroll_settle?: boolean;
30
+ viewport?: {
31
+ w: number;
32
+ h: number;
33
+ };
34
+ timeoutMs?: number;
35
+ };
36
+ export declare function capturePage(url: string, opts?: CaptureOptions): Promise<CaptureResult>;
37
+ export declare function annotateVideoArtifacts(elements: any[]): VideoArtifact[];
38
+ export type Verdict = "confirmed" | "likely-artifact" | "inconclusive";
39
+ export type VerifiableFinding = {
40
+ key: string;
41
+ rule: string;
42
+ message: string;
43
+ kind: "issue" | "video-artifact";
44
+ selector?: string;
45
+ };
46
+ export type FindingVerdict = {
47
+ key: string;
48
+ verdict: Verdict;
49
+ evidence: string;
50
+ };
51
+ export type VerifyTarget = {
52
+ url?: string;
53
+ html?: string;
54
+ };
55
+ export declare function verifyFindings(target: VerifyTarget, findings: VerifiableFinding[], opts?: {
56
+ viewport?: {
57
+ w: number;
58
+ h: number;
59
+ };
60
+ timeoutMs?: number;
61
+ }): Promise<FindingVerdict[]>;