@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
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "rolepod-uiproof",
|
|
12
12
|
"source": "./",
|
|
13
|
-
"description": "26 MCP tools (21 atomic browser/mobile primitives + 5 composite workflows) + 5 user-invocable skills. v0.
|
|
14
|
-
"version": "0.
|
|
13
|
+
"description": "26 MCP tools (21 atomic browser/mobile primitives + 5 composite workflows) + 5 user-invocable skills. v0.6 adds Extension Protocol v1 support — works standalone today, becomes the verify-phase UI provider when installed alongside the `rolepod` parent plugin (evidence routes to `.rolepod/evidence/` with `manifest.json`). Replaces chrome-devtools-mcp and playwright-mcp for UI testing. Web production-ready via Playwright; mobile (iOS/Android) via Appium scaffolded — see `rolepod-uiproof doctor` for readiness.",
|
|
14
|
+
"version": "0.6.0",
|
|
15
15
|
"author": {
|
|
16
16
|
"name": "nuttaruj"
|
|
17
17
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rolepod-uiproof",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. v0.5
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. Works standalone OR with the `rolepod` parent plugin: when ROLEPOD_PARENT=1 is set, evidence routes to `.rolepod/evidence/` with a `manifest.json` per Extension Protocol v1, so parent's `check-work` skill can aggregate UI verify results into its phase report. v0.5 completed the UI verification surface (console + network observability, hover/drag/fill_form/upload/dialog, runtime emulation, multi-page, gated JS eval).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "nuttaruj",
|
|
7
7
|
"url": "https://github.com/nuttaruj"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rolepod-uiproof",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. v0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Multi-platform UI/mobile automation for AI agents — 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) + MCP server with 26 tools. v0.6 adds Extension Protocol v1 — works standalone today, becomes the verify-phase UI provider when paired with the `rolepod` parent plugin.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "nuttaruj",
|
|
7
7
|
"url": "https://github.com/nuttaruj"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"interface": {
|
|
26
26
|
"displayName": "Rolepod UIProof",
|
|
27
27
|
"shortDescription": "UI verification, a11y audits, visual diff, e2e scaffolding — for AI coding agents.",
|
|
28
|
-
"longDescription": "rolepod-uiproof ships an MCP server with 26 tools (21 atomic + 5 composite) and 5 user-invocable skills (/verify-ui, /audit-a11y, /visual-diff, /scaffold-e2e, /check-errors). Web is fully supported via Playwright; mobile (iOS/Android via Appium) supports basic input. v0.
|
|
28
|
+
"longDescription": "rolepod-uiproof ships an MCP server with 26 tools (21 atomic + 5 composite) and 5 user-invocable skills (/verify-ui, /audit-a11y, /visual-diff, /scaffold-e2e, /check-errors). Web is fully supported via Playwright; mobile (iOS/Android via Appium) supports basic input. v0.6: pair with the `rolepod` parent plugin (v2.7+) and uiproof becomes the verify-phase UI provider — evidence routes to `.rolepod/evidence/` with a `manifest.json` per Extension Protocol v1.",
|
|
29
29
|
"developerName": "nuttaruj",
|
|
30
30
|
"category": "Productivity",
|
|
31
31
|
"capabilities": ["Read", "Write", "Bash"],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rolepod-uiproof",
|
|
3
3
|
"displayName": "Rolepod UIProof",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Multi-platform UI / mobile automation MCP server + 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) for AI coding agents. v0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
|
+
"description": "Multi-platform UI / mobile automation MCP server + 5 shipped skills (verify-ui, audit-a11y, visual-diff, scaffold-e2e, check-errors) for AI coding agents. v0.6 adds Extension Protocol v1 — works standalone today, becomes the verify-phase UI provider when paired with the `rolepod` parent plugin (evidence routes to `.rolepod/evidence/` with `manifest.json`). Replaces chrome-devtools-mcp and playwright-mcp.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "nuttaruj"
|
|
8
8
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,75 @@ release.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] — 2026-05-27
|
|
11
|
+
|
|
12
|
+
**Extension Protocol v1 — `uiproof` becomes parent-aware. Standalone
|
|
13
|
+
behavior unchanged.**
|
|
14
|
+
|
|
15
|
+
When the parent `rolepod` plugin (v2.7+) sets `ROLEPOD_PARENT=1` via
|
|
16
|
+
its SessionStart hook, uiproof routes evidence to the shared
|
|
17
|
+
`.rolepod/evidence/` tree and emits a `manifest.json` per spec so the
|
|
18
|
+
parent's `check-work` skill can aggregate UI verify results into its
|
|
19
|
+
phase report. With no parent installed the v0.5 behavior is preserved
|
|
20
|
+
exactly — same artifact path, same tool output, plus a `manifest.json`
|
|
21
|
+
in each run dir as a bonus.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Env-aware evidence path** in `ArtifactStore`. Detected at
|
|
26
|
+
construction from `process.env.ROLEPOD_PARENT === "1"`.
|
|
27
|
+
- standalone: `.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
|
|
28
|
+
- with-parent: `.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
|
|
29
|
+
- **`manifest.json`** written by every composite that starts a run
|
|
30
|
+
(`verify_ui_flow`, `audit_a11y`, `visual_diff`, `scaffold_e2e`).
|
|
31
|
+
Schema follows Extension Protocol v1: `protocol`, `plugin`, `skill`,
|
|
32
|
+
`phase`, `status`, `summary`, `started_at`, `finished_at`,
|
|
33
|
+
`artifacts: [{type, path}]`, `metadata`. Best-effort: any IO failure
|
|
34
|
+
is logged but never thrown.
|
|
35
|
+
- **Graduated a11y status**. `audit_a11y` manifest carries `status`:
|
|
36
|
+
`critical/serious > 0 → fail`, `moderate/minor > 0 → warn`, no
|
|
37
|
+
issues → `pass`. Keeps the `warn` signal a strict pass/fail would
|
|
38
|
+
discard.
|
|
39
|
+
- **Protocol version check**. When `ROLEPOD_PROTOCOL` is set but
|
|
40
|
+
does not equal `v1`, `buildServer()` logs a one-shot warning. Does
|
|
41
|
+
not block; manifest is still written in v1 shape.
|
|
42
|
+
- **`/check-errors` evidence routing doc** alongside the other 4
|
|
43
|
+
skills.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `ArtifactStore.startRun(prefix, opts?)` — `opts.skill` is new and
|
|
48
|
+
optional. Provides the canonical skill name for both the
|
|
49
|
+
with-parent dirname and the manifest's `skill` field. Return shape
|
|
50
|
+
extended with `skill` and `mode` (back-compat: existing destructuring
|
|
51
|
+
of `{ runId, runDir }` keeps working).
|
|
52
|
+
- `buildServer()` log line surfaces `protocol: "v1"` and
|
|
53
|
+
`mode: "standalone" | "with-parent"` alongside the existing version
|
|
54
|
+
+ tools list.
|
|
55
|
+
- All 5 shipped skills' SKILL.md gained an "Evidence routing" section
|
|
56
|
+
between "Process" / "Outputs" and "If the tool is unavailable".
|
|
57
|
+
Mirrored to `plugins/rolepod-uiproof/skills/`.
|
|
58
|
+
- README "Standalone vs Combined" section added explaining the two
|
|
59
|
+
modes.
|
|
60
|
+
|
|
61
|
+
### Behavior
|
|
62
|
+
|
|
63
|
+
- **Standalone:** unchanged. Evidence still written to
|
|
64
|
+
`.rolepod-uiproof/artifacts/`. New: a `manifest.json` appears in each
|
|
65
|
+
run dir. Tool return values gain an optional `manifest: "<path>"`
|
|
66
|
+
field; everything else is byte-for-byte identical.
|
|
67
|
+
- **With rolepod parent:** evidence written to
|
|
68
|
+
`.rolepod/evidence/<ts>-rolepod-uiproof-<skill>/` with `manifest.json`
|
|
69
|
+
per protocol spec. Visual baselines stay in
|
|
70
|
+
`.rolepod-uiproof/baselines/` regardless of mode.
|
|
71
|
+
|
|
72
|
+
### Non-goals (kept out of v0.6)
|
|
73
|
+
|
|
74
|
+
- Dynamic capabilities registry (`.claude-plugin/capabilities.json`)
|
|
75
|
+
- Protocol version negotiation beyond a single warn
|
|
76
|
+
- Cross-child coordination (uiproof ↔ wplab handoff inside one run)
|
|
77
|
+
- Mobile platform support stays at the v0.5 partial level
|
|
78
|
+
|
|
10
79
|
## [0.5.0] — 2026-05-27
|
|
11
80
|
|
|
12
81
|
**Complete UI verification surface — one MCP replaces chrome-devtools-mcp
|
package/README.md
CHANGED
|
@@ -27,6 +27,21 @@ One MCP server, one tool surface, five skills you invoke from chat. Web is produ
|
|
|
27
27
|
|
|
28
28
|
Every skill is **single-backend** (D-024) — it calls the rolepod-uiproof server and only the rolepod-uiproof server. If the server is unavailable, the skill fails with a clear diagnostic. Multi-backend routing belongs in the parent [`rolepod`](https://github.com/nuttaruj/rolepod) plugin's phase skills, not here.
|
|
29
29
|
|
|
30
|
+
## Standalone vs Combined
|
|
31
|
+
|
|
32
|
+
`rolepod-uiproof` works either as a **standalone** browser MCP for any project, or **combined** with the [`rolepod`](https://github.com/nuttaruj/rolepod) parent plugin (v2.7+) where it becomes the Verify phase provider for UI artifacts.
|
|
33
|
+
|
|
34
|
+
**Standalone** (default): use the 5 skills directly as atomic browser tools. Evidence saved under `./.rolepod-uiproof/artifacts/<run>/` with a `manifest.json` per Extension Protocol v1.
|
|
35
|
+
|
|
36
|
+
**Combined with rolepod parent**: when the parent's SessionStart hook sets `ROLEPOD_PARENT=1`, uiproof writes evidence to `./.rolepod/evidence/<ts>-rolepod-uiproof-<skill>/` instead, where parent's `check-work` skill auto-aggregates manifests into the verify report. No skill changes — same 26 tools, same 5 skills, smarter routing.
|
|
37
|
+
|
|
38
|
+
| Install | Unlocks |
|
|
39
|
+
|---|---|
|
|
40
|
+
| uiproof alone | Browser test, a11y audit, visual diff, e2e scaffold, error gate |
|
|
41
|
+
| uiproof + rolepod parent | + verify-phase aggregation, evidence handoff to `check-work` |
|
|
42
|
+
|
|
43
|
+
The `manifest.json` is written in BOTH modes, so installing the parent later still lets historic artifacts get picked up. Baselines for `/visual-diff` always live in `./.rolepod-uiproof/baselines/` regardless of mode — they are user-curated configuration, not per-run evidence.
|
|
44
|
+
|
|
30
45
|
## Install
|
|
31
46
|
|
|
32
47
|
Pick your CLI. All install paths share the same MCP server (`@rolepod/uiproof` on npm) and the same skill set.
|
|
@@ -175,7 +175,7 @@ function runInstallMobile() {
|
|
|
175
175
|
|
|
176
176
|
// src/cli/replay.ts
|
|
177
177
|
import { readFile } from "fs/promises";
|
|
178
|
-
import { resolve as
|
|
178
|
+
import { resolve as resolve4 } from "path";
|
|
179
179
|
|
|
180
180
|
// src/artifact/ArtifactStore.ts
|
|
181
181
|
import { randomUUID } from "crypto";
|
|
@@ -204,16 +204,49 @@ var log = {
|
|
|
204
204
|
// src/artifact/ArtifactStore.ts
|
|
205
205
|
var ArtifactStore = class {
|
|
206
206
|
rootDir;
|
|
207
|
+
mode;
|
|
208
|
+
baselineRoot;
|
|
207
209
|
constructor(opts = {}) {
|
|
208
|
-
|
|
210
|
+
const detectedParent = process.env.ROLEPOD_PARENT === "1";
|
|
211
|
+
this.mode = opts.mode ?? (detectedParent ? "with-parent" : "standalone");
|
|
212
|
+
if (opts.rootDir !== void 0) {
|
|
213
|
+
this.rootDir = opts.rootDir;
|
|
214
|
+
} else if (this.mode === "with-parent") {
|
|
215
|
+
this.rootDir = resolve2(process.cwd(), ".rolepod", "evidence");
|
|
216
|
+
} else {
|
|
217
|
+
this.rootDir = resolve2(process.cwd(), ".rolepod-uiproof", "artifacts");
|
|
218
|
+
}
|
|
219
|
+
this.baselineRoot = resolve2(process.cwd(), ".rolepod-uiproof", "baselines");
|
|
209
220
|
}
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Allocate a fresh run dir and ensure it exists.
|
|
223
|
+
*
|
|
224
|
+
* - standalone: `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
|
|
225
|
+
* - with-parent: `./.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
|
|
226
|
+
*
|
|
227
|
+
* `prefix` is preserved for back-compat with v0.5 callers; new callers
|
|
228
|
+
* should also pass `opts.skill` so the with-parent path can be derived
|
|
229
|
+
* unambiguously and the manifest can be emitted with the canonical
|
|
230
|
+
* skill name.
|
|
231
|
+
*/
|
|
232
|
+
async startRun(prefix = "run", opts = {}) {
|
|
233
|
+
const ts = this.timestampSlug();
|
|
234
|
+
const skill = opts.skill ?? prefix;
|
|
235
|
+
let runId;
|
|
236
|
+
if (this.mode === "with-parent") {
|
|
237
|
+
runId = `${ts}-rolepod-uiproof-${skill}`;
|
|
238
|
+
} else {
|
|
239
|
+
runId = `${prefix}_${ts}_${randomUUID().slice(0, 8)}`;
|
|
240
|
+
}
|
|
213
241
|
const runDir = resolve2(this.rootDir, runId);
|
|
214
242
|
await mkdir(runDir, { recursive: true });
|
|
215
|
-
log.debug("artifact run started", {
|
|
216
|
-
|
|
243
|
+
log.debug("artifact run started", {
|
|
244
|
+
run_id: runId,
|
|
245
|
+
dir: runDir,
|
|
246
|
+
mode: this.mode,
|
|
247
|
+
skill
|
|
248
|
+
});
|
|
249
|
+
return { runId, runDir, skill, mode: this.mode };
|
|
217
250
|
}
|
|
218
251
|
async writeScreenshot(runDir, buf, name) {
|
|
219
252
|
const path = resolve2(runDir, `${name}.png`);
|
|
@@ -241,7 +274,7 @@ var ArtifactStore = class {
|
|
|
241
274
|
}
|
|
242
275
|
/** Root for stored visual baselines: `./.rolepod-uiproof/baselines/`. */
|
|
243
276
|
get baselineDir() {
|
|
244
|
-
return
|
|
277
|
+
return this.baselineRoot;
|
|
245
278
|
}
|
|
246
279
|
timestampSlug() {
|
|
247
280
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -1170,21 +1203,21 @@ var PlaywrightEngine = class {
|
|
|
1170
1203
|
if (s.dialogArming) {
|
|
1171
1204
|
s.dialogArming.resolve(false);
|
|
1172
1205
|
}
|
|
1173
|
-
return new Promise((
|
|
1206
|
+
return new Promise((resolve7) => {
|
|
1174
1207
|
const arming = {
|
|
1175
1208
|
action: opts.action,
|
|
1176
1209
|
text: opts.text,
|
|
1177
1210
|
expiresAt,
|
|
1178
1211
|
resolve: (handled) => {
|
|
1179
1212
|
s.dialogArming = null;
|
|
1180
|
-
|
|
1213
|
+
resolve7({ handled });
|
|
1181
1214
|
}
|
|
1182
1215
|
};
|
|
1183
1216
|
s.dialogArming = arming;
|
|
1184
1217
|
const timer = setTimeout(() => {
|
|
1185
1218
|
if (s.dialogArming === arming) {
|
|
1186
1219
|
s.dialogArming = null;
|
|
1187
|
-
|
|
1220
|
+
resolve7({ handled: false });
|
|
1188
1221
|
}
|
|
1189
1222
|
}, timeoutMs);
|
|
1190
1223
|
timer.unref?.();
|
|
@@ -2035,6 +2068,37 @@ async function ddmin(input, reproduces) {
|
|
|
2035
2068
|
return current;
|
|
2036
2069
|
}
|
|
2037
2070
|
|
|
2071
|
+
// src/util/manifest.ts
|
|
2072
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
2073
|
+
import { resolve as resolve3 } from "path";
|
|
2074
|
+
var ROLEPOD_PROTOCOL_VERSION = "rolepod/v1";
|
|
2075
|
+
async function writeManifest(input) {
|
|
2076
|
+
const manifest = {
|
|
2077
|
+
protocol: ROLEPOD_PROTOCOL_VERSION,
|
|
2078
|
+
plugin: "rolepod-uiproof",
|
|
2079
|
+
skill: input.skill,
|
|
2080
|
+
phase: input.phase,
|
|
2081
|
+
status: input.status,
|
|
2082
|
+
summary: input.summary,
|
|
2083
|
+
started_at: input.startedAt,
|
|
2084
|
+
finished_at: input.finishedAt,
|
|
2085
|
+
artifacts: input.artifacts,
|
|
2086
|
+
metadata: input.metadata ?? {}
|
|
2087
|
+
};
|
|
2088
|
+
const path = resolve3(input.runDir, "manifest.json");
|
|
2089
|
+
try {
|
|
2090
|
+
await writeFile2(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
2091
|
+
return path;
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
log.warn("manifest write failed", {
|
|
2094
|
+
run_dir: input.runDir,
|
|
2095
|
+
skill: input.skill,
|
|
2096
|
+
err: String(err)
|
|
2097
|
+
});
|
|
2098
|
+
return void 0;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2038
2102
|
// src/tools/result.ts
|
|
2039
2103
|
function ok(value) {
|
|
2040
2104
|
return {
|
|
@@ -2076,7 +2140,11 @@ var verifyUiFlowTool = {
|
|
|
2076
2140
|
inputShape: verifyUiFlowShape,
|
|
2077
2141
|
build(ctx) {
|
|
2078
2142
|
return safeHandler(async (args) => {
|
|
2079
|
-
const
|
|
2143
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2144
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
2145
|
+
"verify",
|
|
2146
|
+
{ skill: "verify-ui" }
|
|
2147
|
+
);
|
|
2080
2148
|
const initial = await runFlow(ctx, args, args.steps, runDir, {
|
|
2081
2149
|
captureEvidence: true,
|
|
2082
2150
|
bundleName: "replay.json"
|
|
@@ -2101,10 +2169,49 @@ var verifyUiFlowTool = {
|
|
|
2101
2169
|
attempts: min.attempts
|
|
2102
2170
|
};
|
|
2103
2171
|
}
|
|
2172
|
+
const manifestPath = await writeManifest({
|
|
2173
|
+
runDir,
|
|
2174
|
+
skill,
|
|
2175
|
+
phase: "verify",
|
|
2176
|
+
status: initial.passed ? "pass" : "fail",
|
|
2177
|
+
summary: buildVerifySummary(args, initial),
|
|
2178
|
+
startedAt,
|
|
2179
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2180
|
+
artifacts: flattenVerifyEvidence(initial.evidence),
|
|
2181
|
+
metadata: {
|
|
2182
|
+
mode: args.mode,
|
|
2183
|
+
step_count: args.steps.length,
|
|
2184
|
+
expect_count: args.expect.length,
|
|
2185
|
+
...initial.finalUrl !== void 0 ? { final_url: initial.finalUrl } : {}
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
if (manifestPath) result.manifest = manifestPath;
|
|
2104
2189
|
return ok(result);
|
|
2105
2190
|
});
|
|
2106
2191
|
}
|
|
2107
2192
|
};
|
|
2193
|
+
function buildVerifySummary(args, outcome) {
|
|
2194
|
+
const stepCount = args.steps.length;
|
|
2195
|
+
const expectCount = args.expect.length;
|
|
2196
|
+
if (outcome.passed) {
|
|
2197
|
+
return `${stepCount} step(s), ${expectCount} expect(s) passed`;
|
|
2198
|
+
}
|
|
2199
|
+
if (outcome.failedAtStep !== void 0) {
|
|
2200
|
+
return `failed at step ${outcome.failedAtStep}: ${outcome.failureReason ?? "unknown"}`;
|
|
2201
|
+
}
|
|
2202
|
+
return `failed: ${outcome.failureReason ?? "unknown"}`;
|
|
2203
|
+
}
|
|
2204
|
+
function flattenVerifyEvidence(ev) {
|
|
2205
|
+
const out = [];
|
|
2206
|
+
for (const s of ev.screenshots) out.push({ type: "screenshot", path: s });
|
|
2207
|
+
if (ev.replay_bundle) out.push({ type: "replay_bundle", path: ev.replay_bundle });
|
|
2208
|
+
if (ev.console) out.push({ type: "console", path: ev.console });
|
|
2209
|
+
if (ev.a11y_tree) out.push({ type: "a11y_tree", path: ev.a11y_tree });
|
|
2210
|
+
if (ev.har) out.push({ type: "har", path: ev.har });
|
|
2211
|
+
if (ev.trace) out.push({ type: "trace", path: ev.trace });
|
|
2212
|
+
if (ev.video) for (const v of ev.video) out.push({ type: "video", path: v });
|
|
2213
|
+
return out;
|
|
2214
|
+
}
|
|
2108
2215
|
function buildCaptureOptions(captures, runDir) {
|
|
2109
2216
|
const cap = {};
|
|
2110
2217
|
if (captures.has("har")) {
|
|
@@ -2518,7 +2625,7 @@ function treeHasText(tree, text) {
|
|
|
2518
2625
|
|
|
2519
2626
|
// src/cli/replay.ts
|
|
2520
2627
|
async function runReplay(bundlePath) {
|
|
2521
|
-
const abs =
|
|
2628
|
+
const abs = resolve4(bundlePath);
|
|
2522
2629
|
const raw = await readFile(abs, "utf8");
|
|
2523
2630
|
const bundle = JSON.parse(raw);
|
|
2524
2631
|
if (bundle.version !== 1) {
|
|
@@ -3042,7 +3149,11 @@ var auditA11yTool = {
|
|
|
3042
3149
|
inputShape: auditA11yShape,
|
|
3043
3150
|
build(ctx) {
|
|
3044
3151
|
return safeHandler(async (args) => {
|
|
3045
|
-
const
|
|
3152
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3153
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3154
|
+
"audit",
|
|
3155
|
+
{ skill: "audit-a11y" }
|
|
3156
|
+
);
|
|
3046
3157
|
const session = await ctx.registry.open(args.open);
|
|
3047
3158
|
const engine = ctx.registry.engineFor(session.id);
|
|
3048
3159
|
if (!(engine instanceof PlaywrightEngine)) {
|
|
@@ -3110,15 +3221,45 @@ var auditA11yTool = {
|
|
|
3110
3221
|
await ctx.registry.close(session).catch(() => void 0);
|
|
3111
3222
|
}
|
|
3112
3223
|
}
|
|
3224
|
+
const counts = countBySeverity(issues);
|
|
3225
|
+
const status = a11yStatus(counts);
|
|
3226
|
+
const artifacts = reportPath ? [{ type: "report", path: reportPath }] : [];
|
|
3227
|
+
const manifestPath = await writeManifest({
|
|
3228
|
+
runDir,
|
|
3229
|
+
skill,
|
|
3230
|
+
phase: "verify",
|
|
3231
|
+
status,
|
|
3232
|
+
summary: buildAuditSummary(args.level, counts, status),
|
|
3233
|
+
startedAt,
|
|
3234
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3235
|
+
artifacts,
|
|
3236
|
+
metadata: {
|
|
3237
|
+
level: args.level,
|
|
3238
|
+
scope: args.scope,
|
|
3239
|
+
counts,
|
|
3240
|
+
report_format: args.report_format
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3113
3243
|
return ok({
|
|
3114
3244
|
run_id: runId,
|
|
3115
|
-
counts
|
|
3245
|
+
counts,
|
|
3116
3246
|
issues,
|
|
3117
|
-
report_path: reportPath
|
|
3247
|
+
report_path: reportPath,
|
|
3248
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
3118
3249
|
});
|
|
3119
3250
|
});
|
|
3120
3251
|
}
|
|
3121
3252
|
};
|
|
3253
|
+
function a11yStatus(counts) {
|
|
3254
|
+
if ((counts.critical ?? 0) + (counts.serious ?? 0) > 0) return "fail";
|
|
3255
|
+
if ((counts.moderate ?? 0) + (counts.minor ?? 0) > 0) return "warn";
|
|
3256
|
+
return "pass";
|
|
3257
|
+
}
|
|
3258
|
+
function buildAuditSummary(level, counts, status) {
|
|
3259
|
+
const total = (counts.critical ?? 0) + (counts.serious ?? 0) + (counts.moderate ?? 0) + (counts.minor ?? 0);
|
|
3260
|
+
if (status === "pass") return `${level}: 0 issues`;
|
|
3261
|
+
return `${level}: ${total} issue(s) \u2014 critical=${counts.critical ?? 0}, serious=${counts.serious ?? 0}, moderate=${counts.moderate ?? 0}, minor=${counts.minor ?? 0}`;
|
|
3262
|
+
}
|
|
3122
3263
|
function pickWcagRef(tags) {
|
|
3123
3264
|
return tags.find((t) => /^wcag\d/.test(t));
|
|
3124
3265
|
}
|
|
@@ -3235,14 +3376,18 @@ function scoreTree(root, tokens) {
|
|
|
3235
3376
|
|
|
3236
3377
|
// src/tools/composite/scaffold_e2e.ts
|
|
3237
3378
|
import { readFile as readFile2 } from "fs/promises";
|
|
3238
|
-
import { resolve as
|
|
3379
|
+
import { resolve as resolve5 } from "path";
|
|
3239
3380
|
var scaffoldE2eTool = {
|
|
3240
3381
|
name: ToolNames.scaffoldE2e,
|
|
3241
3382
|
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.",
|
|
3242
3383
|
inputShape: scaffoldE2eShape,
|
|
3243
3384
|
build(ctx) {
|
|
3244
3385
|
return safeHandler(async (args) => {
|
|
3245
|
-
const
|
|
3386
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3387
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3388
|
+
"scaffold",
|
|
3389
|
+
{ skill: "scaffold-e2e" }
|
|
3390
|
+
);
|
|
3246
3391
|
const slug = slugify(args.scenario_nl);
|
|
3247
3392
|
const bundle = args.recorded_bundle ? await loadReplay(args.recorded_bundle) : null;
|
|
3248
3393
|
const ctxObj = { args, slug, bundle };
|
|
@@ -3280,19 +3425,35 @@ var scaffoldE2eTool = {
|
|
|
3280
3425
|
);
|
|
3281
3426
|
}
|
|
3282
3427
|
const path = await ctx.store.writeReport(runDir, filename, body);
|
|
3428
|
+
const manifestPath = await writeManifest({
|
|
3429
|
+
runDir,
|
|
3430
|
+
skill,
|
|
3431
|
+
phase: "build",
|
|
3432
|
+
status: "pass",
|
|
3433
|
+
summary: `generated ${args.framework} test "${filename}" from ${bundle ? "replay bundle" : "scenario"}`,
|
|
3434
|
+
startedAt,
|
|
3435
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3436
|
+
artifacts: [{ type: "test_file", path }],
|
|
3437
|
+
metadata: {
|
|
3438
|
+
framework: args.framework,
|
|
3439
|
+
language,
|
|
3440
|
+
from_replay_bundle: Boolean(bundle)
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
3283
3443
|
return ok({
|
|
3284
3444
|
run_id: runId,
|
|
3285
3445
|
test_file_path: path,
|
|
3286
3446
|
language,
|
|
3287
3447
|
dependencies,
|
|
3288
3448
|
setup_notes: setupNotes,
|
|
3289
|
-
from_replay_bundle: Boolean(bundle)
|
|
3449
|
+
from_replay_bundle: Boolean(bundle),
|
|
3450
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
3290
3451
|
});
|
|
3291
3452
|
});
|
|
3292
3453
|
}
|
|
3293
3454
|
};
|
|
3294
3455
|
async function loadReplay(bundlePath) {
|
|
3295
|
-
const raw = await readFile2(
|
|
3456
|
+
const raw = await readFile2(resolve5(bundlePath), "utf8");
|
|
3296
3457
|
return JSON.parse(raw);
|
|
3297
3458
|
}
|
|
3298
3459
|
function slugify(s) {
|
|
@@ -3581,7 +3742,7 @@ function indent(block, n) {
|
|
|
3581
3742
|
// src/tools/composite/visual_diff.ts
|
|
3582
3743
|
import { existsSync as existsSync2 } from "fs";
|
|
3583
3744
|
import { readFile as readFile3 } from "fs/promises";
|
|
3584
|
-
import { resolve as
|
|
3745
|
+
import { resolve as resolve6 } from "path";
|
|
3585
3746
|
import pixelmatch from "pixelmatch";
|
|
3586
3747
|
import { PNG } from "pngjs";
|
|
3587
3748
|
var visualDiffTool = {
|
|
@@ -3590,7 +3751,11 @@ var visualDiffTool = {
|
|
|
3590
3751
|
inputShape: visualDiffShape,
|
|
3591
3752
|
build(ctx) {
|
|
3592
3753
|
return safeHandler(async (args) => {
|
|
3593
|
-
const
|
|
3754
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3755
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3756
|
+
"vdiff",
|
|
3757
|
+
{ skill: "visual-diff" }
|
|
3758
|
+
);
|
|
3594
3759
|
const session = await ctx.registry.open({
|
|
3595
3760
|
...args.open,
|
|
3596
3761
|
...args.viewport ? { viewport: args.viewport } : {}
|
|
@@ -3609,7 +3774,7 @@ var visualDiffTool = {
|
|
|
3609
3774
|
);
|
|
3610
3775
|
const currentPath = await ctx.store.writeScreenshot(runDir, buf, "current");
|
|
3611
3776
|
await ctx.store.ensureDir(ctx.store.baselineDir);
|
|
3612
|
-
const baselinePath =
|
|
3777
|
+
const baselinePath = resolve6(
|
|
3613
3778
|
ctx.store.baselineDir,
|
|
3614
3779
|
`${args.baseline_id}.png`
|
|
3615
3780
|
);
|
|
@@ -3619,6 +3784,20 @@ var visualDiffTool = {
|
|
|
3619
3784
|
`${args.baseline_id}.png`,
|
|
3620
3785
|
buf
|
|
3621
3786
|
);
|
|
3787
|
+
const manifestPath2 = await writeManifest({
|
|
3788
|
+
runDir,
|
|
3789
|
+
skill,
|
|
3790
|
+
phase: "verify",
|
|
3791
|
+
status: "pass",
|
|
3792
|
+
summary: `baseline "${args.baseline_id}" seeded from current capture`,
|
|
3793
|
+
startedAt,
|
|
3794
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3795
|
+
artifacts: [
|
|
3796
|
+
{ type: "baseline", path: baselinePath },
|
|
3797
|
+
{ type: "screenshot", path: currentPath }
|
|
3798
|
+
],
|
|
3799
|
+
metadata: { baseline_id: args.baseline_id, seeded: true }
|
|
3800
|
+
});
|
|
3622
3801
|
return ok({
|
|
3623
3802
|
run_id: runId,
|
|
3624
3803
|
baseline_id: args.baseline_id,
|
|
@@ -3626,6 +3805,7 @@ var visualDiffTool = {
|
|
|
3626
3805
|
passed: true,
|
|
3627
3806
|
baseline_path: baselinePath,
|
|
3628
3807
|
current_path: currentPath,
|
|
3808
|
+
...manifestPath2 ? { manifest: manifestPath2 } : {},
|
|
3629
3809
|
note: "Baseline did not exist \u2014 current capture saved as the new baseline."
|
|
3630
3810
|
});
|
|
3631
3811
|
}
|
|
@@ -3656,21 +3836,45 @@ var visualDiffTool = {
|
|
|
3656
3836
|
);
|
|
3657
3837
|
const total = baseline.width * baseline.height;
|
|
3658
3838
|
const diffPct = diffPixels / total;
|
|
3839
|
+
const passed = diffPct <= args.threshold_pct;
|
|
3659
3840
|
const diffImagePath = await ctx.store.writeBytes(
|
|
3660
3841
|
runDir,
|
|
3661
3842
|
"diff.png",
|
|
3662
3843
|
PNG.sync.write(diff)
|
|
3663
3844
|
);
|
|
3845
|
+
const artifacts = [
|
|
3846
|
+
{ type: "baseline", path: baselinePath },
|
|
3847
|
+
{ type: "screenshot", path: currentPath },
|
|
3848
|
+
{ type: "diff", path: diffImagePath }
|
|
3849
|
+
];
|
|
3850
|
+
const manifestPath = await writeManifest({
|
|
3851
|
+
runDir,
|
|
3852
|
+
skill,
|
|
3853
|
+
phase: "verify",
|
|
3854
|
+
status: passed ? "pass" : "fail",
|
|
3855
|
+
summary: `diff ${(diffPct * 100).toFixed(3)}% vs baseline "${args.baseline_id}" (threshold ${(args.threshold_pct * 100).toFixed(3)}%)`,
|
|
3856
|
+
startedAt,
|
|
3857
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3858
|
+
artifacts,
|
|
3859
|
+
metadata: {
|
|
3860
|
+
baseline_id: args.baseline_id,
|
|
3861
|
+
diff_pct: Number(diffPct.toFixed(6)),
|
|
3862
|
+
diff_pixels: diffPixels,
|
|
3863
|
+
total_pixels: total,
|
|
3864
|
+
threshold_pct: args.threshold_pct
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3664
3867
|
return ok({
|
|
3665
3868
|
run_id: runId,
|
|
3666
3869
|
baseline_id: args.baseline_id,
|
|
3667
3870
|
diff_pct: Number(diffPct.toFixed(6)),
|
|
3668
3871
|
diff_pixels: diffPixels,
|
|
3669
3872
|
total_pixels: total,
|
|
3670
|
-
passed
|
|
3873
|
+
passed,
|
|
3671
3874
|
baseline_path: baselinePath,
|
|
3672
3875
|
current_path: currentPath,
|
|
3673
|
-
diff_image_path: diffImagePath
|
|
3876
|
+
diff_image_path: diffImagePath,
|
|
3877
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
3674
3878
|
});
|
|
3675
3879
|
} finally {
|
|
3676
3880
|
if (args.close_on_finish) {
|
|
@@ -3939,8 +4143,19 @@ var toolMetadata = {
|
|
|
3939
4143
|
|
|
3940
4144
|
// src/server.ts
|
|
3941
4145
|
var SERVER_NAME = "rolepod-uiproof";
|
|
3942
|
-
var SERVER_VERSION = "0.
|
|
4146
|
+
var SERVER_VERSION = "0.6.0";
|
|
4147
|
+
var SUPPORTED_PROTOCOL = "v1";
|
|
4148
|
+
function checkProtocolCompat() {
|
|
4149
|
+
const requested = process.env.ROLEPOD_PROTOCOL;
|
|
4150
|
+
if (!requested) return;
|
|
4151
|
+
if (requested !== SUPPORTED_PROTOCOL) {
|
|
4152
|
+
console.warn(
|
|
4153
|
+
`rolepod protocol mismatch: expected ${SUPPORTED_PROTOCOL}, got ${requested}. Manifest will still be written in ${SUPPORTED_PROTOCOL} shape \u2014 parent may not parse it correctly.`
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
3943
4157
|
function buildServer(opts = {}) {
|
|
4158
|
+
checkProtocolCompat();
|
|
3944
4159
|
const webEngine = createWebEngine();
|
|
3945
4160
|
const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
|
|
3946
4161
|
registry.register("web", webEngine);
|
|
@@ -4001,6 +4216,8 @@ function buildServer(opts = {}) {
|
|
|
4001
4216
|
}
|
|
4002
4217
|
log.info("rolepod-uiproof server built", {
|
|
4003
4218
|
version: SERVER_VERSION,
|
|
4219
|
+
protocol: SUPPORTED_PROTOCOL,
|
|
4220
|
+
mode: store.mode,
|
|
4004
4221
|
tools: tools.map((t) => t.name)
|
|
4005
4222
|
});
|
|
4006
4223
|
return {
|