@rolepod/uiproof 0.5.0 → 0.6.1

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.
@@ -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 resolve3 } from "path";
178
+ import { resolve as resolve4 } from "path";
179
179
 
180
180
  // src/artifact/ArtifactStore.ts
181
181
  import { randomUUID } from "crypto";
@@ -201,19 +201,74 @@ var log = {
201
201
  }
202
202
  };
203
203
 
204
+ // src/util/rolepodProtocol.ts
205
+ import { execSync } from "child_process";
206
+ import { existsSync as existsSync2, readFileSync } from "fs";
207
+ import { join as join2 } from "path";
208
+ function detectRolepodParent(cwd = process.cwd()) {
209
+ let gitRoot = cwd;
210
+ try {
211
+ gitRoot = execSync("git rev-parse --show-toplevel", {
212
+ cwd,
213
+ encoding: "utf8",
214
+ stdio: ["ignore", "pipe", "ignore"]
215
+ }).trim();
216
+ } catch {
217
+ }
218
+ const file = join2(gitRoot, ".rolepod", "parent-active");
219
+ if (!existsSync2(file)) {
220
+ return { active: false, protocol: null, gitRoot };
221
+ }
222
+ const protocol = readFileSync(file, "utf8").trim().split(/\r?\n/)[0] ?? null;
223
+ return { active: true, protocol, gitRoot };
224
+ }
225
+
204
226
  // src/artifact/ArtifactStore.ts
