@jevitate/cli 0.9.0 → 0.11.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.
Files changed (190) hide show
  1. package/dist/anchor-baselines.d.ts +26 -0
  2. package/dist/anchor-baselines.js +25 -0
  3. package/dist/anchor-baselines.js.map +1 -0
  4. package/dist/approval-provenance.d.ts +32 -2
  5. package/dist/approval-provenance.js +92 -5
  6. package/dist/approval-provenance.js.map +1 -1
  7. package/dist/approved-demo.d.ts +86 -0
  8. package/dist/approved-demo.js +184 -0
  9. package/dist/approved-demo.js.map +1 -0
  10. package/dist/bin.js +13352 -7050
  11. package/dist/build-info.generated.js +2 -2
  12. package/dist/campaign-api.d.ts +15 -0
  13. package/dist/campaign-api.js +67 -6
  14. package/dist/campaign-api.js.map +1 -1
  15. package/dist/campaign-cli.js +7 -2
  16. package/dist/campaign-cli.js.map +1 -1
  17. package/dist/campaign-preflight.d.ts +76 -0
  18. package/dist/campaign-preflight.js +117 -0
  19. package/dist/campaign-preflight.js.map +1 -0
  20. package/dist/catalog-analysis.js +14 -0
  21. package/dist/catalog-analysis.js.map +1 -1
  22. package/dist/catalog-api.js +12 -4
  23. package/dist/catalog-api.js.map +1 -1
  24. package/dist/catalog-bundle-api.d.ts +83 -0
  25. package/dist/catalog-bundle-api.js +309 -0
  26. package/dist/catalog-bundle-api.js.map +1 -0
  27. package/dist/catalog-bundle-findings.d.ts +64 -0
  28. package/dist/catalog-bundle-findings.js +326 -0
  29. package/dist/catalog-bundle-findings.js.map +1 -0
  30. package/dist/catalog-bundle.d.ts +296 -0
  31. package/dist/catalog-bundle.js +777 -0
  32. package/dist/catalog-bundle.js.map +1 -0
  33. package/dist/catalog-cli.d.ts +6 -0
  34. package/dist/catalog-cli.js +181 -5
  35. package/dist/catalog-cli.js.map +1 -1
  36. package/dist/catalog-refs.d.ts +42 -0
  37. package/dist/catalog-refs.js +188 -0
  38. package/dist/catalog-refs.js.map +1 -0
  39. package/dist/catalog-review.d.ts +3 -1
  40. package/dist/catalog-review.js +61 -0
  41. package/dist/catalog-review.js.map +1 -1
  42. package/dist/catalog.d.ts +27 -3
  43. package/dist/catalog.js +46 -2
  44. package/dist/catalog.js.map +1 -1
  45. package/dist/check-api.js +180 -9
  46. package/dist/check-api.js.map +1 -1
  47. package/dist/check-cli.d.ts +2 -0
  48. package/dist/check-cli.js +96 -2
  49. package/dist/check-cli.js.map +1 -1
  50. package/dist/check-execute.d.ts +18 -0
  51. package/dist/check-execute.js +56 -3
  52. package/dist/check-execute.js.map +1 -1
  53. package/dist/check-types.d.ts +105 -0
  54. package/dist/check-types.js.map +1 -1
  55. package/dist/cli-output.d.ts +6 -0
  56. package/dist/cli-output.js +38 -1
  57. package/dist/cli-output.js.map +1 -1
  58. package/dist/cli-shared.d.ts +10 -0
  59. package/dist/cli-shared.js +0 -4
  60. package/dist/cli-shared.js.map +1 -1
  61. package/dist/cold-start-report.d.ts +32 -0
  62. package/dist/cold-start-report.js +31 -0
  63. package/dist/cold-start-report.js.map +1 -0
  64. package/dist/defect-evidence.js +10 -5
  65. package/dist/defect-evidence.js.map +1 -1
  66. package/dist/demo-aspect-api.d.ts +11 -0
  67. package/dist/demo-aspect-api.js +27 -2
  68. package/dist/demo-aspect-api.js.map +1 -1
  69. package/dist/demo-aspect-cli.js +5 -0
  70. package/dist/demo-aspect-cli.js.map +1 -1
  71. package/dist/demo-capture.d.ts +79 -2
  72. package/dist/demo-capture.js +504 -12
  73. package/dist/demo-capture.js.map +1 -1
  74. package/dist/environments.d.ts +8 -1
  75. package/dist/environments.js +14 -3
  76. package/dist/environments.js.map +1 -1
  77. package/dist/exit-codes.js +16 -0
  78. package/dist/exit-codes.js.map +1 -1
  79. package/dist/explore-adversarial.js +2 -2
  80. package/dist/explore-adversarial.js.map +1 -1
  81. package/dist/explore-shared.d.ts +11 -0
  82. package/dist/explore-shared.js +9 -0
  83. package/dist/explore-shared.js.map +1 -1
  84. package/dist/findings-filing.js +7 -3
  85. package/dist/findings-filing.js.map +1 -1
  86. package/dist/forge-verify.d.ts +224 -0
  87. package/dist/forge-verify.js +649 -0
  88. package/dist/forge-verify.js.map +1 -0
  89. package/dist/github-issue-filer.js +15 -9
  90. package/dist/github-issue-filer.js.map +1 -1
  91. package/dist/init-codeowners.d.ts +19 -0
  92. package/dist/init-codeowners.js +66 -0
  93. package/dist/init-codeowners.js.map +1 -1
  94. package/dist/job-draft-api.d.ts +52 -0
  95. package/dist/job-draft-api.js +110 -0
  96. package/dist/job-draft-api.js.map +1 -0
  97. package/dist/journeeze-api.d.ts +77 -0
  98. package/dist/journeeze-api.js +371 -0
  99. package/dist/journeeze-api.js.map +1 -0
  100. package/dist/journeeze-connect.d.ts +156 -0
  101. package/dist/journeeze-connect.js +357 -0
  102. package/dist/journeeze-connect.js.map +1 -0
  103. package/dist/journey-anchor-api.d.ts +37 -0
  104. package/dist/journey-anchor-api.js +58 -0
  105. package/dist/journey-anchor-api.js.map +1 -0
  106. package/dist/journey-anchor-cli.d.ts +17 -0
  107. package/dist/journey-anchor-cli.js +104 -0
  108. package/dist/journey-anchor-cli.js.map +1 -0
  109. package/dist/journey-anchor-gate.d.ts +52 -0
  110. package/dist/journey-anchor-gate.js +101 -0
  111. package/dist/journey-anchor-gate.js.map +1 -0
  112. package/dist/journey-api.d.ts +30 -1
  113. package/dist/journey-api.js +118 -16
  114. package/dist/journey-api.js.map +1 -1
  115. package/dist/journey-cli.js +93 -10
  116. package/dist/journey-cli.js.map +1 -1
  117. package/dist/journey-demo-api.d.ts +29 -0
  118. package/dist/journey-demo-api.js +29 -7
  119. package/dist/journey-demo-api.js.map +1 -1
  120. package/dist/journey-heal.js +1 -1
  121. package/dist/journey-heal.js.map +1 -1
  122. package/dist/journey-lint-sarif.d.ts +3 -2
  123. package/dist/journey-lint-sarif.js +11 -2
  124. package/dist/journey-lint-sarif.js.map +1 -1
  125. package/dist/journey-migrate-api.d.ts +54 -0
  126. package/dist/journey-migrate-api.js +64 -0
  127. package/dist/journey-migrate-api.js.map +1 -0
  128. package/dist/journey-network-checks.d.ts +11 -0
  129. package/dist/journey-network-checks.js +36 -13
  130. package/dist/journey-network-checks.js.map +1 -1
  131. package/dist/journey-proposal-store.d.ts +9 -0
  132. package/dist/journey-proposal-store.js +23 -0
  133. package/dist/journey-proposal-store.js.map +1 -1
  134. package/dist/journey-result-record.d.ts +2 -0
  135. package/dist/journey-result-record.js +5 -0
  136. package/dist/journey-result-record.js.map +1 -1
  137. package/dist/journey-review-api.js +9 -1
  138. package/dist/journey-review-api.js.map +1 -1
  139. package/dist/journey-review.d.ts +11 -1
  140. package/dist/journey-review.js +77 -5
  141. package/dist/journey-review.js.map +1 -1
  142. package/dist/journey-stale-api.d.ts +40 -0
  143. package/dist/journey-stale-api.js +57 -0
  144. package/dist/journey-stale-api.js.map +1 -0
  145. package/dist/load-api.js +4 -2
  146. package/dist/load-api.js.map +1 -1
  147. package/dist/locator-health-api.d.ts +101 -0
  148. package/dist/locator-health-api.js +203 -0
  149. package/dist/locator-health-api.js.map +1 -0
  150. package/dist/locator-health.d.ts +195 -0
  151. package/dist/locator-health.js +419 -0
  152. package/dist/locator-health.js.map +1 -0
  153. package/dist/mcp-api.d.ts +3 -1
  154. package/dist/mcp-api.js +21 -2
  155. package/dist/mcp-api.js.map +1 -1
  156. package/dist/mcp-cli-tools.d.ts +4 -1
  157. package/dist/mcp-cli-tools.js +82 -10
  158. package/dist/mcp-cli-tools.js.map +1 -1
  159. package/dist/openrouter-call.d.ts +23 -10
  160. package/dist/openrouter-call.js +14 -11
  161. package/dist/openrouter-call.js.map +1 -1
  162. package/dist/png-pixels.d.ts +12 -0
  163. package/dist/png-pixels.js +96 -0
  164. package/dist/png-pixels.js.map +1 -0
  165. package/dist/project-config.d.ts +29 -0
  166. package/dist/project-config.js +76 -0
  167. package/dist/project-config.js.map +1 -0
  168. package/dist/regression-api.js +2 -1
  169. package/dist/regression-api.js.map +1 -1
  170. package/dist/server-log-verify.js +2 -1
  171. package/dist/server-log-verify.js.map +1 -1
  172. package/dist/signal-triage.js +7 -3
  173. package/dist/signal-triage.js.map +1 -1
  174. package/dist/source-run-api.js +2 -2
  175. package/dist/source-run-api.js.map +1 -1
  176. package/dist/verify-fix-api.d.ts +29 -0
  177. package/dist/verify-fix-api.js +43 -2
  178. package/dist/verify-fix-api.js.map +1 -1
  179. package/dist/verify-fix-cli.js +4 -0
  180. package/dist/verify-fix-cli.js.map +1 -1
  181. package/dist/zip-store.d.ts +15 -0
  182. package/dist/zip-store.js +93 -0
  183. package/dist/zip-store.js.map +1 -0
  184. package/package.json +12 -10
  185. package/skills/jevitate-ci-check/SKILL.md +16 -0
  186. package/skills/jevitate-demo/SKILL.md +4 -1
  187. package/skills/jevitate-getting-started/SKILL.md +7 -2
  188. package/skills/jevitate-run-journey/SKILL.md +15 -0
  189. package/skills/jevitate-test-campaign/SKILL.md +26 -0
  190. package/skills/jevitate-verify-fix/SKILL.md +5 -0
