raven-mcp 1.6.2 → 1.8.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 +87 -2
- package/dist/api-contract.d.ts +36 -0
- package/dist/api-contract.js +235 -0
- package/dist/api-contract.js.map +1 -0
- package/dist/asset-integrity.d.ts +13 -0
- package/dist/asset-integrity.js +104 -0
- package/dist/asset-integrity.js.map +1 -0
- package/dist/capture.d.ts +61 -0
- package/dist/capture.js +620 -0
- package/dist/capture.js.map +1 -0
- package/dist/contract.d.ts +29 -0
- package/dist/contract.js +310 -0
- package/dist/contract.js.map +1 -0
- package/dist/contrast.d.ts +66 -0
- package/dist/contrast.js +283 -0
- package/dist/contrast.js.map +1 -0
- package/dist/image-diff.d.ts +21 -0
- package/dist/image-diff.js +175 -0
- package/dist/image-diff.js.map +1 -0
- package/dist/index.js +1059 -31
- package/dist/index.js.map +1 -1
- package/dist/ios-a11y.d.ts +43 -0
- package/dist/ios-a11y.js +222 -0
- package/dist/ios-a11y.js.map +1 -0
- package/dist/ios-capture.d.ts +66 -0
- package/dist/ios-capture.js +415 -0
- package/dist/ios-capture.js.map +1 -0
- package/dist/parity.d.ts +38 -0
- package/dist/parity.js +235 -0
- package/dist/parity.js.map +1 -0
- package/dist/responsive.d.ts +32 -0
- package/dist/responsive.js +233 -0
- package/dist/responsive.js.map +1 -0
- package/package.json +12 -3
- package/scripts/AccessibilitySnapshot.swift +90 -0
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,36 @@
|
|
|
1
|
+
export type ShapeSchema = {
|
|
2
|
+
required?: string[];
|
|
3
|
+
types?: Record<string, "string" | "number" | "boolean" | "object" | "array">;
|
|
4
|
+
};
|
|
5
|
+
export type Query = {
|
|
6
|
+
name: string;
|
|
7
|
+
method?: "GET" | "POST";
|
|
8
|
+
path?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
expect?: {
|
|
12
|
+
contains?: string[];
|
|
13
|
+
equals?: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type QueryVerdict = {
|
|
17
|
+
name: string;
|
|
18
|
+
verdict: "shape-valid" | "shape-invalid" | "confident-wrong" | "uncertain";
|
|
19
|
+
reason: string;
|
|
20
|
+
offending?: string;
|
|
21
|
+
status?: number;
|
|
22
|
+
};
|
|
23
|
+
export type ApiContractResult = {
|
|
24
|
+
results: QueryVerdict[];
|
|
25
|
+
shape_valid_count: number;
|
|
26
|
+
confident_wrong_count: number;
|
|
27
|
+
shape_invalid_count: number;
|
|
28
|
+
uncertain_count: number;
|
|
29
|
+
};
|
|
30
|
+
export declare function getPath(obj: unknown, path: string): unknown;
|
|
31
|
+
export declare function validateShape(response: unknown, schema: ShapeSchema): {
|
|
32
|
+
valid: boolean;
|
|
33
|
+
offending?: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function runApiContract(endpointUrl: string, queries: Query[], schema: ShapeSchema): Promise<ApiContractResult>;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
export function getPath(obj, path) {
|
|
2
|
+
const segments = path.split(".");
|
|
3
|
+
let current = obj;
|
|
4
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
5
|
+
const segment = segments[index];
|
|
6
|
+
if (Array.isArray(current)) {
|
|
7
|
+
if (!isArrayIndex(segment)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const arrayIndex = Number(segment);
|
|
11
|
+
if (arrayIndex >= current.length) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
current = current[arrayIndex];
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!isRecord(current)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
current = current[segment];
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
export function validateShape(response, schema) {
|
|
28
|
+
const required = schema.required || [];
|
|
29
|
+
for (let index = 0; index < required.length; index += 1) {
|
|
30
|
+
const path = required[index];
|
|
31
|
+
if (getPath(response, path) === undefined) {
|
|
32
|
+
return {
|
|
33
|
+
valid: false,
|
|
34
|
+
offending: path,
|
|
35
|
+
reason: "missing required path: " + path
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const types = schema.types || {};
|
|
40
|
+
const entries = Object.entries(types);
|
|
41
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
42
|
+
const path = entries[index][0];
|
|
43
|
+
const expected = entries[index][1];
|
|
44
|
+
const value = getPath(response, path);
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!matchesType(value, expected)) {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
offending: path,
|
|
52
|
+
reason: "type mismatch at " + path + ": expected " + expected
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { valid: true, reason: "all checks passed" };
|
|
57
|
+
}
|
|
58
|
+
export async function runApiContract(endpointUrl, queries, schema) {
|
|
59
|
+
const results = [];
|
|
60
|
+
for (let index = 0; index < queries.length; index += 1) {
|
|
61
|
+
const query = queries[index];
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeout = setTimeout(function abortRequest() {
|
|
64
|
+
controller.abort();
|
|
65
|
+
}, 10000);
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await fetch(endpointUrl + (query.path || ""), {
|
|
69
|
+
method: query.method || "GET",
|
|
70
|
+
headers: requestHeaders(query),
|
|
71
|
+
body: query.method === "POST" ? JSON.stringify(query.body) : undefined,
|
|
72
|
+
signal: controller.signal
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
results.push({
|
|
77
|
+
name: query.name,
|
|
78
|
+
verdict: "uncertain",
|
|
79
|
+
reason: errorMessage(error)
|
|
80
|
+
});
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = await response.json();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
results.push({
|
|
91
|
+
name: query.name,
|
|
92
|
+
verdict: "uncertain",
|
|
93
|
+
reason: "non-JSON response",
|
|
94
|
+
status: response.status
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (response.status < 200 || response.status >= 300) {
|
|
99
|
+
results.push({
|
|
100
|
+
name: query.name,
|
|
101
|
+
verdict: "uncertain",
|
|
102
|
+
reason: "HTTP " + response.status,
|
|
103
|
+
status: response.status
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const shape = validateShape(parsed, schema);
|
|
108
|
+
if (!shape.valid) {
|
|
109
|
+
results.push({
|
|
110
|
+
name: query.name,
|
|
111
|
+
verdict: "shape-invalid",
|
|
112
|
+
reason: shape.reason,
|
|
113
|
+
offending: shape.offending,
|
|
114
|
+
status: response.status
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const expected = query.expect;
|
|
119
|
+
const confidentWrong = expected ? checkExpectations(parsed, expected) : null;
|
|
120
|
+
if (confidentWrong) {
|
|
121
|
+
results.push({
|
|
122
|
+
name: query.name,
|
|
123
|
+
verdict: "confident-wrong",
|
|
124
|
+
reason: confidentWrong.reason,
|
|
125
|
+
offending: confidentWrong.offending,
|
|
126
|
+
status: response.status
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
results.push({
|
|
131
|
+
name: query.name,
|
|
132
|
+
verdict: "shape-valid",
|
|
133
|
+
reason: "all checks passed",
|
|
134
|
+
status: response.status
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return tallyResults(results);
|
|
138
|
+
}
|
|
139
|
+
function isArrayIndex(segment) {
|
|
140
|
+
return /^\d+$/.test(segment);
|
|
141
|
+
}
|
|
142
|
+
function isRecord(value) {
|
|
143
|
+
return typeof value === "object" && value !== null;
|
|
144
|
+
}
|
|
145
|
+
function matchesType(value, expected) {
|
|
146
|
+
if (expected === "array") {
|
|
147
|
+
return Array.isArray(value);
|
|
148
|
+
}
|
|
149
|
+
if (expected === "object") {
|
|
150
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
151
|
+
}
|
|
152
|
+
return typeof value === expected;
|
|
153
|
+
}
|
|
154
|
+
function requestHeaders(query) {
|
|
155
|
+
if (!query.headers && query.method !== "POST") {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const headers = { ...(query.headers || {}) };
|
|
159
|
+
if (query.method === "POST" && !hasHeader(headers, "content-type")) {
|
|
160
|
+
headers["Content-Type"] = "application/json";
|
|
161
|
+
}
|
|
162
|
+
return headers;
|
|
163
|
+
}
|
|
164
|
+
function hasHeader(headers, name) {
|
|
165
|
+
const names = Object.keys(headers);
|
|
166
|
+
const target = name.toLowerCase();
|
|
167
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
168
|
+
if (names[index].toLowerCase() === target) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
function checkExpectations(parsed, expected) {
|
|
175
|
+
const serialized = JSON.stringify(parsed);
|
|
176
|
+
const contains = expected.contains || [];
|
|
177
|
+
for (let index = 0; index < contains.length; index += 1) {
|
|
178
|
+
const expectedString = contains[index];
|
|
179
|
+
if (!serialized.includes(expectedString)) {
|
|
180
|
+
return {
|
|
181
|
+
reason: "expected string not found: " + expectedString,
|
|
182
|
+
offending: expectedString
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const equals = expected.equals || {};
|
|
187
|
+
const entries = Object.entries(equals);
|
|
188
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
189
|
+
const path = entries[index][0];
|
|
190
|
+
const expectedValue = entries[index][1];
|
|
191
|
+
const actualValue = getPath(parsed, path);
|
|
192
|
+
if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {
|
|
193
|
+
return {
|
|
194
|
+
reason: "value mismatch at " + path,
|
|
195
|
+
offending: path
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function tallyResults(results) {
|
|
202
|
+
let shapeValidCount = 0;
|
|
203
|
+
let confidentWrongCount = 0;
|
|
204
|
+
let shapeInvalidCount = 0;
|
|
205
|
+
let uncertainCount = 0;
|
|
206
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
207
|
+
const result = results[index];
|
|
208
|
+
if (result.verdict === "shape-valid") {
|
|
209
|
+
shapeValidCount += 1;
|
|
210
|
+
}
|
|
211
|
+
else if (result.verdict === "confident-wrong") {
|
|
212
|
+
confidentWrongCount += 1;
|
|
213
|
+
}
|
|
214
|
+
else if (result.verdict === "shape-invalid") {
|
|
215
|
+
shapeInvalidCount += 1;
|
|
216
|
+
}
|
|
217
|
+
else if (result.verdict === "uncertain") {
|
|
218
|
+
uncertainCount += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
results,
|
|
223
|
+
shape_valid_count: shapeValidCount,
|
|
224
|
+
confident_wrong_count: confidentWrongCount,
|
|
225
|
+
shape_invalid_count: shapeInvalidCount,
|
|
226
|
+
uncertain_count: uncertainCount
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function errorMessage(error) {
|
|
230
|
+
if (error instanceof Error) {
|
|
231
|
+
return error.message;
|
|
232
|
+
}
|
|
233
|
+
return String(error);
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=api-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-contract.js","sourceRoot":"","sources":["../src/api-contract.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,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[]>;
|