@rolepod/uiproof 0.5.0 → 0.6.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +2 -2
- package/CHANGELOG.md +69 -0
- package/README.md +15 -0
- package/dist/bin/rolepod-uiproof.js +243 -26
- package/dist/bin/rolepod-uiproof.js.map +1 -1
- package/dist/index.d.ts +46 -9
- package/dist/index.js +243 -24
- package/dist/index.js.map +1 -1
- package/dist/schemas/tools.json +1 -1
- package/package.json +1 -1
- package/skills/audit-a11y/SKILL.md +9 -0
- package/skills/check-errors/SKILL.md +9 -0
- package/skills/scaffold-e2e/SKILL.md +9 -0
- package/skills/verify-ui/SKILL.md +9 -0
- package/skills/visual-diff/SKILL.md +9 -0
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,18 @@ import { z } from 'zod';
|
|
|
3
3
|
import { Page } from 'playwright';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Writes artifacts
|
|
6
|
+
* Writes run artifacts. In **standalone** mode (default) — and in v0.4 and
|
|
7
|
+
* earlier — runs live under `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* In **with-parent** mode — activated automatically when the env var
|
|
10
|
+
* `ROLEPOD_PARENT=1` is set by the parent `rolepod` plugin's SessionStart
|
|
11
|
+
* hook — runs live under `./.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`,
|
|
12
|
+
* per the Extension Protocol v1 evidence-path convention. Parent's
|
|
13
|
+
* `check-work` skill aggregates manifest.json files from this directory.
|
|
14
|
+
*
|
|
15
|
+
* Baselines for `visual_diff` always live in `./.rolepod-uiproof/baselines/`
|
|
16
|
+
* regardless of mode — they are user-curated configuration, not per-run
|
|
17
|
+
* evidence.
|
|
10
18
|
*/
|
|
11
19
|
type ReplayStep = Record<string, unknown>;
|
|
12
20
|
type ReplayBundle = {
|
|
@@ -17,16 +25,45 @@ type ReplayBundle = {
|
|
|
17
25
|
steps: ReplayStep[];
|
|
18
26
|
expect: ReplayStep[];
|
|
19
27
|
};
|
|
28
|
+
type ArtifactMode = "standalone" | "with-parent";
|
|
29
|
+
type StartRunOptions = {
|
|
30
|
+
/**
|
|
31
|
+
* Skill name as it appears in marketplace (`verify-ui`, `audit-a11y`,
|
|
32
|
+
* `visual-diff`, `scaffold-e2e`, `check-errors`). REQUIRED when emitting
|
|
33
|
+
* a manifest.json (Extension Protocol v1) so parent's `check-work` can
|
|
34
|
+
* route artifacts to the right phase.
|
|
35
|
+
*
|
|
36
|
+
* In `with-parent` mode the run dirname is derived from this field; if
|
|
37
|
+
* omitted the `prefix` argument is used as a fallback (legacy).
|
|
38
|
+
*/
|
|
39
|
+
skill?: string;
|
|
40
|
+
};
|
|
41
|
+
type StartRunResult = {
|
|
42
|
+
runId: string;
|
|
43
|
+
runDir: string;
|
|
44
|
+
skill: string;
|
|
45
|
+
mode: ArtifactMode;
|
|
46
|
+
};
|
|
20
47
|
declare class ArtifactStore {
|
|
21
48
|
readonly rootDir: string;
|
|
49
|
+
readonly mode: ArtifactMode;
|
|
50
|
+
private readonly baselineRoot;
|
|
22
51
|
constructor(opts?: {
|
|
23
52
|
rootDir?: string;
|
|
53
|
+
mode?: ArtifactMode;
|
|
24
54
|
});
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Allocate a fresh run dir and ensure it exists.
|
|
57
|
+
*
|
|
58
|
+
* - standalone: `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
|
|
59
|
+
* - with-parent: `./.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
|
|
60
|
+
*
|
|
61
|
+
* `prefix` is preserved for back-compat with v0.5 callers; new callers
|
|
62
|
+
* should also pass `opts.skill` so the with-parent path can be derived
|
|
63
|
+
* unambiguously and the manifest can be emitted with the canonical
|
|
64
|
+
* skill name.
|
|
65
|
+
*/
|
|
66
|
+
startRun(prefix?: string, opts?: StartRunOptions): Promise<StartRunResult>;
|
|
30
67
|
writeScreenshot(runDir: string, buf: Buffer, name: string): Promise<string>;
|
|
31
68
|
writeReplayBundle(runDir: string, bundle: ReplayBundle, name?: string): Promise<string>;
|
|
32
69
|
writeReport(runDir: string, name: string, body: string): Promise<string>;
|
|
@@ -1258,7 +1295,7 @@ declare class SessionRegistry {
|
|
|
1258
1295
|
}
|
|
1259
1296
|
|
|
1260
1297
|
declare const SERVER_NAME = "rolepod-uiproof";
|
|
1261
|
-
declare const SERVER_VERSION = "0.
|
|
1298
|
+
declare const SERVER_VERSION = "0.6.0";
|
|
1262
1299
|
type ServerHandle = {
|
|
1263
1300
|
mcp: McpServer;
|
|
1264
1301
|
registry: SessionRegistry;
|
package/dist/index.js
CHANGED
|
@@ -28,16 +28,49 @@ var log = {
|
|
|
28
28
|
// src/artifact/ArtifactStore.ts
|
|
29
29
|
var ArtifactStore = class {
|
|
30
30
|
rootDir;
|
|
31
|
+
mode;
|
|
32
|
+
baselineRoot;
|
|
31
33
|
constructor(opts = {}) {
|
|
32
|
-
|
|
34
|
+
const detectedParent = process.env.ROLEPOD_PARENT === "1";
|
|
35
|
+
this.mode = opts.mode ?? (detectedParent ? "with-parent" : "standalone");
|
|
36
|
+
if (opts.rootDir !== void 0) {
|
|
37
|
+
this.rootDir = opts.rootDir;
|
|
38
|
+
} else if (this.mode === "with-parent") {
|
|
39
|
+
this.rootDir = resolve(process.cwd(), ".rolepod", "evidence");
|
|
40
|
+
} else {
|
|
41
|
+
this.rootDir = resolve(process.cwd(), ".rolepod-uiproof", "artifacts");
|
|
42
|
+
}
|
|
43
|
+
this.baselineRoot = resolve(process.cwd(), ".rolepod-uiproof", "baselines");
|
|
33
44
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Allocate a fresh run dir and ensure it exists.
|
|
47
|
+
*
|
|
48
|
+
* - standalone: `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
|
|
49
|
+
* - with-parent: `./.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
|
|
50
|
+
*
|
|
51
|
+
* `prefix` is preserved for back-compat with v0.5 callers; new callers
|
|
52
|
+
* should also pass `opts.skill` so the with-parent path can be derived
|
|
53
|
+
* unambiguously and the manifest can be emitted with the canonical
|
|
54
|
+
* skill name.
|
|
55
|
+
*/
|
|
56
|
+
async startRun(prefix = "run", opts = {}) {
|
|
57
|
+
const ts = this.timestampSlug();
|
|
58
|
+
const skill = opts.skill ?? prefix;
|
|
59
|
+
let runId;
|
|
60
|
+
if (this.mode === "with-parent") {
|
|
61
|
+
runId = `${ts}-rolepod-uiproof-${skill}`;
|
|
62
|
+
} else {
|
|
63
|
+
runId = `${prefix}_${ts}_${randomUUID().slice(0, 8)}`;
|
|
64
|
+
}
|
|
37
65
|
const runDir = resolve(this.rootDir, runId);
|
|
38
66
|
await mkdir(runDir, { recursive: true });
|
|
39
|
-
log.debug("artifact run started", {
|
|
40
|
-
|
|
67
|
+
log.debug("artifact run started", {
|
|
68
|
+
run_id: runId,
|
|
69
|
+
dir: runDir,
|
|
70
|
+
mode: this.mode,
|
|
71
|
+
skill
|
|
72
|
+
});
|
|
73
|
+
return { runId, runDir, skill, mode: this.mode };
|
|
41
74
|
}
|
|
42
75
|
async writeScreenshot(runDir, buf, name) {
|
|
43
76
|
const path = resolve(runDir, `${name}.png`);
|
|
@@ -65,7 +98,7 @@ var ArtifactStore = class {
|
|
|
65
98
|
}
|
|
66
99
|
/** Root for stored visual baselines: `./.rolepod-uiproof/baselines/`. */
|
|
67
100
|
get baselineDir() {
|
|
68
|
-
return
|
|
101
|
+
return this.baselineRoot;
|
|
69
102
|
}
|
|
70
103
|
timestampSlug() {
|
|
71
104
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -1003,21 +1036,21 @@ var PlaywrightEngine = class {
|
|
|
1003
1036
|
if (s.dialogArming) {
|
|
1004
1037
|
s.dialogArming.resolve(false);
|
|
1005
1038
|
}
|
|
1006
|
-
return new Promise((
|
|
1039
|
+
return new Promise((resolve5) => {
|
|
1007
1040
|
const arming = {
|
|
1008
1041
|
action: opts.action,
|
|
1009
1042
|
text: opts.text,
|
|
1010
1043
|
expiresAt,
|
|
1011
1044
|
resolve: (handled) => {
|
|
1012
1045
|
s.dialogArming = null;
|
|
1013
|
-
|
|
1046
|
+
resolve5({ handled });
|
|
1014
1047
|
}
|
|
1015
1048
|
};
|
|
1016
1049
|
s.dialogArming = arming;
|
|
1017
1050
|
const timer = setTimeout(() => {
|
|
1018
1051
|
if (s.dialogArming === arming) {
|
|
1019
1052
|
s.dialogArming = null;
|
|
1020
|
-
|
|
1053
|
+
resolve5({ handled: false });
|
|
1021
1054
|
}
|
|
1022
1055
|
}, timeoutMs);
|
|
1023
1056
|
timer.unref?.();
|
|
@@ -2336,6 +2369,39 @@ var browserWaitForTool = {
|
|
|
2336
2369
|
|
|
2337
2370
|
// src/tools/composite/audit_a11y.ts
|
|
2338
2371
|
import AxeBuilder from "@axe-core/playwright";
|
|
2372
|
+
|
|
2373
|
+
// src/util/manifest.ts
|
|
2374
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
2375
|
+
import { resolve as resolve2 } from "path";
|
|
2376
|
+
var ROLEPOD_PROTOCOL_VERSION = "rolepod/v1";
|
|
2377
|
+
async function writeManifest(input) {
|
|
2378
|
+
const manifest = {
|
|
2379
|
+
protocol: ROLEPOD_PROTOCOL_VERSION,
|
|
2380
|
+
plugin: "rolepod-uiproof",
|
|
2381
|
+
skill: input.skill,
|
|
2382
|
+
phase: input.phase,
|
|
2383
|
+
status: input.status,
|
|
2384
|
+
summary: input.summary,
|
|
2385
|
+
started_at: input.startedAt,
|
|
2386
|
+
finished_at: input.finishedAt,
|
|
2387
|
+
artifacts: input.artifacts,
|
|
2388
|
+
metadata: input.metadata ?? {}
|
|
2389
|
+
};
|
|
2390
|
+
const path = resolve2(input.runDir, "manifest.json");
|
|
2391
|
+
try {
|
|
2392
|
+
await writeFile2(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
2393
|
+
return path;
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
log.warn("manifest write failed", {
|
|
2396
|
+
run_dir: input.runDir,
|
|
2397
|
+
skill: input.skill,
|
|
2398
|
+
err: String(err)
|
|
2399
|
+
});
|
|
2400
|
+
return void 0;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/tools/composite/audit_a11y.ts
|
|
2339
2405
|
var TAGS_BY_LEVEL = {
|
|
2340
2406
|
"wcag-a": ["wcag2a", "wcag21a"],
|
|
2341
2407
|
"wcag-aa": ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
|
|
@@ -2360,7 +2426,11 @@ var auditA11yTool = {
|
|
|
2360
2426
|
inputShape: auditA11yShape,
|
|
2361
2427
|
build(ctx) {
|
|
2362
2428
|
return safeHandler(async (args) => {
|
|
2363
|
-
const
|
|
2429
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2430
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
2431
|
+
"audit",
|
|
2432
|
+
{ skill: "audit-a11y" }
|
|
2433
|
+
);
|
|
2364
2434
|
const session = await ctx.registry.open(args.open);
|
|
2365
2435
|
const engine = ctx.registry.engineFor(session.id);
|
|
2366
2436
|
if (!(engine instanceof PlaywrightEngine)) {
|
|
@@ -2428,15 +2498,45 @@ var auditA11yTool = {
|
|
|
2428
2498
|
await ctx.registry.close(session).catch(() => void 0);
|
|
2429
2499
|
}
|
|
2430
2500
|
}
|
|
2501
|
+
const counts = countBySeverity(issues);
|
|
2502
|
+
const status = a11yStatus(counts);
|
|
2503
|
+
const artifacts = reportPath ? [{ type: "report", path: reportPath }] : [];
|
|
2504
|
+
const manifestPath = await writeManifest({
|
|
2505
|
+
runDir,
|
|
2506
|
+
skill,
|
|
2507
|
+
phase: "verify",
|
|
2508
|
+
status,
|
|
2509
|
+
summary: buildAuditSummary(args.level, counts, status),
|
|
2510
|
+
startedAt,
|
|
2511
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2512
|
+
artifacts,
|
|
2513
|
+
metadata: {
|
|
2514
|
+
level: args.level,
|
|
2515
|
+
scope: args.scope,
|
|
2516
|
+
counts,
|
|
2517
|
+
report_format: args.report_format
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2431
2520
|
return ok({
|
|
2432
2521
|
run_id: runId,
|
|
2433
|
-
counts
|
|
2522
|
+
counts,
|
|
2434
2523
|
issues,
|
|
2435
|
-
report_path: reportPath
|
|
2524
|
+
report_path: reportPath,
|
|
2525
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
2436
2526
|
});
|
|
2437
2527
|
});
|
|
2438
2528
|
}
|
|
2439
2529
|
};
|
|
2530
|
+
function a11yStatus(counts) {
|
|
2531
|
+
if ((counts.critical ?? 0) + (counts.serious ?? 0) > 0) return "fail";
|
|
2532
|
+
if ((counts.moderate ?? 0) + (counts.minor ?? 0) > 0) return "warn";
|
|
2533
|
+
return "pass";
|
|
2534
|
+
}
|
|
2535
|
+
function buildAuditSummary(level, counts, status) {
|
|
2536
|
+
const total = (counts.critical ?? 0) + (counts.serious ?? 0) + (counts.moderate ?? 0) + (counts.minor ?? 0);
|
|
2537
|
+
if (status === "pass") return `${level}: 0 issues`;
|
|
2538
|
+
return `${level}: ${total} issue(s) \u2014 critical=${counts.critical ?? 0}, serious=${counts.serious ?? 0}, moderate=${counts.moderate ?? 0}, minor=${counts.minor ?? 0}`;
|
|
2539
|
+
}
|
|
2440
2540
|
function pickWcagRef(tags) {
|
|
2441
2541
|
return tags.find((t) => /^wcag\d/.test(t));
|
|
2442
2542
|
}
|
|
@@ -2553,14 +2653,18 @@ function scoreTree(root, tokens) {
|
|
|
2553
2653
|
|
|
2554
2654
|
// src/tools/composite/scaffold_e2e.ts
|
|
2555
2655
|
import { readFile } from "fs/promises";
|
|
2556
|
-
import { resolve as
|
|
2656
|
+
import { resolve as resolve3 } from "path";
|
|
2557
2657
|
var scaffoldE2eTool = {
|
|
2558
2658
|
name: ToolNames.scaffoldE2e,
|
|
2559
2659
|
description: "Generate a runnable e2e test file (playwright-test, vitest+playwright, or pytest+selenium) from a scenario description and optional replay bundle from a prior verify_ui_flow run.",
|
|
2560
2660
|
inputShape: scaffoldE2eShape,
|
|
2561
2661
|
build(ctx) {
|
|
2562
2662
|
return safeHandler(async (args) => {
|
|
2563
|
-
const
|
|
2663
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2664
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
2665
|
+
"scaffold",
|
|
2666
|
+
{ skill: "scaffold-e2e" }
|
|
2667
|
+
);
|
|
2564
2668
|
const slug = slugify(args.scenario_nl);
|
|
2565
2669
|
const bundle = args.recorded_bundle ? await loadReplay(args.recorded_bundle) : null;
|
|
2566
2670
|
const ctxObj = { args, slug, bundle };
|
|
@@ -2598,19 +2702,35 @@ var scaffoldE2eTool = {
|
|
|
2598
2702
|
);
|
|
2599
2703
|
}
|
|
2600
2704
|
const path = await ctx.store.writeReport(runDir, filename, body);
|
|
2705
|
+
const manifestPath = await writeManifest({
|
|
2706
|
+
runDir,
|
|
2707
|
+
skill,
|
|
2708
|
+
phase: "build",
|
|
2709
|
+
status: "pass",
|
|
2710
|
+
summary: `generated ${args.framework} test "${filename}" from ${bundle ? "replay bundle" : "scenario"}`,
|
|
2711
|
+
startedAt,
|
|
2712
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2713
|
+
artifacts: [{ type: "test_file", path }],
|
|
2714
|
+
metadata: {
|
|
2715
|
+
framework: args.framework,
|
|
2716
|
+
language,
|
|
2717
|
+
from_replay_bundle: Boolean(bundle)
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2601
2720
|
return ok({
|
|
2602
2721
|
run_id: runId,
|
|
2603
2722
|
test_file_path: path,
|
|
2604
2723
|
language,
|
|
2605
2724
|
dependencies,
|
|
2606
2725
|
setup_notes: setupNotes,
|
|
2607
|
-
from_replay_bundle: Boolean(bundle)
|
|
2726
|
+
from_replay_bundle: Boolean(bundle),
|
|
2727
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
2608
2728
|
});
|
|
2609
2729
|
});
|
|
2610
2730
|
}
|
|
2611
2731
|
};
|
|
2612
2732
|
async function loadReplay(bundlePath) {
|
|
2613
|
-
const raw = await readFile(
|
|
2733
|
+
const raw = await readFile(resolve3(bundlePath), "utf8");
|
|
2614
2734
|
return JSON.parse(raw);
|
|
2615
2735
|
}
|
|
2616
2736
|
function slugify(s) {
|
|
@@ -2934,7 +3054,11 @@ var verifyUiFlowTool = {
|
|
|
2934
3054
|
inputShape: verifyUiFlowShape,
|
|
2935
3055
|
build(ctx) {
|
|
2936
3056
|
return safeHandler(async (args) => {
|
|
2937
|
-
const
|
|
3057
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3058
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3059
|
+
"verify",
|
|
3060
|
+
{ skill: "verify-ui" }
|
|
3061
|
+
);
|
|
2938
3062
|
const initial = await runFlow(ctx, args, args.steps, runDir, {
|
|
2939
3063
|
captureEvidence: true,
|
|
2940
3064
|
bundleName: "replay.json"
|
|
@@ -2959,10 +3083,49 @@ var verifyUiFlowTool = {
|
|
|
2959
3083
|
attempts: min.attempts
|
|
2960
3084
|
};
|
|
2961
3085
|
}
|
|
3086
|
+
const manifestPath = await writeManifest({
|
|
3087
|
+
runDir,
|
|
3088
|
+
skill,
|
|
3089
|
+
phase: "verify",
|
|
3090
|
+
status: initial.passed ? "pass" : "fail",
|
|
3091
|
+
summary: buildVerifySummary(args, initial),
|
|
3092
|
+
startedAt,
|
|
3093
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3094
|
+
artifacts: flattenVerifyEvidence(initial.evidence),
|
|
3095
|
+
metadata: {
|
|
3096
|
+
mode: args.mode,
|
|
3097
|
+
step_count: args.steps.length,
|
|
3098
|
+
expect_count: args.expect.length,
|
|
3099
|
+
...initial.finalUrl !== void 0 ? { final_url: initial.finalUrl } : {}
|
|
3100
|
+
}
|
|
3101
|
+
});
|
|
3102
|
+
if (manifestPath) result.manifest = manifestPath;
|
|
2962
3103
|
return ok(result);
|
|
2963
3104
|
});
|
|
2964
3105
|
}
|
|
2965
3106
|
};
|
|
3107
|
+
function buildVerifySummary(args, outcome) {
|
|
3108
|
+
const stepCount = args.steps.length;
|
|
3109
|
+
const expectCount = args.expect.length;
|
|
3110
|
+
if (outcome.passed) {
|
|
3111
|
+
return `${stepCount} step(s), ${expectCount} expect(s) passed`;
|
|
3112
|
+
}
|
|
3113
|
+
if (outcome.failedAtStep !== void 0) {
|
|
3114
|
+
return `failed at step ${outcome.failedAtStep}: ${outcome.failureReason ?? "unknown"}`;
|
|
3115
|
+
}
|
|
3116
|
+
return `failed: ${outcome.failureReason ?? "unknown"}`;
|
|
3117
|
+
}
|
|
3118
|
+
function flattenVerifyEvidence(ev) {
|
|
3119
|
+
const out = [];
|
|
3120
|
+
for (const s of ev.screenshots) out.push({ type: "screenshot", path: s });
|
|
3121
|
+
if (ev.replay_bundle) out.push({ type: "replay_bundle", path: ev.replay_bundle });
|
|
3122
|
+
if (ev.console) out.push({ type: "console", path: ev.console });
|
|
3123
|
+
if (ev.a11y_tree) out.push({ type: "a11y_tree", path: ev.a11y_tree });
|
|
3124
|
+
if (ev.har) out.push({ type: "har", path: ev.har });
|
|
3125
|
+
if (ev.trace) out.push({ type: "trace", path: ev.trace });
|
|
3126
|
+
if (ev.video) for (const v of ev.video) out.push({ type: "video", path: v });
|
|
3127
|
+
return out;
|
|
3128
|
+
}
|
|
2966
3129
|
function buildCaptureOptions(captures, runDir) {
|
|
2967
3130
|
const cap = {};
|
|
2968
3131
|
if (captures.has("har")) {
|
|
@@ -3377,7 +3540,7 @@ function treeHasText(tree, text) {
|
|
|
3377
3540
|
// src/tools/composite/visual_diff.ts
|
|
3378
3541
|
import { existsSync } from "fs";
|
|
3379
3542
|
import { readFile as readFile2 } from "fs/promises";
|
|
3380
|
-
import { resolve as
|
|
3543
|
+
import { resolve as resolve4 } from "path";
|
|
3381
3544
|
import pixelmatch from "pixelmatch";
|
|
3382
3545
|
import { PNG } from "pngjs";
|
|
3383
3546
|
var visualDiffTool = {
|
|
@@ -3386,7 +3549,11 @@ var visualDiffTool = {
|
|
|
3386
3549
|
inputShape: visualDiffShape,
|
|
3387
3550
|
build(ctx) {
|
|
3388
3551
|
return safeHandler(async (args) => {
|
|
3389
|
-
const
|
|
3552
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3553
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3554
|
+
"vdiff",
|
|
3555
|
+
{ skill: "visual-diff" }
|
|
3556
|
+
);
|
|
3390
3557
|
const session = await ctx.registry.open({
|
|
3391
3558
|
...args.open,
|
|
3392
3559
|
...args.viewport ? { viewport: args.viewport } : {}
|
|
@@ -3405,7 +3572,7 @@ var visualDiffTool = {
|
|
|
3405
3572
|
);
|
|
3406
3573
|
const currentPath = await ctx.store.writeScreenshot(runDir, buf, "current");
|
|
3407
3574
|
await ctx.store.ensureDir(ctx.store.baselineDir);
|
|
3408
|
-
const baselinePath =
|
|
3575
|
+
const baselinePath = resolve4(
|
|
3409
3576
|
ctx.store.baselineDir,
|
|
3410
3577
|
`${args.baseline_id}.png`
|
|
3411
3578
|
);
|
|
@@ -3415,6 +3582,20 @@ var visualDiffTool = {
|
|
|
3415
3582
|
`${args.baseline_id}.png`,
|
|
3416
3583
|
buf
|
|
3417
3584
|
);
|
|
3585
|
+
const manifestPath2 = await writeManifest({
|
|
3586
|
+
runDir,
|
|
3587
|
+
skill,
|
|
3588
|
+
phase: "verify",
|
|
3589
|
+
status: "pass",
|
|
3590
|
+
summary: `baseline "${args.baseline_id}" seeded from current capture`,
|
|
3591
|
+
startedAt,
|
|
3592
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3593
|
+
artifacts: [
|
|
3594
|
+
{ type: "baseline", path: baselinePath },
|
|
3595
|
+
{ type: "screenshot", path: currentPath }
|
|
3596
|
+
],
|
|
3597
|
+
metadata: { baseline_id: args.baseline_id, seeded: true }
|
|
3598
|
+
});
|
|
3418
3599
|
return ok({
|
|
3419
3600
|
run_id: runId,
|
|
3420
3601
|
baseline_id: args.baseline_id,
|
|
@@ -3422,6 +3603,7 @@ var visualDiffTool = {
|
|
|
3422
3603
|
passed: true,
|
|
3423
3604
|
baseline_path: baselinePath,
|
|
3424
3605
|
current_path: currentPath,
|
|
3606
|
+
...manifestPath2 ? { manifest: manifestPath2 } : {},
|
|
3425
3607
|
note: "Baseline did not exist \u2014 current capture saved as the new baseline."
|
|
3426
3608
|
});
|
|
3427
3609
|
}
|
|
@@ -3452,21 +3634,45 @@ var visualDiffTool = {
|
|
|
3452
3634
|
);
|
|
3453
3635
|
const total = baseline.width * baseline.height;
|
|
3454
3636
|
const diffPct = diffPixels / total;
|
|
3637
|
+
const passed = diffPct <= args.threshold_pct;
|
|
3455
3638
|
const diffImagePath = await ctx.store.writeBytes(
|
|
3456
3639
|
runDir,
|
|
3457
3640
|
"diff.png",
|
|
3458
3641
|
PNG.sync.write(diff)
|
|
3459
3642
|
);
|
|
3643
|
+
const artifacts = [
|
|
3644
|
+
{ type: "baseline", path: baselinePath },
|
|
3645
|
+
{ type: "screenshot", path: currentPath },
|
|
3646
|
+
{ type: "diff", path: diffImagePath }
|
|
3647
|
+
];
|
|
3648
|
+
const manifestPath = await writeManifest({
|
|
3649
|
+
runDir,
|
|
3650
|
+
skill,
|
|
3651
|
+
phase: "verify",
|
|
3652
|
+
status: passed ? "pass" : "fail",
|
|
3653
|
+
summary: `diff ${(diffPct * 100).toFixed(3)}% vs baseline "${args.baseline_id}" (threshold ${(args.threshold_pct * 100).toFixed(3)}%)`,
|
|
3654
|
+
startedAt,
|
|
3655
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3656
|
+
artifacts,
|
|
3657
|
+
metadata: {
|
|
3658
|
+
baseline_id: args.baseline_id,
|
|
3659
|
+
diff_pct: Number(diffPct.toFixed(6)),
|
|
3660
|
+
diff_pixels: diffPixels,
|
|
3661
|
+
total_pixels: total,
|
|
3662
|
+
threshold_pct: args.threshold_pct
|
|
3663
|
+
}
|
|
3664
|
+
});
|
|
3460
3665
|
return ok({
|
|
3461
3666
|
run_id: runId,
|
|
3462
3667
|
baseline_id: args.baseline_id,
|
|
3463
3668
|
diff_pct: Number(diffPct.toFixed(6)),
|
|
3464
3669
|
diff_pixels: diffPixels,
|
|
3465
3670
|
total_pixels: total,
|
|
3466
|
-
passed
|
|
3671
|
+
passed,
|
|
3467
3672
|
baseline_path: baselinePath,
|
|
3468
3673
|
current_path: currentPath,
|
|
3469
|
-
diff_image_path: diffImagePath
|
|
3674
|
+
diff_image_path: diffImagePath,
|
|
3675
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
3470
3676
|
});
|
|
3471
3677
|
} finally {
|
|
3472
3678
|
if (args.close_on_finish) {
|
|
@@ -3735,8 +3941,19 @@ var toolMetadata = {
|
|
|
3735
3941
|
|
|
3736
3942
|
// src/server.ts
|
|
3737
3943
|
var SERVER_NAME = "rolepod-uiproof";
|
|
3738
|
-
var SERVER_VERSION = "0.
|
|
3944
|
+
var SERVER_VERSION = "0.6.0";
|
|
3945
|
+
var SUPPORTED_PROTOCOL = "v1";
|
|
3946
|
+
function checkProtocolCompat() {
|
|
3947
|
+
const requested = process.env.ROLEPOD_PROTOCOL;
|
|
3948
|
+
if (!requested) return;
|
|
3949
|
+
if (requested !== SUPPORTED_PROTOCOL) {
|
|
3950
|
+
console.warn(
|
|
3951
|
+
`rolepod protocol mismatch: expected ${SUPPORTED_PROTOCOL}, got ${requested}. Manifest will still be written in ${SUPPORTED_PROTOCOL} shape \u2014 parent may not parse it correctly.`
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3739
3955
|
function buildServer(opts = {}) {
|
|
3956
|
+
checkProtocolCompat();
|
|
3740
3957
|
const webEngine = createWebEngine();
|
|
3741
3958
|
const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
|
|
3742
3959
|
registry.register("web", webEngine);
|
|
@@ -3797,6 +4014,8 @@ function buildServer(opts = {}) {
|
|
|
3797
4014
|
}
|
|
3798
4015
|
log.info("rolepod-uiproof server built", {
|
|
3799
4016
|
version: SERVER_VERSION,
|
|
4017
|
+
protocol: SUPPORTED_PROTOCOL,
|
|
4018
|
+
mode: store.mode,
|
|
3800
4019
|
tools: tools.map((t) => t.name)
|
|
3801
4020
|
});
|
|
3802
4021
|
return {
|