@@ -1,7 +1,8 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { rm } from "node:fs/promises";
2
+ import { readFile, rm } from "node:fs/promises";
3
3
  import { REVEALED_SECRET_SELECTORS, REVEALED_SECRET_SHAPES, revealedSecretsIn, secretForms } from "@jevitate/ai-core";
4
- import { DEMO_OVERLAY_HIDE_STYLE, hideDemoOverlayForCapture } from "@jevitate/explore";
4
+ import { DEMO_OVERLAY_ATTR, DEMO_OVERLAY_HIDE_STYLE, hideDemoOverlayForCapture } from "@jevitate/explore";
5
+ import { decodePng } from "./png-pixels.js";
5
6
  import { descriptorToLocator } from "@jevitate/recorder";
6
7
  import { clock } from "@jevitate/domain";
7
8
  /**
@@ -52,7 +53,7 @@ export async function captureStepScreenshot(page, path, ctx, layers = [], clip)
52
53
  }
53
54
  try {
54
55
  for (const layer of layers)
55
- await layer.confirm?.(page, ctx);
56
+ await layer.confirm?.(page, ctx, { path, ...(clip === undefined ? {} : { clip }) });
56
57
  }
57
58
  catch (err) {
58
59
  await rm(path, { force: true });
@@ -124,8 +125,14 @@ const MASK_RUNTIME = String.raw `((cfg) => {
124
125
  };
125
126
  const SKIP = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "HEAD", "TITLE", "META", "LINK"]);
126
127
  const PAD = 2;
128
+ // Captured at install (an init script runs before any page script), so a page that later replaces
129
+ // requestAnimationFrame cannot stop the mask from re-measuring before each paint.
130
+ const raf = window.requestAnimationFrame.bind(window);
127
131
  const CSS = ":host{all:initial}*{pointer-events:none !important;box-sizing:border-box}[hidden]{display:none !important}" +
128
132
  ".m{position:fixed;background:" + cfg.fill + ";border-radius:2px}" +
133
+ ".jm{position:fixed;background:" + cfg.jzFill.mask + "}" +
134
+ ".jp{position:fixed;background:" + cfg.jzFill.placeholder + "}" +
135
+ ".jb{position:fixed;background:" + cfg.jzFill.block + "}" +
129
136
  ".h{position:fixed;border:3px solid #f59e0b;border-radius:6px;box-shadow:0 0 0 4px rgba(245,158,11,.35)}";
130
137
  let host = null;
131
138
  let layer = null;
@@ -244,6 +251,178 @@ const MASK_RUNTIME = String.raw `((cfg) => {
244
251
  }
245
252
  return out;
246
253
  };
254
+ // ── #471: jz-mask-v1 (journeeze-saas docs/contract/journey-import-v1.md "Recording references and
255
+ // masking"), top document only — every frame below it is an iframe/frame the policy replaces whole.
256
+ // - masked: input, textarea, select, every editable element (contenteditable in any spelling, or
257
+ // inherited; designMode) and [data-jz-mask], each with its subtree;
258
+ // - placeholder: iframe, frame, object, embed, video, audio, canvas;
259
+ // - left out: [data-jz-block] and its subtree (an empty box of its size).
260
+ // A region is its element's box; a descendant painting outside that box gets a box of its own, so
261
+ // "with its subtree" holds for overflowing content too. Content is never read: only geometry.
262
+ const JZ = cfg.jz === true && window === window.top;
263
+ const LATE = document.readyState !== "loading";
264
+ const JZ_FIELDS = new Set(["input", "textarea", "select"]);
265
+ const JZ_PLACEHOLDERS = new Set(["iframe", "frame", "object", "embed", "video", "audio", "canvas"]);
266
+ const JZ_SKIP = new Set(["script", "style", "noscript", "template", "head", "title", "meta", "link"]);
267
+ const JZ_MAX_NODES = 50000;
268
+ const JZ_CLASS = { mask: "jm", placeholder: "jp", block: "jb" };
269
+ let jzTargets = [];
270
+ let jzTopLayer = [];
271
+ let jzDrawn = [];
272
+ let jzTooBig = false;
273
+ let jzPseudo = false;
274
+ let jzFirst = true;
275
+ let breach = "";
276
+ const own = (el) => el === host || (el.hasAttribute && (el.hasAttribute(cfg.attr) || el.hasAttribute(cfg.overlayAttr)));
277
+ const jzCollect = () => {
278
+ const out = [];
279
+ const top = [];
280
+ let nodes = 0;
281
+ jzTooBig = false;
282
+ jzPseudo = false;
283
+ // A positioned ::before/::after can paint generated content outside every box measured here.
284
+ const escapes = (el) => {
285
+ for (const pseudo of ["::before", "::after"]) {
286
+ const cs = getComputedStyle(el, pseudo);
287
+ if (cs.content !== "none" && cs.content !== "normal" && (cs.position === "absolute" || cs.position === "fixed")) return true;
288
+ }
289
+ return false;
290
+ };
291
+ // Every descendant (elements, text, open shadow trees) of a region root, as overflow candidates.
292
+ const subtree = (rootEl, kind) => {
293
+ out.push({ kind, el: rootEl });
294
+ // A placeholder's fallback children are never rendered: only masked and left-out subtrees paint.
295
+ const painted = kind !== "placeholder";
296
+ if (painted && escapes(rootEl)) jzPseudo = true;
297
+ const stack = [rootEl];
298
+ if (rootEl.shadowRoot) stack.push(rootEl.shadowRoot);
299
+ while (stack.length > 0) {
300
+ const n = stack.pop();
301
+ for (let c = n.firstChild; c !== null; c = c.nextSibling) {
302
+ if (++nodes > JZ_MAX_NODES) { jzTooBig = true; return; }
303
+ if (c.nodeType === 3) { if (c.nodeValue && c.nodeValue.trim() !== "") out.push({ kind, node: c, within: rootEl }); continue; }
304
+ if (c.nodeType !== 1 || own(c)) continue;
305
+ out.push({ kind, el: c, within: rootEl });
306
+ // A modal dialog or popover inside a region leaves its box for the top layer.
307
+ if (c.localName === "dialog" || c.hasAttribute("popover")) top.push(c);
308
+ if (painted && escapes(c)) jzPseudo = true;
309
+ stack.push(c);
310
+ if (c.shadowRoot) stack.push(c.shadowRoot);
311
+ }
312
+ }
313
+ };
314
+ if (document.designMode === "on" && document.documentElement) subtree(document.documentElement, "mask");
315
+ else {
316
+ const stack = [document];
317
+ while (stack.length > 0 && !jzTooBig) {
318
+ const n = stack.pop();
319
+ for (let c = n.firstChild; c !== null && !jzTooBig; c = c.nextSibling) {
320
+ if (c.nodeType !== 1) continue;
321
+ if (++nodes > JZ_MAX_NODES) { jzTooBig = true; break; }
322
+ if (own(c)) continue;
323
+ const name = (c.localName || "").toLowerCase();
324
+ if (JZ_SKIP.has(name)) continue;
325
+ if (name === "dialog" || c.hasAttribute("popover")) top.push(c);
326
+ if (c.hasAttribute("data-jz-block")) { subtree(c, "block"); continue; }
327
+ if (c.hasAttribute("data-jz-mask") || JZ_FIELDS.has(name) || c.isContentEditable === true) { subtree(c, "mask"); continue; }
328
+ if (JZ_PLACEHOLDERS.has(name)) { subtree(c, "placeholder"); continue; }
329
+ stack.push(c);
330
+ if (c.shadowRoot) stack.push(c.shadowRoot);
331
+ }
332
+ }
333
+ }
334
+ jzTopLayer = top;
335
+ return out;
336
+ };
337
+ const covers = (b, r) => b.left <= r.left + 0.5 && b.top <= r.top + 0.5 && b.right >= r.right - 0.5 && b.bottom >= r.bottom - 0.5;
338
+ const nodeRects = (node) => {
339
+ if (!node.isConnected) return null;
340
+ const r = document.createRange();
341
+ try { r.selectNodeContents(node); } catch (e) { return null; }
342
+ return Array.from(r.getClientRects());
343
+ };
344
+ // This frame's jz regions: [{ r, kind }] (a target that left the document has no box).
345
+ const jzRegions = () => {
346
+ const out = [];
347
+ const rootBox = new Map();
348
+ for (const t of jzTargets) {
349
+ let rs;
350
+ if (t.node !== undefined) rs = nodeRects(t.node);
351
+ else rs = t.el.isConnected ? [t.el.getBoundingClientRect()] : null;
352
+ if (rs === null) continue;
353
+ let within = null;
354
+ if (t.within !== undefined) {
355
+ within = rootBox.get(t.within);
356
+ if (within === undefined) { within = t.within.isConnected ? t.within.getBoundingClientRect() : null; rootBox.set(t.within, within); }
357
+ }
358
+ for (const r of rs) {
359
+ if (!(r.width > 0 || r.height > 0)) continue;
360
+ if (within !== null && within !== undefined && covers(within, r)) continue;
361
+ out.push({ r, kind: t.kind });
362
+ }
363
+ }
364
+ return out;
365
+ };
366
+ const topLayerOpen = () => {
367
+ if (document.fullscreenElement) return true;
368
+ for (const el of jzTopLayer) {
369
+ try { if (el.isConnected && el.matches(":modal, :popover-open")) return true; } catch (e) { return true; }
370
+ }
371
+ return false;
372
+ };
373
+ // An animation on a region, an ancestor or a descendant may move it on the compositor between two
374
+ // main-thread frames: the video cannot be proven for that stretch. One that only changes paint
375
+ // (a colour, an outline, a shadow, opacity) moves nothing and is ignored.
376
+ const PAINT_ONLY = /^(color|background-color|border(-top|-right|-bottom|-left)?-color|outline(-color|-offset|-width|-style)?|box-shadow|opacity|text-decoration-color|fill|stroke|caret-color|accent-color|visibility)$/;
377
+ const kebab = (k) => k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
378
+ const movesGeometry = (a) => {
379
+ try {
380
+ if (typeof a.transitionProperty === "string") return !PAINT_ONLY.test(a.transitionProperty);
381
+ const frames = a.effect && typeof a.effect.getKeyframes === "function" ? a.effect.getKeyframes() : null;
382
+ if (frames === null) return true;
383
+ for (const f of frames) for (const k of Object.keys(f)) {
384
+ if (k === "offset" || k === "computedOffset" || k === "easing" || k === "composite") continue;
385
+ if (!PAINT_ONLY.test(kebab(k))) return true;
386
+ }
387
+ return false;
388
+ } catch (e) { return true; }
389
+ };
390
+ const animatedRegion = () => {
391
+ if (typeof document.getAnimations !== "function") return false;
392
+ const els = jzTargets.filter((t) => t.el !== undefined && t.within === undefined).map((t) => t.el);
393
+ if (els.length === 0) return false;
394
+ for (const a of document.getAnimations()) {
395
+ if (a.playState !== "running" || !movesGeometry(a)) continue;
396
+ const tg = a.effect && a.effect.target;
397
+ if (!tg) continue;
398
+ for (const el of els) if (tg === el || (tg.contains && tg.contains(el)) || (el.contains && el.contains(tg))) return true;
399
+ }
400
+ return false;
401
+ };
402
+ const report = (reason) => {
403
+ if (breach !== "") return;
404
+ breach = String(reason).slice(0, 200);
405
+ try { const f = window[cfg.report]; if (typeof f === "function") f(breach); } catch (e) { /* the sticky breach is read at the next verify */ }
406
+ };
407
+ const jzWatch = () => {
408
+ if (!JZ) return;
409
+ if (jzFirst) {
410
+ jzFirst = false;
411
+ if (LATE && jzDrawn.length > 0) report("the jz-mask-v1 layer was installed after the page had painted");
412
+ }
413
+ if (jzTooBig) report("the page is too large to prove jz-mask-v1");
414
+ if (jzPseudo) report("a positioned ::before/::after in a jz-mask-v1 region can paint outside it");
415
+ if (jzDrawn.length > 0 && topLayerOpen()) report("a top-layer element (modal dialog, popover or fullscreen) could paint over jz-mask-v1 regions");
416
+ if (jzDrawn.length > 0 && animatedRegion()) report("an animation moved a jz-mask-v1 region");
417
+ if (lastError !== "") report("the mask loop failed");
418
+ };
419
+ if (JZ) {
420
+ window.addEventListener("scroll", (e) => {
421
+ if (jzDrawn.length === 0) return;
422
+ const t = e.target === document || e.target === window ? document.scrollingElement : e.target;
423
+ try { if (t && getComputedStyle(t).scrollBehavior === "smooth") report("a smooth scroll can move jz-mask-v1 regions on the compositor"); } catch (e2) { report("a scroll could not be checked"); }
424
+ }, true);
425
+ }
247
426
  const rectsOf = (t) => {
248
427
  if (t.kind === "range") {
249
428
  if (!t.node.isConnected) return null;
@@ -279,7 +458,15 @@ const MASK_RUNTIME = String.raw `((cfg) => {
279
458
  if (rs === null) { dirty = true; continue; }
280
459
  for (const r of rs) if (r.width > 0 || r.height > 0) rects.push(r);
281
460
  }
282
- while (boxes.length < rects.length) {
461
+ let jz = [];
462
+ if (JZ) {
463
+ // Re-collected on EVERY frame, not only on a mutation: a field can appear where no observer
464
+ // sees it (inside a shadow root attached later, an element upgraded by customElements.define).
465
+ jzTargets = jzCollect();
466
+ jz = jzRegions();
467
+ }
468
+ const all = rects.map((r) => ({ r, cls: "m" })).concat(jz.map((x) => ({ r: x.r, cls: JZ_CLASS[x.kind] })));
469
+ while (boxes.length < all.length) {
283
470
  const d = document.createElement("div");
284
471
  d.className = "m";
285
472
  layer.appendChild(d);
@@ -287,9 +474,14 @@ const MASK_RUNTIME = String.raw `((cfg) => {
287
474
  }
288
475
  for (let i = 0; i < boxes.length; i++) {
289
476
  const d = boxes[i];
290
- if (i < rects.length) { place(d, rects[i], PAD); d.hidden = false; } else d.hidden = true;
477
+ if (i < all.length) {
478
+ if (d.className !== all[i].cls) d.className = all[i].cls;
479
+ place(d, all[i].r, PAD);
480
+ d.hidden = false;
481
+ } else d.hidden = true;
291
482
  }
292
483
  drawn = rects;
484
+ jzDrawn = jz;
293
485
  if (highlightEl !== null && highlightEl.isConnected) {
294
486
  place(hiBox, highlightEl.getBoundingClientRect(), 5);
295
487
  hiBox.hidden = false;
@@ -297,7 +489,8 @@ const MASK_RUNTIME = String.raw `((cfg) => {
297
489
  };
298
490
  const tick = () => {
299
491
  try { update(); lastError = ""; } catch (e) { lastError = String((e && e.message) || e).slice(0, 200); }
300
- requestAnimationFrame(tick);
492
+ try { jzWatch(); } catch (e) { report("the jz-mask-v1 watch failed"); }
493
+ raf(tick);
301
494
  };
302
495
  const mo = new MutationObserver((records) => {
303
496
  for (const r of records) {
@@ -312,12 +505,12 @@ const MASK_RUNTIME = String.raw `((cfg) => {
312
505
  const mark = () => { dirty = true; };
313
506
  window.addEventListener("input", mark, true);
314
507
  window.addEventListener("change", mark, true);
315
- const covers = (b, r) => b.left <= r.left + 0.5 && b.top <= r.top + 0.5 && b.right >= r.right - 0.5 && b.bottom >= r.bottom - 0.5;
316
508
  const api = {
317
509
  verify() {
318
510
  dirty = true;
319
511
  try { update(); } catch (e) {
320
- return { ok: false, occurrences: 0, masked: 0, reason: "the mask update failed: " + String((e && e.message) || e).slice(0, 200), rects: [] };
512
+ return { ok: false, occurrences: 0, masked: 0, reason: "the mask update failed: " + String((e && e.message) || e).slice(0, 200), rects: [],
513
+ ...(JZ ? { jz: { ok: false, regions: 0, rects: [], reason: "the mask update failed", breach: breach || "the mask update failed", viewport: { width: window.innerWidth, height: window.innerHeight } } } : {}) };
321
514
  }
322
515
  const problems = [];
323
516
  {
@@ -338,6 +531,24 @@ const MASK_RUNTIME = String.raw `((cfg) => {
338
531
  if (lastError !== "") problems.push("the mask loop failed: " + lastError);
339
532
  const out = { ok: problems.length === 0, occurrences: drawn.length, masked, rects: drawn.map((r) => ({ x: r.left, y: r.top, width: r.width, height: r.height })) };
340
533
  if (problems.length > 0) out.reason = problems.join("; ");
534
+ if (JZ) {
535
+ const jp = problems.filter((p) => p.indexOf("mask layer") >= 0);
536
+ if (jzTooBig) jp.push("the page is too large to prove jz-mask-v1");
537
+ if (jzPseudo) jp.push("a positioned ::before/::after in a jz-mask-v1 region can paint outside it");
538
+ if (jzDrawn.length > 0 && topLayerOpen()) jp.push("a top-layer element (modal dialog, popover or fullscreen) could paint over jz-mask-v1 regions");
539
+ let jzMasked = 0;
540
+ for (const x of jzDrawn) if (live.some((b) => covers(b, x.r))) jzMasked++;
541
+ if (jzMasked < jzDrawn.length) jp.push((jzDrawn.length - jzMasked) + " jz-mask-v1 region(s) not covered by a box");
542
+ if (lastError !== "") jp.push("the mask loop failed");
543
+ out.jz = {
544
+ ok: jp.length === 0,
545
+ regions: jzDrawn.length,
546
+ rects: jzDrawn.map((x) => ({ x: x.r.left, y: x.r.top, width: x.r.width, height: x.r.height, kind: x.kind })),
547
+ viewport: { width: window.innerWidth, height: window.innerHeight },
548
+ breach,
549
+ };
550
+ if (jp.length > 0) out.jz.reason = jp.join("; ");
551
+ }
341
552
  return out;
342
553
  },
343
554
  learn(values) { if (Array.isArray(values)) for (const v of values) learn(v); dirty = true; return true; },
@@ -348,9 +559,19 @@ const MASK_RUNTIME = String.raw `((cfg) => {
348
559
  clearHighlight() { highlightEl = null; try { update(); } catch (e) { /* presentation only */ } return true; },
349
560
  };
350
561
  Object.defineProperty(window, cfg.name, { value: Object.freeze(api), enumerable: false, configurable: false, writable: false });
351
- requestAnimationFrame(tick);
562
+ raf(tick);
352
563
  return true;
353
564
  })(__CFG__)`;
565
+ /** #471: the jz-mask-v1 fills — masked, placeholder, left out (opaque, never a page colour by intent). */
566
+ export const JZ_MASK_FILLS = { mask: "#4B5563", placeholder: "#9CA3AF", block: "#E5E7EB" };
567
+ /** #471: a jz-mask-v1 capture could not be proven: that screenshot is left out (and the demo's video with it). */
568
+ export class JzMaskUnprovenError extends Error {
569
+ code = "E_JZ_MASK";
570
+ constructor(message) {
571
+ super(message);
572
+ this.name = "JzMaskUnprovenError";
573
+ }
574
+ }
354
575
  /** Bound on one mask round-trip (ms). */
355
576
  const MASK_CALL_MS = 5_000;
356
577
  async function within(p, what) {
@@ -393,9 +614,14 @@ export class SecretPixelMask {
393
614
  * element, a credential-shaped value) is masked, so every capture installs and proves the mask.
394
615
  */
395
616
  active = true;
396
- constructor(secrets) {
617
+ /** #471: the mask also enforces jz-mask-v1 on the top document (fields, editables, embeds, marked regions). */
618
+ jzMaskV1;
619
+ /** #471: frames of a top document the jz layer could not prove (reported live by the page). */
620
+ #jzBreaches = new Set();
621
+ constructor(secrets, opts = {}) {
397
622
  const forms = [...new Set(secrets.filter((s) => typeof s === "string" && s.trim() !== "").flatMap((s) => [...secretForms(s)]))];
398
623
  this.#name = `__jevitateMask_${randomBytes(8).toString("hex")}`;
624
+ this.jzMaskV1 = opts.jzMaskV1 === true;
399
625
  // JSON inside a JS expression (valid JS since ES2019); `<` escaped so no value can close a script context.
400
626
  const cfg = JSON.stringify({
401
627
  name: this.#name,
@@ -404,15 +630,124 @@ export class SecretPixelMask {
404
630
  markers: REVEALED_SECRET_SELECTORS,
405
631
  fill: PIXEL_MASK_COLOR,
406
632
  attr: PIXEL_MASK_ATTR,
633
+ overlayAttr: DEMO_OVERLAY_ATTR,
634
+ jz: this.jzMaskV1,
635
+ jzFill: JZ_MASK_FILLS,
636
+ report: `${this.#name}_jz`,
407
637
  }).replace(/</g, "\\u003c");
408
638
  this.#source = MASK_RUNTIME.replace("__CFG__", () => cfg);
409
639
  }
640
+ /**
641
+ * #471: every closed shadow root that appears in `page` while it is open — at load, attached
642
+ * later to a known node, or inside a subtree inserted with one — that is not one of jevitate's own
643
+ * layers is a video breach. Uses the DevTools DOM domain (Chromium); a watch that cannot start or
644
+ * that errors is a breach too (fail closed). Frames below the top document are included
645
+ * (conservative: they are placeholders, but the watch does not tell them apart).
646
+ */
647
+ async #watchClosedRoots(page) {
648
+ const breach = (why) => void this.#jzBreaches.add(why.slice(0, 200));
649
+ let session;
650
+ try {
651
+ session = await within(page.context().newCDPSession(page), "watching shadow roots");
652
+ }
653
+ catch (e) {
654
+ breach(`closed shadow roots cannot be watched: ${errText(e)}`);
655
+ return;
656
+ }
657
+ let closed = false;
658
+ page.once("close", () => {
659
+ closed = true;
660
+ });
661
+ /** Known nodes' attributes (by nodeId): what tells jevitate's own layer hosts apart. */
662
+ const attrs = new Map();
663
+ const ours = (a) => {
664
+ for (let i = 0; a !== undefined && i < a.length; i += 2)
665
+ if (a[i] === PIXEL_MASK_ATTR || a[i] === DEMO_OVERLAY_ATTR)
666
+ return true;
667
+ return false;
668
+ };
669
+ const scan = (n) => {
670
+ if (n.nodeId !== undefined && n.attributes !== undefined)
671
+ attrs.set(n.nodeId, n.attributes);
672
+ for (const sr of (n.shadowRoots ?? [])) {
673
+ if (sr.shadowRootType === "closed" && !ours(n.attributes))
674
+ breach("a closed shadow root appeared that jz-mask-v1 cannot see into");
675
+ scan(sr);
676
+ }
677
+ for (const c of (n.children ?? []))
678
+ scan(c);
679
+ if (n.contentDocument !== undefined)
680
+ scan(n.contentDocument);
681
+ };
682
+ const fail = (e) => {
683
+ if (!closed)
684
+ breach(`the shadow-root watch failed: ${errText(e)}`);
685
+ };
686
+ const load = async () => {
687
+ const { root } = (await session.send("DOM.getDocument", { depth: -1, pierce: true }));
688
+ attrs.clear();
689
+ scan(root);
690
+ };
691
+ const s = session;
692
+ s.on("DOM.documentUpdated", () => void load().catch(fail));
693
+ s.on("DOM.setChildNodes", (p) => {
694
+ for (const n of (p.nodes ?? []))
695
+ scan(n);
696
+ });
697
+ s.on("DOM.childNodeInserted", (p) => {
698
+ const n = p.node;
699
+ scan(n);
700
+ // Learn the inserted subtree whole (pierce), so a root deep inside it is reported too.
701
+ if (n.nodeId !== undefined)
702
+ void session.send("DOM.requestChildNodes", { nodeId: n.nodeId, depth: -1, pierce: true }).catch(fail);
703
+ });
704
+ s.on("DOM.attributeModified", (p) => {
705
+ const id = p.nodeId;
706
+ const a = [...(attrs.get(id) ?? [])];
707
+ const at = a.findIndex((x, i) => i % 2 === 0 && x === p.name);
708
+ if (at >= 0)
709
+ a[at + 1] = String(p.value);
710
+ else
711
+ a.push(String(p.name), String(p.value));
712
+ attrs.set(id, a);
713
+ });
714
+ s.on("DOM.shadowRootPushed", (p) => {
715
+ const root = p.root;
716
+ if (root.shadowRootType === "closed" && !ours(attrs.get(p.hostId)))
717
+ breach("a closed shadow root appeared that jz-mask-v1 cannot see into");
718
+ scan(root);
719
+ });
720
+ try {
721
+ await within(session.send("DOM.enable"), "watching shadow roots");
722
+ await within(load(), "watching shadow roots");
723
+ }
724
+ catch (e) {
725
+ breach(`closed shadow roots cannot be watched: ${errText(e)}`);
726
+ }
727
+ }
410
728
  /** Installs the live mask in `page`'s context (every future document/frame) and every current frame. Throws when it cannot. */
411
729
  async install(page) {
412
730
  const ctx = page.context();
413
731
  if (!this.#contexts.has(ctx)) {
414
732
  try {
733
+ // #471: a frame the jz layer cannot prove is reported as it happens (a document may be gone by the next check).
734
+ if (this.jzMaskV1) {
735
+ await within(ctx.exposeBinding(`${this.#name}_jz`, ({ frame }, reason) => {
736
+ if (frame === frame.page().mainFrame())
737
+ this.#jzBreaches.add(String(reason).slice(0, 200));
738
+ }), "installing the jz-mask-v1 report");
739
+ }
415
740
  await within(ctx.addInitScript({ content: this.#source }), "installing the pixel mask");
741
+ // #471: reduced motion, so a site's smooth scrolling and motion (which can move a region
742
+ // on the compositor between two checked frames, voiding the video) mostly stays off.
743
+ if (this.jzMaskV1)
744
+ for (const p of ctx.pages())
745
+ await within(p.emulateMedia({ reducedMotion: "reduce" }), "emulating reduced motion");
746
+ // #471: a closed shadow root is content the page layer cannot see into — watched for the
747
+ // whole recording through the DevTools protocol, however briefly it exists.
748
+ if (this.jzMaskV1)
749
+ for (const p of ctx.pages())
750
+ await this.#watchClosedRoots(p);
416
751
  }
417
752
  catch (e) {
418
753
  throw new MaskUnavailableError(`could not install the pixel mask: ${errText(e)}`);
@@ -513,6 +848,7 @@ export class SecretPixelMask {
513
848
  let occurrences = 0;
514
849
  let masked = 0;
515
850
  let rects = [];
851
+ let jz;
516
852
  const reasons = [];
517
853
  for (const frame of page.frames()) {
518
854
  if (frame.isDetached())
@@ -527,8 +863,14 @@ export class SecretPixelMask {
527
863
  }
528
864
  occurrences += r.occurrences;
529
865
  masked += r.masked;
530
- if (frame === page.mainFrame())
866
+ if (frame === page.mainFrame()) {
531
867
  rects = r.rects;
868
+ if (r.jz !== undefined) {
869
+ jz = r.jz;
870
+ if (r.jz.breach !== "")
871
+ this.#jzBreaches.add(r.jz.breach);
872
+ }
873
+ }
532
874
  if (!r.ok)
533
875
  reasons.push(r.reason ?? "the mask could not be proven");
534
876
  await this.#sync(frame);
@@ -539,7 +881,10 @@ export class SecretPixelMask {
539
881
  reasons.push(errText(e));
540
882
  }
541
883
  }
542
- return { ok: reasons.length === 0, occurrences, masked, rects, ...(reasons.length === 0 ? {} : { reason: reasons.join("; ") }) };
884
+ if (this.jzMaskV1 && jz === undefined) {
885
+ jz = { ok: false, regions: 0, rects: [], viewport: { width: 0, height: 0 }, reason: `the jz-mask-v1 layer is not running in the page${reasons.length === 0 ? "" : `: ${reasons.join("; ")}`}`, breach: "" };
886
+ }
887
+ return { ok: reasons.length === 0, occurrences, masked, rects, ...(reasons.length === 0 ? {} : { reason: reasons.join("; ") }), ...(jz === undefined ? {} : { jz }) };
543
888
  }
544
889
  /** Throws {@link MaskUnavailableError} unless {@link verify} proves the mask. */
545
890
  async assertMasked(page) {
@@ -560,6 +905,63 @@ export class SecretPixelMask {
560
905
  },
561
906
  };
562
907
  }
908
+ /**
909
+ * #471: what the jz layer could not prove in a top document during the run, in order — a video
910
+ * recorded over such a frame is never exported. Empty when every frame was proven (or jz is off).
911
+ */
912
+ jzBreaches() {
913
+ return [...this.#jzBreaches];
914
+ }
915
+ /**
916
+ * #471: the jz-mask-v1 capture layer (method `dom-before-capture`: the regions are painted over in
917
+ * the page, before the screenshot). Fails closed with {@link JzMaskUnprovenError}, which leaves out
918
+ * that screenshot — never the run. Proof per capture:
919
+ * 1. before: the live layer proves every region covered by a box, nothing in the top layer, no
920
+ * closed shadow root it cannot see into (Chromium DevTools protocol, the page's own roots only);
921
+ * 2. after: the same regions at the same places (nothing moved in between), and every pixel of
922
+ * every region in the written PNG is one of the jz-mask-v1 / secret fills.
923
+ * `regions(step)` is the proven region count of that step's capture.
924
+ */
925
+ jzLayer() {
926
+ const before = new Map();
927
+ const proven = new Map();
928
+ const fail = (step, why) => {
929
+ throw new JzMaskUnprovenError(`step ${step}: jz-mask-v1 could not be proven: ${why}`);
930
+ };
931
+ return {
932
+ name: "jz-mask-v1",
933
+ regions: (step) => proven.get(step),
934
+ prepare: async (page, ctx) => {
935
+ proven.delete(ctx.step);
936
+ if (!this.jzMaskV1)
937
+ fail(ctx.step, "the mask was made without jz-mask-v1");
938
+ const r = await this.verify(page);
939
+ if (r.jz === undefined || !r.jz.ok)
940
+ fail(ctx.step, r.jz?.reason ?? "no proof");
941
+ const hidden = await closedShadowRoots(page);
942
+ if (hidden !== 0)
943
+ fail(ctx.step, hidden < 0 ? "the page's shadow roots could not be inspected" : `${hidden} closed shadow root(s) it cannot see into`);
944
+ before.set(ctx.step, r.jz);
945
+ },
946
+ confirm: async (page, ctx, shot) => {
947
+ const first = before.get(ctx.step);
948
+ before.delete(ctx.step);
949
+ if (first === undefined)
950
+ return fail(ctx.step, "no proof before the capture");
951
+ if (shot === undefined || shot.clip !== undefined)
952
+ return fail(ctx.step, "only a whole-viewport capture can be pixel-checked");
953
+ const r = await this.verify(page);
954
+ if (r.jz === undefined || !r.jz.ok)
955
+ return fail(ctx.step, r.jz?.reason ?? "no proof after the capture");
956
+ if (!sameRegions(first, r.jz))
957
+ return fail(ctx.step, "a region moved or changed during the capture");
958
+ const problem = jzPixelProblem(await readFile(shot.path), first);
959
+ if (problem !== null)
960
+ return fail(ctx.step, problem);
961
+ proven.set(ctx.step, first.regions);
962
+ },
963
+ };
964
+ }
563
965
  /** Draws a capture highlight box around `target`'s first match (display-only). True when shown. */
564
966
  async highlight(page, target) {
565
967
  return this.highlightLocator(page, descriptorToLocator(page, target));
@@ -601,4 +1003,94 @@ export function maskingPort(port, mask) {
601
1003
  },
602
1004
  };
603
1005
  }
1006
+ // ── #471: jz-mask-v1 proof helpers ──────────────────────────────────────────────────────────────
1007
+ /** Same regions, same kinds, same places (within half a CSS pixel). */
1008
+ function sameRegions(a, b) {
1009
+ if (a.rects.length !== b.rects.length)
1010
+ return false;
1011
+ const close = (x, y) => Math.abs(x - y) <= 0.5;
1012
+ return a.rects.every((r, i) => {
1013
+ const o = b.rects[i];
1014
+ return o !== undefined && o.kind === r.kind && close(o.x, r.x) && close(o.y, r.y) && close(o.width, r.width) && close(o.height, r.height);
1015
+ });
1016
+ }
1017
+ function hexRgb(hex) {
1018
+ const n = Number.parseInt(hex.slice(1), 16);
1019
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
1020
+ }
1021
+ /** Every fill a mask box can have: the jz-mask-v1 kinds and the secret mask (boxes may overlap). */
1022
+ const MASK_FILLS_RGB = [...Object.values(JZ_MASK_FILLS), PIXEL_MASK_COLOR].map(hexRgb);
1023
+ /**
1024
+ * Why `png` (a whole-viewport capture) does not show a mask fill on every pixel inside every region
1025
+ * of `check`, or null. Regions are in CSS pixels; the image is the viewport at the device scale.
1026
+ */
1027
+ export function jzPixelProblem(png, check) {
1028
+ let img;
1029
+ try {
1030
+ img = decodePng(png);
1031
+ }
1032
+ catch (e) {
1033
+ return `the capture could not be decoded: ${errText(e)}`;
1034
+ }
1035
+ if (check.rects.length === 0)
1036
+ return null;
1037
+ if (!(check.viewport.width > 0))
1038
+ return "the viewport size is unknown";
1039
+ const scale = img.width / check.viewport.width;
1040
+ let bad = 0;
1041
+ for (const r of check.rects) {
1042
+ const x0 = Math.max(0, Math.ceil(r.x * scale));
1043
+ const y0 = Math.max(0, Math.ceil(r.y * scale));
1044
+ const x1 = Math.min(img.width, Math.floor((r.x + r.width) * scale));
1045
+ const y1 = Math.min(img.height, Math.floor((r.y + r.height) * scale));
1046
+ for (let y = y0; y < y1; y++) {
1047
+ for (let x = x0; x < x1; x++) {
1048
+ const i = (y * img.width + x) * 4;
1049
+ const [pr, pg, pb, pa] = [img.rgba[i], img.rgba[i + 1], img.rgba[i + 2], img.rgba[i + 3]];
1050
+ if (pa !== 255 || !MASK_FILLS_RGB.some(([fr, fg, fb]) => fr === pr && fg === pg && fb === pb))
1051
+ bad++;
1052
+ }
1053
+ }
1054
+ }
1055
+ return bad === 0 ? null : `${bad} pixel(s) inside jz-mask-v1 regions are not a mask fill`;
1056
+ }
1057
+ /**
1058
+ * #471: closed shadow roots in the top document that are not jevitate's own layers — content the
1059
+ * jz layer cannot see into, so it cannot prove a field there masked. -1 when the page cannot be
1060
+ * inspected (not Chromium, a protocol failure): unprovable. Frames below (contentDocument) are
1061
+ * skipped: each is covered whole by a placeholder.
1062
+ */
1063
+ export async function closedShadowRoots(page) {
1064
+ let session;
1065
+ try {
1066
+ session = await within(page.context().newCDPSession(page), "inspecting shadow roots");
1067
+ const { root } = (await within(session.send("DOM.getDocument", { depth: -1, pierce: true }), "inspecting shadow roots"));
1068
+ let closed = 0;
1069
+ const ours = (n) => {
1070
+ const a = n.attributes ?? [];
1071
+ for (let i = 0; i < a.length; i += 2)
1072
+ if (a[i] === PIXEL_MASK_ATTR || a[i] === DEMO_OVERLAY_ATTR)
1073
+ return true;
1074
+ return false;
1075
+ };
1076
+ const stack = [root];
1077
+ while (stack.length > 0) {
1078
+ const n = stack.pop();
1079
+ for (const sr of n.shadowRoots ?? []) {
1080
+ if (sr.shadowRootType === "closed" && !ours(n))
1081
+ closed++;
1082
+ stack.push(sr);
1083
+ }
1084
+ for (const c of n.children ?? [])
1085
+ stack.push(c);
1086
+ }
1087
+ return closed;
1088
+ }
1089
+ catch {
1090
+ return -1;
1091
+ }
1092
+ finally {
1093
+ await session?.detach().catch(() => undefined);
1094
+ }
1095
+ }
604
1096
  //# sourceMappingURL=demo-capture.js.map