205
227
  var ArtifactStore = class {
206
228
  rootDir;
229
+ mode;
230
+ baselineRoot;
207
231
  constructor(opts = {}) {
208
- this.rootDir = opts.rootDir ?? resolve2(process.cwd(), ".rolepod-uiproof", "artifacts");
232
+ const parent = detectRolepodParent();
233
+ this.mode = opts.mode ?? (parent.active ? "with-parent" : "standalone");
234
+ if (opts.rootDir !== void 0) {
235
+ this.rootDir = opts.rootDir;
236
+ } else if (this.mode === "with-parent") {
237
+ this.rootDir = resolve2(parent.gitRoot, ".rolepod", "evidence");
238
+ } else {
239
+ this.rootDir = resolve2(process.cwd(), ".rolepod-uiproof", "artifacts");
240
+ }
241
+ this.baselineRoot = resolve2(process.cwd(), ".rolepod-uiproof", "baselines");
209
242
  }
210
- /** Allocate a fresh run id and ensure its directory exists. */
211
- async startRun(prefix = "run") {
212
- const runId = `${prefix}_${this.timestampSlug()}_${randomUUID().slice(0, 8)}`;
243
+ /**
244
+ * Allocate a fresh run dir and ensure it exists.
245
+ *
246
+ * - standalone: `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
247
+ * - with-parent: `<git-root>/.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
248
+ *
249
+ * `prefix` is preserved for back-compat with v0.5 callers; new callers
250
+ * should also pass `opts.skill` so the with-parent path can be derived
251
+ * unambiguously and the manifest can be emitted with the canonical
252
+ * skill name.
253
+ */
254
+ async startRun(prefix = "run", opts = {}) {
255
+ const ts = this.timestampSlug();
256
+ const skill = opts.skill ?? prefix;
257
+ let runId;
258
+ if (this.mode === "with-parent") {
259
+ runId = `${ts}-rolepod-uiproof-${skill}`;
260
+ } else {
261
+ runId = `${prefix}_${ts}_${randomUUID().slice(0, 8)}`;
262
+ }
213
263
  const runDir = resolve2(this.rootDir, runId);
214
264
  await mkdir(runDir, { recursive: true });
215
- log.debug("artifact run started", { run_id: runId, dir: runDir });
216
- return { runId, runDir };
265
+ log.debug("artifact run started", {
266
+ run_id: runId,
267
+ dir: runDir,
268
+ mode: this.mode,
269
+ skill
270
+ });
271
+ return { runId, runDir, skill, mode: this.mode };
217
272
  }
218
273
  async writeScreenshot(runDir, buf, name) {
219
274
  const path = resolve2(runDir, `${name}.png`);
@@ -241,7 +296,7 @@ var ArtifactStore = class {
241
296
  }
242
297
  /** Root for stored visual baselines: `./.rolepod-uiproof/baselines/`. */
243
298
  get baselineDir() {
244
- return resolve2(this.rootDir, "..", "baselines");
299
+ return this.baselineRoot;
245
300
  }
246
301
  timestampSlug() {
247
302
  const d = /* @__PURE__ */ new Date();
@@ -1170,21 +1225,21 @@ var PlaywrightEngine = class {
1170
1225
  if (s.dialogArming) {
1171
1226
  s.dialogArming.resolve(false);
1172
1227
  }
1173
- return new Promise((resolve6) => {
1228
+ return new Promise((resolve7) => {
1174
1229
  const arming = {
1175
1230
  action: opts.action,
1176
1231
  text: opts.text,
1177
1232
  expiresAt,
1178
1233
  resolve: (handled) => {
1179
1234
  s.dialogArming = null;
1180
- resolve6({ handled });
1235
+ resolve7({ handled });
1181
1236
  }
1182
1237
  };
1183
1238
  s.dialogArming = arming;
1184
1239
  const timer = setTimeout(() => {
1185
1240
  if (s.dialogArming === arming) {
1186
1241
  s.dialogArming = null;
1187
- resolve6({ handled: false });
1242
+ resolve7({ handled: false });
1188
1243
  }
1189
1244
  }, timeoutMs);
1190
1245
  timer.unref?.();
@@ -2035,6 +2090,37 @@ async function ddmin(input, reproduces) {
2035
2090
  return current;
2036
2091
  }
2037
2092
 
2093
+ // src/util/manifest.ts
2094
+ import { writeFile as writeFile2 } from "fs/promises";
2095
+ import { resolve as resolve3 } from "path";
2096
+ var ROLEPOD_PROTOCOL_VERSION = "rolepod/v1";
2097
+ async function writeManifest(input) {
2098
+ const manifest = {
2099
+ protocol: ROLEPOD_PROTOCOL_VERSION,
2100
+ plugin: "rolepod-uiproof",
2101
+ skill: input.skill,
2102
+ phase: input.phase,
2103
+ status: input.status,
2104
+ summary: input.summary,
2105
+ started_at: input.startedAt,
2106
+ finished_at: input.finishedAt,
2107
+ artifacts: input.artifacts,
2108
+ metadata: input.metadata ?? {}
2109
+ };
2110
+ const path = resolve3(input.runDir, "manifest.json");
2111
+ try {
2112
+ await writeFile2(path, JSON.stringify(manifest, null, 2), "utf8");
2113
+ return path;
2114
+ } catch (err) {
2115
+ log.warn("manifest write failed", {
2116
+ run_dir: input.runDir,
2117
+ skill: input.skill,
2118
+ err: String(err)
2119
+ });
2120
+ return void 0;
2121
+ }
2122
+ }
2123
+
2038
2124
  // src/tools/result.ts
2039
2125
  function ok(value) {
2040
2126
  return {
@@ -2076,7 +2162,11 @@ var verifyUiFlowTool = {
2076
2162
  inputShape: verifyUiFlowShape,
2077
2163
  build(ctx) {
2078
2164
  return safeHandler(async (args) => {
2079
- const { runId, runDir } = await ctx.store.startRun("verify");
2165
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2166
+ const { runId, runDir, skill } = await ctx.store.startRun(
2167
+ "verify",
2168
+ { skill: "verify-ui" }
2169
+ );
2080
2170
  const initial = await runFlow(ctx, args, args.steps, runDir, {
2081
2171
  captureEvidence: true,
2082
2172
  bundleName: "replay.json"
@@ -2101,10 +2191,49 @@ var verifyUiFlowTool = {
2101
2191
  attempts: min.attempts
2102
2192
  };
2103
2193
  }
2194
+ const manifestPath = await writeManifest({
2195
+ runDir,
2196
+ skill,
2197
+ phase: "verify",
2198
+ status: initial.passed ? "pass" : "fail",
2199
+ summary: buildVerifySummary(args, initial),
2200
+ startedAt,
2201
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2202
+ artifacts: flattenVerifyEvidence(initial.evidence),
2203
+ metadata: {
2204
+ mode: args.mode,
2205
+ step_count: args.steps.length,
2206
+ expect_count: args.expect.length,
2207
+ ...initial.finalUrl !== void 0 ? { final_url: initial.finalUrl } : {}
2208
+ }
2209
+ });
2210
+ if (manifestPath) result.manifest = manifestPath;
2104
2211
  return ok(result);
2105
2212
  });
2106
2213
  }
2107
2214
  };
2215
+ function buildVerifySummary(args, outcome) {
2216
+ const stepCount = args.steps.length;
2217
+ const expectCount = args.expect.length;
2218
+ if (outcome.passed) {
2219
+ return `${stepCount} step(s), ${expectCount} expect(s) passed`;
2220
+ }
2221
+ if (outcome.failedAtStep !== void 0) {
2222
+ return `failed at step ${outcome.failedAtStep}: ${outcome.failureReason ?? "unknown"}`;
2223
+ }
2224
+ return `failed: ${outcome.failureReason ?? "unknown"}`;
2225
+ }
2226
+ function flattenVerifyEvidence(ev) {
2227
+ const out = [];
2228
+ for (const s of ev.screenshots) out.push({ type: "screenshot", path: s });
2229
+ if (ev.replay_bundle) out.push({ type: "replay_bundle", path: ev.replay_bundle });
2230
+ if (ev.console) out.push({ type: "console", path: ev.console });
2231
+ if (ev.a11y_tree) out.push({ type: "a11y_tree", path: ev.a11y_tree });
2232
+ if (ev.har) out.push({ type: "har", path: ev.har });
2233
+ if (ev.trace) out.push({ type: "trace", path: ev.trace });
2234
+ if (ev.video) for (const v of ev.video) out.push({ type: "video", path: v });
2235
+ return out;
2236
+ }
2108
2237
  function buildCaptureOptions(captures, runDir) {
2109
2238
  const cap = {};
2110
2239
  if (captures.has("har")) {
@@ -2518,7 +2647,7 @@ function treeHasText(tree, text) {
2518
2647
 
2519
2648
  // src/cli/replay.ts
2520
2649
  async function runReplay(bundlePath) {
2521
- const abs = resolve3(bundlePath);
2650
+ const abs = resolve4(bundlePath);
2522
2651
  const raw = await readFile(abs, "utf8");
2523
2652
  const bundle = JSON.parse(raw);
2524
2653
  if (bundle.version !== 1) {
@@ -3042,7 +3171,11 @@ var auditA11yTool = {
3042
3171
  inputShape: auditA11yShape,
3043
3172
  build(ctx) {
3044
3173
  return safeHandler(async (args) => {
3045
- const { runId, runDir } = await ctx.store.startRun("audit");
3174
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3175
+ const { runId, runDir, skill } = await ctx.store.startRun(
3176
+ "audit",
3177
+ { skill: "audit-a11y" }
3178
+ );
3046
3179
  const session = await ctx.registry.open(args.open);
3047
3180
  const engine = ctx.registry.engineFor(session.id);
3048
3181
  if (!(engine instanceof PlaywrightEngine)) {
@@ -3110,15 +3243,45 @@ var auditA11yTool = {
3110
3243
  await ctx.registry.close(session).catch(() => void 0);
3111
3244
  }
3112
3245
  }
3246
+ const counts = countBySeverity(issues);
3247
+ const status = a11yStatus(counts);
3248
+ const artifacts = reportPath ? [{ type: "report", path: reportPath }] : [];
3249
+ const manifestPath = await writeManifest({
3250
+ runDir,
3251
+ skill,
3252
+ phase: "verify",
3253
+ status,
3254
+ summary: buildAuditSummary(args.level, counts, status),
3255
+ startedAt,
3256
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3257
+ artifacts,
3258
+ metadata: {
3259
+ level: args.level,
3260
+ scope: args.scope,
3261
+ counts,
3262
+ report_format: args.report_format
3263
+ }
3264
+ });
3113
3265
  return ok({
3114
3266
  run_id: runId,
3115
- counts: countBySeverity(issues),
3267
+ counts,
3116
3268
  issues,
3117
- report_path: reportPath
3269
+ report_path: reportPath,
3270
+ ...manifestPath ? { manifest: manifestPath } : {}
3118
3271
  });
3119
3272
  });
3120
3273
  }
3121
3274
  };
3275
+ function a11yStatus(counts) {
3276
+ if ((counts.critical ?? 0) + (counts.serious ?? 0) > 0) return "fail";
3277
+ if ((counts.moderate ?? 0) + (counts.minor ?? 0) > 0) return "warn";
3278
+ return "pass";
3279
+ }
3280
+ function buildAuditSummary(level, counts, status) {
3281
+ const total = (counts.critical ?? 0) + (counts.serious ?? 0) + (counts.moderate ?? 0) + (counts.minor ?? 0);
3282
+ if (status === "pass") return `${level}: 0 issues`;
3283
+ return `${level}: ${total} issue(s) \u2014 critical=${counts.critical ?? 0}, serious=${counts.serious ?? 0}, moderate=${counts.moderate ?? 0}, minor=${counts.minor ?? 0}`;
3284
+ }
3122
3285
  function pickWcagRef(tags) {
3123
3286
  return tags.find((t) => /^wcag\d/.test(t));
3124
3287
  }
@@ -3235,14 +3398,18 @@ function scoreTree(root, tokens) {
3235
3398
 
3236
3399
  // src/tools/composite/scaffold_e2e.ts
3237
3400
  import { readFile as readFile2 } from "fs/promises";
3238
- import { resolve as resolve4 } from "path";
3401
+ import { resolve as resolve5 } from "path";
3239
3402
  var scaffoldE2eTool = {
3240
3403
  name: ToolNames.scaffoldE2e,
3241
3404
  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
3405
  inputShape: scaffoldE2eShape,
3243
3406
  build(ctx) {
3244
3407
  return safeHandler(async (args) => {
3245
- const { runId, runDir } = await ctx.store.startRun("scaffold");
3408
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3409
+ const { runId, runDir, skill } = await ctx.store.startRun(
3410
+ "scaffold",
3411
+ { skill: "scaffold-e2e" }
3412
+ );
3246
3413
  const slug = slugify(args.scenario_nl);
3247
3414
  const bundle = args.recorded_bundle ? await loadReplay(args.recorded_bundle) : null;
3248
3415
  const ctxObj = { args, slug, bundle };
@@ -3280,19 +3447,35 @@ var scaffoldE2eTool = {
3280
3447
  );
3281
3448
  }
3282
3449
  const path = await ctx.store.writeReport(runDir, filename, body);
3450
+ const manifestPath = await writeManifest({
3451
+ runDir,
3452
+ skill,
3453
+ phase: "build",
3454
+ status: "pass",
3455
+ summary: `generated ${args.framework} test "${filename}" from ${bundle ? "replay bundle" : "scenario"}`,
3456
+ startedAt,
3457
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3458
+ artifacts: [{ type: "test_file", path }],
3459
+ metadata: {
3460
+ framework: args.framework,
3461
+ language,
3462
+ from_replay_bundle: Boolean(bundle)
3463
+ }
3464
+ });
3283
3465
  return ok({
3284
3466
  run_id: runId,
3285
3467
  test_file_path: path,
3286
3468
  language,
3287
3469
  dependencies,
3288
3470
  setup_notes: setupNotes,
3289
- from_replay_bundle: Boolean(bundle)
3471
+ from_replay_bundle: Boolean(bundle),
3472
+ ...manifestPath ? { manifest: manifestPath } : {}
3290
3473
  });
3291
3474
  });
3292
3475
  }
3293
3476
  };
3294
3477
  async function loadReplay(bundlePath) {
3295
- const raw = await readFile2(resolve4(bundlePath), "utf8");
3478
+ const raw = await readFile2(resolve5(bundlePath), "utf8");
3296
3479
  return JSON.parse(raw);
3297
3480
  }
3298
3481
  function slugify(s) {
@@ -3579,9 +3762,9 @@ function indent(block, n) {
3579
3762
  }
3580
3763
 
3581
3764
  // src/tools/composite/visual_diff.ts
3582
- import { existsSync as existsSync2 } from "fs";
3765
+ import { existsSync as existsSync3 } from "fs";
3583
3766
  import { readFile as readFile3 } from "fs/promises";
3584
- import { resolve as resolve5 } from "path";
3767
+ import { resolve as resolve6 } from "path";
3585
3768
  import pixelmatch from "pixelmatch";
3586
3769
  import { PNG } from "pngjs";
3587
3770
  var visualDiffTool = {
@@ -3590,7 +3773,11 @@ var visualDiffTool = {
3590
3773
  inputShape: visualDiffShape,
3591
3774
  build(ctx) {
3592
3775
  return safeHandler(async (args) => {
3593
- const { runId, runDir } = await ctx.store.startRun("vdiff");
3776
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3777
+ const { runId, runDir, skill } = await ctx.store.startRun(
3778
+ "vdiff",
3779
+ { skill: "visual-diff" }
3780
+ );
3594
3781
  const session = await ctx.registry.open({
3595
3782
  ...args.open,
3596
3783
  ...args.viewport ? { viewport: args.viewport } : {}
@@ -3609,16 +3796,30 @@ var visualDiffTool = {
3609
3796
  );
3610
3797
  const currentPath = await ctx.store.writeScreenshot(runDir, buf, "current");
3611
3798
  await ctx.store.ensureDir(ctx.store.baselineDir);
3612
- const baselinePath = resolve5(
3799
+ const baselinePath = resolve6(
3613
3800
  ctx.store.baselineDir,
3614
3801
  `${args.baseline_id}.png`
3615
3802
  );
3616
- if (!existsSync2(baselinePath)) {
3803
+ if (!existsSync3(baselinePath)) {
3617
3804
  await ctx.store.writeBytes(
3618
3805
  ctx.store.baselineDir,
3619
3806
  `${args.baseline_id}.png`,
3620
3807
  buf
3621
3808
  );
3809
+ const manifestPath2 = await writeManifest({
3810
+ runDir,
3811
+ skill,
3812
+ phase: "verify",
3813
+ status: "pass",
3814
+ summary: `baseline "${args.baseline_id}" seeded from current capture`,
3815
+ startedAt,
3816
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3817
+ artifacts: [
3818
+ { type: "baseline", path: baselinePath },
3819
+ { type: "screenshot", path: currentPath }
3820
+ ],
3821
+ metadata: { baseline_id: args.baseline_id, seeded: true }
3822
+ });
3622
3823
  return ok({
3623
3824
  run_id: runId,
3624
3825
  baseline_id: args.baseline_id,
@@ -3626,6 +3827,7 @@ var visualDiffTool = {
3626
3827
  passed: true,
3627
3828
  baseline_path: baselinePath,
3628
3829
  current_path: currentPath,
3830
+ ...manifestPath2 ? { manifest: manifestPath2 } : {},
3629
3831
  note: "Baseline did not exist \u2014 current capture saved as the new baseline."
3630
3832
  });
3631
3833
  }
@@ -3656,21 +3858,45 @@ var visualDiffTool = {
3656
3858
  );
3657
3859
  const total = baseline.width * baseline.height;
3658
3860
  const diffPct = diffPixels / total;
3861
+ const passed = diffPct <= args.threshold_pct;
3659
3862
  const diffImagePath = await ctx.store.writeBytes(
3660
3863
  runDir,
3661
3864
  "diff.png",
3662
3865
  PNG.sync.write(diff)
3663
3866
  );
3867
+ const artifacts = [
3868
+ { type: "baseline", path: baselinePath },
3869
+ { type: "screenshot", path: currentPath },
3870
+ { type: "diff", path: diffImagePath }
3871
+ ];
3872
+ const manifestPath = await writeManifest({
3873
+ runDir,
3874
+ skill,
3875
+ phase: "verify",
3876
+ status: passed ? "pass" : "fail",
3877
+ summary: `diff ${(diffPct * 100).toFixed(3)}% vs baseline "${args.baseline_id}" (threshold ${(args.threshold_pct * 100).toFixed(3)}%)`,
3878
+ startedAt,
3879
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3880
+ artifacts,
3881
+ metadata: {
3882
+ baseline_id: args.baseline_id,
3883
+ diff_pct: Number(diffPct.toFixed(6)),
3884
+ diff_pixels: diffPixels,
3885
+ total_pixels: total,
3886
+ threshold_pct: args.threshold_pct
3887
+ }
3888
+ });
3664
3889
  return ok({
3665
3890
  run_id: runId,
3666
3891
  baseline_id: args.baseline_id,
3667
3892
  diff_pct: Number(diffPct.toFixed(6)),
3668
3893
  diff_pixels: diffPixels,
3669
3894
  total_pixels: total,
3670
- passed: diffPct <= args.threshold_pct,
3895
+ passed,
3671
3896
  baseline_path: baselinePath,
3672
3897
  current_path: currentPath,
3673
- diff_image_path: diffImagePath
3898
+ diff_image_path: diffImagePath,
3899
+ ...manifestPath ? { manifest: manifestPath } : {}
3674
3900
  });
3675
3901
  } finally {
3676
3902
  if (args.close_on_finish) {
@@ -3939,8 +4165,19 @@ var toolMetadata = {
3939
4165
 
3940
4166
  // src/server.ts
3941
4167
  var SERVER_NAME = "rolepod-uiproof";
3942
- var SERVER_VERSION = "0.5.0";
4168
+ var SERVER_VERSION = "0.6.1";
4169
+ var SUPPORTED_PROTOCOL = "v1";
4170
+ function checkProtocolCompat() {
4171
+ const parent = detectRolepodParent();
4172
+ if (!parent.active || !parent.protocol) return;
4173
+ if (parent.protocol !== SUPPORTED_PROTOCOL) {
4174
+ console.warn(
4175
+ `rolepod protocol mismatch: expected ${SUPPORTED_PROTOCOL}, got ${parent.protocol}. Manifest will still be written in ${SUPPORTED_PROTOCOL} shape \u2014 parent may not parse it correctly.`
4176
+ );
4177
+ }
4178
+ }
3943
4179
  function buildServer(opts = {}) {
4180
+ checkProtocolCompat();
3944
4181
  const webEngine = createWebEngine();
3945
4182
  const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
3946
4183
  registry.register("web", webEngine);
@@ -4001,6 +4238,8 @@ function buildServer(opts = {}) {
4001
4238
  }
4002
4239
  log.info("rolepod-uiproof server built", {
4003
4240
  version: SERVER_VERSION,
4241
+ protocol: SUPPORTED_PROTOCOL,
4242
+ mode: store.mode,
4004
4243
  tools: tools.map((t) => t.name)
4005
4244
  });
4006
4245
  return {