@riddledc/riddle-proof 0.5.42 → 0.5.44

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.
@@ -381,6 +381,21 @@ function visualDeltaShipGateReason(state = {}) {
381
381
  if (reason) return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof: ${reason}`;
382
382
  return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof`;
383
383
  }
384
+ function visualDeltaEvidenceIssueCode(state = {}, blocker = "") {
385
+ const visualDelta = visualDeltaForState(state || {});
386
+ const status = String(visualDelta.status || "").trim();
387
+ const reason = `${String(visualDelta.reason || "")}
388
+ ${blocker}`.toLowerCase();
389
+ if (status === "unmeasured") {
390
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
391
+ return "comparator_fetch_blocked";
392
+ }
393
+ return "visual_delta_unmeasured";
394
+ }
395
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
396
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
397
+ return "semantic_proof_failed";
398
+ }
384
399
  function requiredBaselineLabelsForState(state = {}) {
385
400
  const reference = normalizedReference(state);
386
401
  const labels = [];
@@ -570,7 +585,7 @@ var CHECKPOINT_CONTRACT_SPECS = {
570
585
  description: "JSON assessment with decision, summary, recommended_stage, continue_with_stage, escalation_target, and reasons."
571
586
  }],
572
587
  response_schema: {
573
- decision: ["ready_to_ship", "needs_richer_proof"],
588
+ decision: ["ready_to_ship", "needs_richer_proof", "revise_capture"],
574
589
  summary: "string",
575
590
  recommended_stage: ["ship", "author", "implement", "recon", "verify"],
576
591
  continue_with_stage: ["ship", "author", "implement", "recon", "verify"],
@@ -773,9 +788,15 @@ function mergeStateFromParams(statePath, params) {
773
788
  const readyBlocker = assessment?.decision === "ready_to_ship" ? visualDeltaShipGateReason({ ...state, proof_assessment: assessment, proof_assessment_source: assessment.source }) : null;
774
789
  if (readyBlocker) {
775
790
  assessment.blocked_decision = assessment.decision;
776
- assessment.decision = "needs_richer_proof";
777
- if (assessment.recommended_stage === "ship") assessment.recommended_stage = "verify";
778
- if (assessment.continue_with_stage === "ship") assessment.continue_with_stage = "verify";
791
+ assessment.decision = "revise_capture";
792
+ assessment.recommended_stage = "verify";
793
+ assessment.continue_with_stage = "verify";
794
+ assessment.evidence_collection_incomplete = true;
795
+ assessment.recovery_stage = "verify";
796
+ assessment.recovery_reason = readyBlocker;
797
+ assessment.evidence_issue_code = visualDeltaEvidenceIssueCode(state, readyBlocker);
798
+ assessment.visual_delta = visualDeltaForState(state);
799
+ assessment.suggested_repair = "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.";
779
800
  const blockers = Array.isArray(assessment.blockers) ? assessment.blockers : [];
780
801
  assessment.blockers = [...blockers, readyBlocker];
781
802
  }
@@ -794,7 +815,7 @@ function mergeStateFromParams(statePath, params) {
794
815
  }
795
816
  appendProofSummaryLine(state, `Supervising proof assessment: ${state.proof_assessment.decision || "unknown"}`);
796
817
  if (readyBlocker) {
797
- appendProofSummaryLine(state, `Ready-to-ship assessment blocked: ${readyBlocker}`);
818
+ appendProofSummaryLine(state, `Ready-to-ship assessment routed to evidence recovery: ${readyBlocker}`);
798
819
  }
799
820
  if (state.proof_assessment_summary) {
800
821
  appendProofSummaryLine(state, `Assessment summary: ${state.proof_assessment_summary}`);
@@ -25,8 +25,10 @@ import {
25
25
  recordValue
26
26
  } from "./chunk-W7VTDN4T.js";
27
27
  import {
28
+ visualDeltaForState,
29
+ visualDeltaRequiredForState,
28
30
  visualDeltaShipGateReason
29
- } from "./chunk-U4VNN2CT.js";
31
+ } from "./chunk-4ASMX4R6.js";
30
32
 
31
33
  // src/engine-harness.ts
32
34
  import { execFileSync } from "child_process";
@@ -315,14 +317,36 @@ function proofAssessmentVisualBlocker(state, payload) {
315
317
  proof_assessment_source: source
316
318
  });
317
319
  }
318
- function blockedProofAssessmentPayload(payload, blocker) {
320
+ function visualDeltaBlockerCode(state, blocker) {
321
+ const visualDelta = visualDeltaForState(state || {});
322
+ const status = nonEmptyString(visualDelta.status);
323
+ const reason = `${nonEmptyString(visualDelta.reason) || ""}
324
+ ${blocker}`.toLowerCase();
325
+ if (status === "unmeasured") {
326
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
327
+ return "comparator_fetch_blocked";
328
+ }
329
+ return "visual_delta_unmeasured";
330
+ }
331
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
332
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
333
+ return "semantic_proof_failed";
334
+ }
335
+ function visualDeltaEvidenceRecoveryAssessment(state, payload, blocker) {
336
+ const visualDelta = visualDeltaForState(state || {});
319
337
  const blockers = Array.isArray(payload.blockers) ? payload.blockers.filter((item) => typeof item === "string") : [];
320
338
  return {
321
339
  ...payload,
322
340
  blocked_decision: payload.decision || "ready_to_ship",
323
- decision: "needs_richer_proof",
324
- recommended_stage: payload.recommended_stage === "ship" ? "verify" : payload.recommended_stage || "verify",
325
- continue_with_stage: payload.continue_with_stage === "ship" ? "verify" : payload.continue_with_stage || "verify",
341
+ decision: "revise_capture",
342
+ recommended_stage: "verify",
343
+ continue_with_stage: "verify",
344
+ evidence_collection_incomplete: true,
345
+ recovery_stage: "verify",
346
+ recovery_reason: blocker,
347
+ evidence_issue_code: visualDeltaBlockerCode(state, blocker),
348
+ visual_delta: Object.keys(visualDelta).length ? visualDelta : null,
349
+ suggested_repair: "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.",
326
350
  blockers: [...blockers, blocker]
327
351
  };
328
352
  }
@@ -887,14 +911,24 @@ async function routeCheckpoint(request, state, result, agent, input) {
887
911
  verification_mode: context.fullRiddleState?.verification_mode || request.verification_mode
888
912
  }, payload);
889
913
  if (visualBlocker) {
914
+ const recoveryAssessment = visualDeltaEvidenceRecoveryAssessment({
915
+ ...context.fullRiddleState || {},
916
+ verification_mode: context.fullRiddleState?.verification_mode || request.verification_mode
917
+ }, payload, visualBlocker);
890
918
  recordEvent(state, {
891
- kind: "agent.proof_assessment.blocked",
919
+ kind: "agent.proof_assessment.evidence_recovery_required",
892
920
  checkpoint,
893
921
  stage: "verify",
894
922
  summary: visualBlocker,
895
- details: { payload }
923
+ details: compactRecord({
924
+ evidence_collection_incomplete: true,
925
+ recovery_stage: "verify",
926
+ evidence_issue_code: recoveryAssessment.evidence_issue_code || null,
927
+ visual_delta: recoveryAssessment.visual_delta || null,
928
+ proof_assessment: recoveryAssessment
929
+ })
896
930
  });
897
- return { next: proofAssessmentContinuation(result, blockedProofAssessmentPayload(payload, visualBlocker)) };
931
+ return { next: proofAssessmentContinuation(result, recoveryAssessment) };
898
932
  }
899
933
  if (effectiveShipMode(request, input.config) !== "ship" && proofAssessmentRequestsShip(payload)) {
900
934
  return {
@@ -96,14 +96,16 @@ function summarizeCaptureArtifacts(payload) {
96
96
  const record = isRecord(payload) ? payload : {};
97
97
  const artifactJson = isRecord(record._artifact_json) ? record._artifact_json : {};
98
98
  const proofJson = isRecord(record._proof_json) ? record._proof_json : isRecord(artifactJson["proof.json"]) ? artifactJson["proof.json"] : {};
99
+ const visualDiffJson = isRecord(record.visual_diff) ? record.visual_diff : isRecord(record.visualDiff) ? record.visualDiff : isRecord(artifactJson["visual-diff.json"]) ? artifactJson["visual-diff.json"] : {};
99
100
  const consoleJson = isRecord(record.console) ? record.console : isRecord(artifactJson["console.json"]) ? artifactJson["console.json"] : {};
100
101
  const result = isRecord(record.result) ? record.result : isRecord(proofJson.result) ? proofJson.result : isRecord(proofJson.script_result) ? proofJson.script_result : isRecord(proofJson.return_value) ? proofJson.return_value : isRecord(proofJson.value) ? proofJson.value : {};
101
102
  const consoleSummary = isRecord(consoleJson.summary) ? redactForProofDiagnostics(consoleJson.summary, { string_limit: 500 }) : void 0;
103
+ const resultKeys = /* @__PURE__ */ new Set([...sortedKeys(result), ...sortedKeys(visualDiffJson)]);
102
104
  return {
103
105
  outputs: artifactItems(record.outputs).slice(0, 20).map((item) => artifactSummary(item, "outputs")),
104
106
  screenshots: artifactItems(record.screenshots).slice(0, 10).map((item) => artifactSummary(item, "screenshots")),
105
107
  artifacts: artifactItems(record.artifacts).slice(0, 20).map((item) => artifactSummary(item, "artifacts")),
106
- result_keys: sortedKeys(result),
108
+ result_keys: Array.from(resultKeys).sort(),
107
109
  artifact_json: sortedKeys(artifactJson),
108
110
  artifact_errors: artifactErrorMap(record._artifact_errors),
109
111
  proof_script_error: Boolean(proofJson.script_error),
@@ -127,14 +127,16 @@ function summarizeCaptureArtifacts(payload) {
127
127
  const record = isRecord(payload) ? payload : {};
128
128
  const artifactJson = isRecord(record._artifact_json) ? record._artifact_json : {};
129
129
  const proofJson = isRecord(record._proof_json) ? record._proof_json : isRecord(artifactJson["proof.json"]) ? artifactJson["proof.json"] : {};
130
+ const visualDiffJson = isRecord(record.visual_diff) ? record.visual_diff : isRecord(record.visualDiff) ? record.visualDiff : isRecord(artifactJson["visual-diff.json"]) ? artifactJson["visual-diff.json"] : {};
130
131
  const consoleJson = isRecord(record.console) ? record.console : isRecord(artifactJson["console.json"]) ? artifactJson["console.json"] : {};
131
132
  const result = isRecord(record.result) ? record.result : isRecord(proofJson.result) ? proofJson.result : isRecord(proofJson.script_result) ? proofJson.script_result : isRecord(proofJson.return_value) ? proofJson.return_value : isRecord(proofJson.value) ? proofJson.value : {};
132
133
  const consoleSummary = isRecord(consoleJson.summary) ? redactForProofDiagnostics(consoleJson.summary, { string_limit: 500 }) : void 0;
134
+ const resultKeys = /* @__PURE__ */ new Set([...sortedKeys(result), ...sortedKeys(visualDiffJson)]);
133
135
  return {
134
136
  outputs: artifactItems(record.outputs).slice(0, 20).map((item) => artifactSummary(item, "outputs")),
135
137
  screenshots: artifactItems(record.screenshots).slice(0, 10).map((item) => artifactSummary(item, "screenshots")),
136
138
  artifacts: artifactItems(record.artifacts).slice(0, 20).map((item) => artifactSummary(item, "artifacts")),
137
- result_keys: sortedKeys(result),
139
+ result_keys: Array.from(resultKeys).sort(),
138
140
  artifact_json: sortedKeys(artifactJson),
139
141
  artifact_errors: artifactErrorMap(record._artifact_errors),
140
142
  proof_script_error: Boolean(proofJson.script_error),
@@ -7,7 +7,7 @@ import {
7
7
  createCaptureDiagnostic,
8
8
  redactForProofDiagnostics,
9
9
  summarizeCaptureArtifacts
10
- } from "./chunk-DWQK7J4U.js";
10
+ } from "./chunk-XNRMMQAY.js";
11
11
  export {
12
12
  DEFAULT_DIAGNOSTIC_ARRAY_LIMIT,
13
13
  DEFAULT_DIAGNOSTIC_HISTORY_LIMIT,
@@ -389,6 +389,21 @@ function visualDeltaShipGateReason(state = {}) {
389
389
  if (reason) return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof: ${reason}`;
390
390
  return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof`;
391
391
  }
392
+ function visualDeltaEvidenceIssueCode(state = {}, blocker = "") {
393
+ const visualDelta = visualDeltaForState(state || {});
394
+ const status = String(visualDelta.status || "").trim();
395
+ const reason = `${String(visualDelta.reason || "")}
396
+ ${blocker}`.toLowerCase();
397
+ if (status === "unmeasured") {
398
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
399
+ return "comparator_fetch_blocked";
400
+ }
401
+ return "visual_delta_unmeasured";
402
+ }
403
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
404
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
405
+ return "semantic_proof_failed";
406
+ }
392
407
  function requiredBaselineLabelsForState(state = {}) {
393
408
  const reference = normalizedReference(state);
394
409
  const labels = [];
@@ -613,9 +628,15 @@ function mergeStateFromParams(statePath, params) {
613
628
  const readyBlocker = assessment?.decision === "ready_to_ship" ? visualDeltaShipGateReason({ ...state, proof_assessment: assessment, proof_assessment_source: assessment.source }) : null;
614
629
  if (readyBlocker) {
615
630
  assessment.blocked_decision = assessment.decision;
616
- assessment.decision = "needs_richer_proof";
617
- if (assessment.recommended_stage === "ship") assessment.recommended_stage = "verify";
618
- if (assessment.continue_with_stage === "ship") assessment.continue_with_stage = "verify";
631
+ assessment.decision = "revise_capture";
632
+ assessment.recommended_stage = "verify";
633
+ assessment.continue_with_stage = "verify";
634
+ assessment.evidence_collection_incomplete = true;
635
+ assessment.recovery_stage = "verify";
636
+ assessment.recovery_reason = readyBlocker;
637
+ assessment.evidence_issue_code = visualDeltaEvidenceIssueCode(state, readyBlocker);
638
+ assessment.visual_delta = visualDeltaForState(state);
639
+ assessment.suggested_repair = "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.";
619
640
  const blockers = Array.isArray(assessment.blockers) ? assessment.blockers : [];
620
641
  assessment.blockers = [...blockers, readyBlocker];
621
642
  }
@@ -634,7 +655,7 @@ function mergeStateFromParams(statePath, params) {
634
655
  }
635
656
  appendProofSummaryLine(state, `Supervising proof assessment: ${state.proof_assessment.decision || "unknown"}`);
636
657
  if (readyBlocker) {
637
- appendProofSummaryLine(state, `Ready-to-ship assessment blocked: ${readyBlocker}`);
658
+ appendProofSummaryLine(state, `Ready-to-ship assessment routed to evidence recovery: ${readyBlocker}`);
638
659
  }
639
660
  if (state.proof_assessment_summary) {
640
661
  appendProofSummaryLine(state, `Assessment summary: ${state.proof_assessment_summary}`);
@@ -908,7 +929,7 @@ var init_proof_run_core = __esm({
908
929
  description: "JSON assessment with decision, summary, recommended_stage, continue_with_stage, escalation_target, and reasons."
909
930
  }],
910
931
  response_schema: {
911
- decision: ["ready_to_ship", "needs_richer_proof"],
932
+ decision: ["ready_to_ship", "needs_richer_proof", "revise_capture"],
912
933
  summary: "string",
913
934
  recommended_stage: ["ship", "author", "implement", "recon", "verify"],
914
935
  continue_with_stage: ["ship", "author", "implement", "recon", "verify"],
@@ -1713,10 +1734,10 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
1713
1734
  const primaryShipGateNextAction = (shipGate) => {
1714
1735
  const reasons = shipGate.reasons || [];
1715
1736
  if (reasons.some((reason) => reason.includes("proof_assessment"))) {
1716
- return "resume with riddle_proof_review using decision=ready_to_ship only after the screenshots and semantic evidence visibly prove the request; otherwise choose needs_implementation or needs_richer_proof";
1737
+ return "resume with riddle_proof_review using decision=ready_to_ship only after screenshots, semantic evidence, and required comparison metrics prove the request; otherwise choose needs_implementation, revise_capture, or needs_richer_proof for the specific missing stage";
1717
1738
  }
1718
1739
  if (reasons.some((reason) => reason.includes("visual_delta"))) {
1719
- return "rerun verify with a measured before/after visual delta, or choose needs_richer_proof instead of ready_to_ship for this visual proof";
1740
+ return "keep the run in verify/evidence recovery until a measured before/after visual_delta exists; choose revise_capture rather than ready_to_ship or generic needs_richer_proof for this visual proof";
1720
1741
  }
1721
1742
  if (reasons.some((reason) => reason.includes("after_cdn") || reason.includes("verify_status"))) {
1722
1743
  return "rerun verify with stronger proof framing so after evidence is captured before shipping";
@@ -3692,14 +3713,36 @@ function proofAssessmentVisualBlocker(state, payload) {
3692
3713
  proof_assessment_source: source
3693
3714
  });
3694
3715
  }
3695
- function blockedProofAssessmentPayload(payload, blocker) {
3716
+ function visualDeltaBlockerCode(state, blocker) {
3717
+ const visualDelta = visualDeltaForState(state || {});
3718
+ const status = nonEmptyString(visualDelta.status);
3719
+ const reason = `${nonEmptyString(visualDelta.reason) || ""}
3720
+ ${blocker}`.toLowerCase();
3721
+ if (status === "unmeasured") {
3722
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
3723
+ return "comparator_fetch_blocked";
3724
+ }
3725
+ return "visual_delta_unmeasured";
3726
+ }
3727
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
3728
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
3729
+ return "semantic_proof_failed";
3730
+ }
3731
+ function visualDeltaEvidenceRecoveryAssessment(state, payload, blocker) {
3732
+ const visualDelta = visualDeltaForState(state || {});
3696
3733
  const blockers = Array.isArray(payload.blockers) ? payload.blockers.filter((item) => typeof item === "string") : [];
3697
3734
  return {
3698
3735
  ...payload,
3699
3736
  blocked_decision: payload.decision || "ready_to_ship",
3700
- decision: "needs_richer_proof",
3701
- recommended_stage: payload.recommended_stage === "ship" ? "verify" : payload.recommended_stage || "verify",
3702
- continue_with_stage: payload.continue_with_stage === "ship" ? "verify" : payload.continue_with_stage || "verify",
3737
+ decision: "revise_capture",
3738
+ recommended_stage: "verify",
3739
+ continue_with_stage: "verify",
3740
+ evidence_collection_incomplete: true,
3741
+ recovery_stage: "verify",
3742
+ recovery_reason: blocker,
3743
+ evidence_issue_code: visualDeltaBlockerCode(state, blocker),
3744
+ visual_delta: Object.keys(visualDelta).length ? visualDelta : null,
3745
+ suggested_repair: "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.",
3703
3746
  blockers: [...blockers, blocker]
3704
3747
  };
3705
3748
  }
@@ -4264,14 +4307,24 @@ async function routeCheckpoint(request, state, result, agent, input) {
4264
4307
  verification_mode: context.fullRiddleState?.verification_mode || request.verification_mode
4265
4308
  }, payload);
4266
4309
  if (visualBlocker) {
4310
+ const recoveryAssessment = visualDeltaEvidenceRecoveryAssessment({
4311
+ ...context.fullRiddleState || {},
4312
+ verification_mode: context.fullRiddleState?.verification_mode || request.verification_mode
4313
+ }, payload, visualBlocker);
4267
4314
  recordEvent(state, {
4268
- kind: "agent.proof_assessment.blocked",
4315
+ kind: "agent.proof_assessment.evidence_recovery_required",
4269
4316
  checkpoint,
4270
4317
  stage: "verify",
4271
4318
  summary: visualBlocker,
4272
- details: { payload }
4319
+ details: compactRecord({
4320
+ evidence_collection_incomplete: true,
4321
+ recovery_stage: "verify",
4322
+ evidence_issue_code: recoveryAssessment.evidence_issue_code || null,
4323
+ visual_delta: recoveryAssessment.visual_delta || null,
4324
+ proof_assessment: recoveryAssessment
4325
+ })
4273
4326
  });
4274
- return { next: proofAssessmentContinuation(result, blockedProofAssessmentPayload(payload, visualBlocker)) };
4327
+ return { next: proofAssessmentContinuation(result, recoveryAssessment) };
4275
4328
  }
4276
4329
  if (effectiveShipMode(request, input.config) !== "ship" && proofAssessmentRequestsShip(payload)) {
4277
4330
  return {
@@ -2,11 +2,11 @@ import {
2
2
  createDisabledRiddleProofAgentAdapter,
3
3
  readRiddleProofRunStatus,
4
4
  runRiddleProofEngineHarness
5
- } from "./chunk-VSLU6XNI.js";
5
+ } from "./chunk-MJD37CLH.js";
6
6
  import "./chunk-RI25RGQP.js";
7
7
  import "./chunk-7S7O3NKF.js";
8
8
  import "./chunk-W7VTDN4T.js";
9
- import "./chunk-U4VNN2CT.js";
9
+ import "./chunk-4ASMX4R6.js";
10
10
  export {
11
11
  createDisabledRiddleProofAgentAdapter,
12
12
  readRiddleProofRunStatus,
package/dist/index.cjs CHANGED
@@ -389,6 +389,21 @@ function visualDeltaShipGateReason(state = {}) {
389
389
  if (reason) return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof: ${reason}`;
390
390
  return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof`;
391
391
  }
392
+ function visualDeltaEvidenceIssueCode(state = {}, blocker = "") {
393
+ const visualDelta = visualDeltaForState(state || {});
394
+ const status = String(visualDelta.status || "").trim();
395
+ const reason = `${String(visualDelta.reason || "")}
396
+ ${blocker}`.toLowerCase();
397
+ if (status === "unmeasured") {
398
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
399
+ return "comparator_fetch_blocked";
400
+ }
401
+ return "visual_delta_unmeasured";
402
+ }
403
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
404
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
405
+ return "semantic_proof_failed";
406
+ }
392
407
  function requiredBaselineLabelsForState(state = {}) {
393
408
  const reference = normalizedReference(state);
394
409
  const labels = [];
@@ -613,9 +628,15 @@ function mergeStateFromParams(statePath, params) {
613
628
  const readyBlocker = assessment?.decision === "ready_to_ship" ? visualDeltaShipGateReason({ ...state, proof_assessment: assessment, proof_assessment_source: assessment.source }) : null;
614
629
  if (readyBlocker) {
615
630
  assessment.blocked_decision = assessment.decision;
616
- assessment.decision = "needs_richer_proof";
617
- if (assessment.recommended_stage === "ship") assessment.recommended_stage = "verify";
618
- if (assessment.continue_with_stage === "ship") assessment.continue_with_stage = "verify";
631
+ assessment.decision = "revise_capture";
632
+ assessment.recommended_stage = "verify";
633
+ assessment.continue_with_stage = "verify";
634
+ assessment.evidence_collection_incomplete = true;
635
+ assessment.recovery_stage = "verify";
636
+ assessment.recovery_reason = readyBlocker;
637
+ assessment.evidence_issue_code = visualDeltaEvidenceIssueCode(state, readyBlocker);
638
+ assessment.visual_delta = visualDeltaForState(state);
639
+ assessment.suggested_repair = "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.";
619
640
  const blockers = Array.isArray(assessment.blockers) ? assessment.blockers : [];
620
641
  assessment.blockers = [...blockers, readyBlocker];
621
642
  }
@@ -634,7 +655,7 @@ function mergeStateFromParams(statePath, params) {
634
655
  }
635
656
  appendProofSummaryLine(state, `Supervising proof assessment: ${state.proof_assessment.decision || "unknown"}`);
636
657
  if (readyBlocker) {
637
- appendProofSummaryLine(state, `Ready-to-ship assessment blocked: ${readyBlocker}`);
658
+ appendProofSummaryLine(state, `Ready-to-ship assessment routed to evidence recovery: ${readyBlocker}`);
638
659
  }
639
660
  if (state.proof_assessment_summary) {
640
661
  appendProofSummaryLine(state, `Assessment summary: ${state.proof_assessment_summary}`);
@@ -908,7 +929,7 @@ var init_proof_run_core = __esm({
908
929
  description: "JSON assessment with decision, summary, recommended_stage, continue_with_stage, escalation_target, and reasons."
909
930
  }],
910
931
  response_schema: {
911
- decision: ["ready_to_ship", "needs_richer_proof"],
932
+ decision: ["ready_to_ship", "needs_richer_proof", "revise_capture"],
912
933
  summary: "string",
913
934
  recommended_stage: ["ship", "author", "implement", "recon", "verify"],
914
935
  continue_with_stage: ["ship", "author", "implement", "recon", "verify"],
@@ -1713,10 +1734,10 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
1713
1734
  const primaryShipGateNextAction = (shipGate) => {
1714
1735
  const reasons = shipGate.reasons || [];
1715
1736
  if (reasons.some((reason) => reason.includes("proof_assessment"))) {
1716
- return "resume with riddle_proof_review using decision=ready_to_ship only after the screenshots and semantic evidence visibly prove the request; otherwise choose needs_implementation or needs_richer_proof";
1737
+ return "resume with riddle_proof_review using decision=ready_to_ship only after screenshots, semantic evidence, and required comparison metrics prove the request; otherwise choose needs_implementation, revise_capture, or needs_richer_proof for the specific missing stage";
1717
1738
  }
1718
1739
  if (reasons.some((reason) => reason.includes("visual_delta"))) {
1719
- return "rerun verify with a measured before/after visual delta, or choose needs_richer_proof instead of ready_to_ship for this visual proof";
1740
+ return "keep the run in verify/evidence recovery until a measured before/after visual_delta exists; choose revise_capture rather than ready_to_ship or generic needs_richer_proof for this visual proof";
1720
1741
  }
1721
1742
  if (reasons.some((reason) => reason.includes("after_cdn") || reason.includes("verify_status"))) {
1722
1743
  return "rerun verify with stronger proof framing so after evidence is captured before shipping";
@@ -4270,14 +4291,36 @@ function proofAssessmentVisualBlocker(state, payload) {
4270
4291
  proof_assessment_source: source
4271
4292
  });
4272
4293
  }
4273
- function blockedProofAssessmentPayload(payload, blocker) {
4294
+ function visualDeltaBlockerCode(state, blocker) {
4295
+ const visualDelta = visualDeltaForState(state || {});
4296
+ const status = nonEmptyString(visualDelta.status);
4297
+ const reason = `${nonEmptyString(visualDelta.reason) || ""}
4298
+ ${blocker}`.toLowerCase();
4299
+ if (status === "unmeasured") {
4300
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
4301
+ return "comparator_fetch_blocked";
4302
+ }
4303
+ return "visual_delta_unmeasured";
4304
+ }
4305
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
4306
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
4307
+ return "semantic_proof_failed";
4308
+ }
4309
+ function visualDeltaEvidenceRecoveryAssessment(state, payload, blocker) {
4310
+ const visualDelta = visualDeltaForState(state || {});
4274
4311
  const blockers = Array.isArray(payload.blockers) ? payload.blockers.filter((item) => typeof item === "string") : [];
4275
4312
  return {
4276
4313
  ...payload,
4277
4314
  blocked_decision: payload.decision || "ready_to_ship",
4278
- decision: "needs_richer_proof",
4279
- recommended_stage: payload.recommended_stage === "ship" ? "verify" : payload.recommended_stage || "verify",
4280
- continue_with_stage: payload.continue_with_stage === "ship" ? "verify" : payload.continue_with_stage || "verify",
4315
+ decision: "revise_capture",
4316
+ recommended_stage: "verify",
4317
+ continue_with_stage: "verify",
4318
+ evidence_collection_incomplete: true,
4319
+ recovery_stage: "verify",
4320
+ recovery_reason: blocker,
4321
+ evidence_issue_code: visualDeltaBlockerCode(state, blocker),
4322
+ visual_delta: Object.keys(visualDelta).length ? visualDelta : null,
4323
+ suggested_repair: "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.",
4281
4324
  blockers: [...blockers, blocker]
4282
4325
  };
4283
4326
  }
@@ -4842,14 +4885,24 @@ async function routeCheckpoint(request, state, result, agent, input) {
4842
4885
  verification_mode: context.fullRiddleState?.verification_mode || request.verification_mode
4843
4886
  }, payload);
4844
4887
  if (visualBlocker) {
4888
+ const recoveryAssessment = visualDeltaEvidenceRecoveryAssessment({
4889
+ ...context.fullRiddleState || {},
4890
+ verification_mode: context.fullRiddleState?.verification_mode || request.verification_mode
4891
+ }, payload, visualBlocker);
4845
4892
  recordEvent(state, {
4846
- kind: "agent.proof_assessment.blocked",
4893
+ kind: "agent.proof_assessment.evidence_recovery_required",
4847
4894
  checkpoint,
4848
4895
  stage: "verify",
4849
4896
  summary: visualBlocker,
4850
- details: { payload }
4897
+ details: compactRecord({
4898
+ evidence_collection_incomplete: true,
4899
+ recovery_stage: "verify",
4900
+ evidence_issue_code: recoveryAssessment.evidence_issue_code || null,
4901
+ visual_delta: recoveryAssessment.visual_delta || null,
4902
+ proof_assessment: recoveryAssessment
4903
+ })
4851
4904
  });
4852
- return { next: proofAssessmentContinuation(result, blockedProofAssessmentPayload(payload, visualBlocker)) };
4905
+ return { next: proofAssessmentContinuation(result, recoveryAssessment) };
4853
4906
  }
4854
4907
  if (effectiveShipMode(request, input.config) !== "ship" && proofAssessmentRequestsShip(payload)) {
4855
4908
  return {
@@ -5172,14 +5225,16 @@ function summarizeCaptureArtifacts(payload) {
5172
5225
  const record = isRecord(payload) ? payload : {};
5173
5226
  const artifactJson = isRecord(record._artifact_json) ? record._artifact_json : {};
5174
5227
  const proofJson = isRecord(record._proof_json) ? record._proof_json : isRecord(artifactJson["proof.json"]) ? artifactJson["proof.json"] : {};
5228
+ const visualDiffJson = isRecord(record.visual_diff) ? record.visual_diff : isRecord(record.visualDiff) ? record.visualDiff : isRecord(artifactJson["visual-diff.json"]) ? artifactJson["visual-diff.json"] : {};
5175
5229
  const consoleJson = isRecord(record.console) ? record.console : isRecord(artifactJson["console.json"]) ? artifactJson["console.json"] : {};
5176
5230
  const result = isRecord(record.result) ? record.result : isRecord(proofJson.result) ? proofJson.result : isRecord(proofJson.script_result) ? proofJson.script_result : isRecord(proofJson.return_value) ? proofJson.return_value : isRecord(proofJson.value) ? proofJson.value : {};
5177
5231
  const consoleSummary = isRecord(consoleJson.summary) ? redactForProofDiagnostics(consoleJson.summary, { string_limit: 500 }) : void 0;
5232
+ const resultKeys = /* @__PURE__ */ new Set([...sortedKeys(result), ...sortedKeys(visualDiffJson)]);
5178
5233
  return {
5179
5234
  outputs: artifactItems(record.outputs).slice(0, 20).map((item) => artifactSummary(item, "outputs")),
5180
5235
  screenshots: artifactItems(record.screenshots).slice(0, 10).map((item) => artifactSummary(item, "screenshots")),
5181
5236
  artifacts: artifactItems(record.artifacts).slice(0, 20).map((item) => artifactSummary(item, "artifacts")),
5182
- result_keys: sortedKeys(result),
5237
+ result_keys: Array.from(resultKeys).sort(),
5183
5238
  artifact_json: sortedKeys(artifactJson),
5184
5239
  artifact_errors: artifactErrorMap(record._artifact_errors),
5185
5240
  proof_script_error: Boolean(proofJson.script_error),
package/dist/index.js CHANGED
@@ -20,12 +20,12 @@ import {
20
20
  createCaptureDiagnostic,
21
21
  redactForProofDiagnostics,
22
22
  summarizeCaptureArtifacts
23
- } from "./chunk-DWQK7J4U.js";
23
+ } from "./chunk-XNRMMQAY.js";
24
24
  import {
25
25
  createDisabledRiddleProofAgentAdapter,
26
26
  readRiddleProofRunStatus,
27
27
  runRiddleProofEngineHarness
28
- } from "./chunk-VSLU6XNI.js";
28
+ } from "./chunk-MJD37CLH.js";
29
29
  import {
30
30
  RIDDLE_PROOF_CHECKPOINT_PACKET_VERSION,
31
31
  RIDDLE_PROOF_CHECKPOINT_RESPONSE_VERSION,
@@ -67,7 +67,7 @@ import {
67
67
  extractPlayabilityEvidence,
68
68
  isRiddleProofPlayabilityMode
69
69
  } from "./chunk-NSWT3VSV.js";
70
- import "./chunk-U4VNN2CT.js";
70
+ import "./chunk-4ASMX4R6.js";
71
71
  export {
72
72
  DEFAULT_DIAGNOSTIC_ARRAY_LIMIT,
73
73
  DEFAULT_DIAGNOSTIC_HISTORY_LIMIT,
@@ -440,6 +440,21 @@ function visualDeltaShipGateReason(state = {}) {
440
440
  if (reason) return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof: ${reason}`;
441
441
  return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof`;
442
442
  }
443
+ function visualDeltaEvidenceIssueCode(state = {}, blocker = "") {
444
+ const visualDelta = visualDeltaForState(state || {});
445
+ const status = String(visualDelta.status || "").trim();
446
+ const reason = `${String(visualDelta.reason || "")}
447
+ ${blocker}`.toLowerCase();
448
+ if (status === "unmeasured") {
449
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
450
+ return "comparator_fetch_blocked";
451
+ }
452
+ return "visual_delta_unmeasured";
453
+ }
454
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
455
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
456
+ return "semantic_proof_failed";
457
+ }
443
458
  function requiredBaselineLabelsForState(state = {}) {
444
459
  const reference = normalizedReference(state);
445
460
  const labels = [];
@@ -629,7 +644,7 @@ var CHECKPOINT_CONTRACT_SPECS = {
629
644
  description: "JSON assessment with decision, summary, recommended_stage, continue_with_stage, escalation_target, and reasons."
630
645
  }],
631
646
  response_schema: {
632
- decision: ["ready_to_ship", "needs_richer_proof"],
647
+ decision: ["ready_to_ship", "needs_richer_proof", "revise_capture"],
633
648
  summary: "string",
634
649
  recommended_stage: ["ship", "author", "implement", "recon", "verify"],
635
650
  continue_with_stage: ["ship", "author", "implement", "recon", "verify"],
@@ -832,9 +847,15 @@ function mergeStateFromParams(statePath, params) {
832
847
  const readyBlocker = assessment?.decision === "ready_to_ship" ? visualDeltaShipGateReason({ ...state, proof_assessment: assessment, proof_assessment_source: assessment.source }) : null;
833
848
  if (readyBlocker) {
834
849
  assessment.blocked_decision = assessment.decision;
835
- assessment.decision = "needs_richer_proof";
836
- if (assessment.recommended_stage === "ship") assessment.recommended_stage = "verify";
837
- if (assessment.continue_with_stage === "ship") assessment.continue_with_stage = "verify";
850
+ assessment.decision = "revise_capture";
851
+ assessment.recommended_stage = "verify";
852
+ assessment.continue_with_stage = "verify";
853
+ assessment.evidence_collection_incomplete = true;
854
+ assessment.recovery_stage = "verify";
855
+ assessment.recovery_reason = readyBlocker;
856
+ assessment.evidence_issue_code = visualDeltaEvidenceIssueCode(state, readyBlocker);
857
+ assessment.visual_delta = visualDeltaForState(state);
858
+ assessment.suggested_repair = "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.";
838
859
  const blockers = Array.isArray(assessment.blockers) ? assessment.blockers : [];
839
860
  assessment.blockers = [...blockers, readyBlocker];
840
861
  }
@@ -853,7 +874,7 @@ function mergeStateFromParams(statePath, params) {
853
874
  }
854
875
  appendProofSummaryLine(state, `Supervising proof assessment: ${state.proof_assessment.decision || "unknown"}`);
855
876
  if (readyBlocker) {
856
- appendProofSummaryLine(state, `Ready-to-ship assessment blocked: ${readyBlocker}`);
877
+ appendProofSummaryLine(state, `Ready-to-ship assessment routed to evidence recovery: ${readyBlocker}`);
857
878
  }
858
879
  if (state.proof_assessment_summary) {
859
880
  appendProofSummaryLine(state, `Assessment summary: ${state.proof_assessment_summary}`);
@@ -24,7 +24,7 @@ import {
24
24
  visualDeltaShipGateReason,
25
25
  workflowFile,
26
26
  writeState
27
- } from "./chunk-U4VNN2CT.js";
27
+ } from "./chunk-4ASMX4R6.js";
28
28
  export {
29
29
  BUNDLED_RIDDLE_PROOF_DIR,
30
30
  CHECKPOINT_CONTRACT_VERSION,
@@ -422,6 +422,21 @@ function visualDeltaShipGateReason(state = {}) {
422
422
  if (reason) return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof: ${reason}`;
423
423
  return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof`;
424
424
  }
425
+ function visualDeltaEvidenceIssueCode(state = {}, blocker = "") {
426
+ const visualDelta = visualDeltaForState(state || {});
427
+ const status = String(visualDelta.status || "").trim();
428
+ const reason = `${String(visualDelta.reason || "")}
429
+ ${blocker}`.toLowerCase();
430
+ if (status === "unmeasured") {
431
+ if (reason.includes("fetch") || reason.includes("allowlist") || reason.includes("registered domain") || reason.includes("high risk") || reason.includes("comparator")) {
432
+ return "comparator_fetch_blocked";
433
+ }
434
+ return "visual_delta_unmeasured";
435
+ }
436
+ if (status === "measured" && visualDelta.passed === false) return "semantic_proof_failed";
437
+ if (visualDeltaRequiredForState(state || {})) return "visual_delta_unmeasured";
438
+ return "semantic_proof_failed";
439
+ }
425
440
  function requiredBaselineLabelsForState(state = {}) {
426
441
  const reference = normalizedReference(state);
427
442
  const labels = [];
@@ -611,7 +626,7 @@ var CHECKPOINT_CONTRACT_SPECS = {
611
626
  description: "JSON assessment with decision, summary, recommended_stage, continue_with_stage, escalation_target, and reasons."
612
627
  }],
613
628
  response_schema: {
614
- decision: ["ready_to_ship", "needs_richer_proof"],
629
+ decision: ["ready_to_ship", "needs_richer_proof", "revise_capture"],
615
630
  summary: "string",
616
631
  recommended_stage: ["ship", "author", "implement", "recon", "verify"],
617
632
  continue_with_stage: ["ship", "author", "implement", "recon", "verify"],
@@ -814,9 +829,15 @@ function mergeStateFromParams(statePath, params) {
814
829
  const readyBlocker = assessment?.decision === "ready_to_ship" ? visualDeltaShipGateReason({ ...state, proof_assessment: assessment, proof_assessment_source: assessment.source }) : null;
815
830
  if (readyBlocker) {
816
831
  assessment.blocked_decision = assessment.decision;
817
- assessment.decision = "needs_richer_proof";
818
- if (assessment.recommended_stage === "ship") assessment.recommended_stage = "verify";
819
- if (assessment.continue_with_stage === "ship") assessment.continue_with_stage = "verify";
832
+ assessment.decision = "revise_capture";
833
+ assessment.recommended_stage = "verify";
834
+ assessment.continue_with_stage = "verify";
835
+ assessment.evidence_collection_incomplete = true;
836
+ assessment.recovery_stage = "verify";
837
+ assessment.recovery_reason = readyBlocker;
838
+ assessment.evidence_issue_code = visualDeltaEvidenceIssueCode(state, readyBlocker);
839
+ assessment.visual_delta = visualDeltaForState(state);
840
+ assessment.suggested_repair = "Keep the same Riddle Proof run in evidence/comparison recovery: repair or retry the visual comparator/fetch path, wait for artifact readiness if applicable, or produce a measured visual_delta artifact before proof review can mark ready_to_ship.";
820
841
  const blockers = Array.isArray(assessment.blockers) ? assessment.blockers : [];
821
842
  assessment.blockers = [...blockers, readyBlocker];
822
843
  }
@@ -835,7 +856,7 @@ function mergeStateFromParams(statePath, params) {
835
856
  }
836
857
  appendProofSummaryLine(state, `Supervising proof assessment: ${state.proof_assessment.decision || "unknown"}`);
837
858
  if (readyBlocker) {
838
- appendProofSummaryLine(state, `Ready-to-ship assessment blocked: ${readyBlocker}`);
859
+ appendProofSummaryLine(state, `Ready-to-ship assessment routed to evidence recovery: ${readyBlocker}`);
839
860
  }
840
861
  if (state.proof_assessment_summary) {
841
862
  appendProofSummaryLine(state, `Assessment summary: ${state.proof_assessment_summary}`);
@@ -1711,10 +1732,10 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
1711
1732
  const primaryShipGateNextAction = (shipGate) => {
1712
1733
  const reasons = shipGate.reasons || [];
1713
1734
  if (reasons.some((reason) => reason.includes("proof_assessment"))) {
1714
- return "resume with riddle_proof_review using decision=ready_to_ship only after the screenshots and semantic evidence visibly prove the request; otherwise choose needs_implementation or needs_richer_proof";
1735
+ return "resume with riddle_proof_review using decision=ready_to_ship only after screenshots, semantic evidence, and required comparison metrics prove the request; otherwise choose needs_implementation, revise_capture, or needs_richer_proof for the specific missing stage";
1715
1736
  }
1716
1737
  if (reasons.some((reason) => reason.includes("visual_delta"))) {
1717
- return "rerun verify with a measured before/after visual delta, or choose needs_richer_proof instead of ready_to_ship for this visual proof";
1738
+ return "keep the run in verify/evidence recovery until a measured before/after visual_delta exists; choose revise_capture rather than ready_to_ship or generic needs_richer_proof for this visual proof";
1718
1739
  }
1719
1740
  if (reasons.some((reason) => reason.includes("after_cdn") || reason.includes("verify_status"))) {
1720
1741
  return "rerun verify with stronger proof framing so after evidence is captured before shipping";
@@ -14,7 +14,7 @@ import {
14
14
  validateShipGate,
15
15
  workflowFile,
16
16
  writeState
17
- } from "./chunk-U4VNN2CT.js";
17
+ } from "./chunk-4ASMX4R6.js";
18
18
 
19
19
  // src/proof-run-engine.ts
20
20
  import { execFileSync } from "child_process";
@@ -768,10 +768,10 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
768
768
  const primaryShipGateNextAction = (shipGate) => {
769
769
  const reasons = shipGate.reasons || [];
770
770
  if (reasons.some((reason) => reason.includes("proof_assessment"))) {
771
- return "resume with riddle_proof_review using decision=ready_to_ship only after the screenshots and semantic evidence visibly prove the request; otherwise choose needs_implementation or needs_richer_proof";
771
+ return "resume with riddle_proof_review using decision=ready_to_ship only after screenshots, semantic evidence, and required comparison metrics prove the request; otherwise choose needs_implementation, revise_capture, or needs_richer_proof for the specific missing stage";
772
772
  }
773
773
  if (reasons.some((reason) => reason.includes("visual_delta"))) {
774
- return "rerun verify with a measured before/after visual delta, or choose needs_richer_proof instead of ready_to_ship for this visual proof";
774
+ return "keep the run in verify/evidence recovery until a measured before/after visual_delta exists; choose revise_capture rather than ready_to_ship or generic needs_richer_proof for this visual proof";
775
775
  }
776
776
  if (reasons.some((reason) => reason.includes("after_cdn") || reason.includes("verify_status"))) {
777
777
  return "rerun verify with stronger proof framing so after evidence is captured before shipping";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.5.42",
3
+ "version": "0.5.44",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -154,6 +154,7 @@ def author_request_payload(state, reference, baselines, current_plan, hypothesis
154
154
  'For playable/gameplay proof, start the experience, send keyboard or pointer input, sample state before/after, measure non-HUD playfield/canvas pixel deltas across time, and set window.__riddleProofEvidence.playability or playability_evidence with version riddle-proof.playability.v1.',
155
155
  'For data/audio/log/metric/custom proof, screenshots are optional; set window.__riddleProofEvidence inside page.evaluate to a JSON-serializable object with the measured observations the verifier should judge.',
156
156
  'Do not assign globalThis.__riddleProofEvidence, window.__riddleProofEvidence, or self.__riddleProofEvidence outside page.evaluate; the Riddle worker context may not expose those globals safely.',
157
+ 'When page.evaluate needs data from the outer Playwright script, pass exactly one serializable argument object; do not pass multiple positional arguments.',
157
158
  'Do not begin capture_script with page.goto unless an in-app navigation is genuinely required after the preview opens the target route.',
158
159
  'Only escalate to the human after the supervising agent concludes the workflow is genuinely stuck or not converging.',
159
160
  ],
@@ -136,6 +136,28 @@ async function run(tool, args) {
136
136
  });
137
137
  }
138
138
 
139
+ if (tool === "riddle_visual_diff") {
140
+ const {
141
+ async,
142
+ options,
143
+ stealth,
144
+ timeout_sec,
145
+ ...visualDiffOptions
146
+ } = args;
147
+ const script = `return await visualDiff(${JSON.stringify(visualDiffOptions)});`;
148
+ return core.runWithDefaults(config, {
149
+ url: args.url_before,
150
+ script,
151
+ options: { ...(options ?? {}), returnResult: true },
152
+ stealth,
153
+ timeout_sec: timeout_sec ?? 60,
154
+ async,
155
+ }, {
156
+ include: ["result", "console", "visual_diff"],
157
+ returnAsync: !!async,
158
+ });
159
+ }
160
+
139
161
  if (tool === "riddle_run") {
140
162
  return core.runWithDefaults(config, args.payload ?? args, {
141
163
  include: ["screenshot", "console", "result"],
@@ -14,6 +14,7 @@ RIDDLE_DIRECT_TOOLS = {
14
14
  'riddle_build_preview',
15
15
  'riddle_build_preview_status',
16
16
  'riddle_script',
17
+ 'riddle_visual_diff',
17
18
  'riddle_run',
18
19
  }
19
20
  CAPTURE_ARTIFACT_JSON_LIMIT = 256 * 1024
@@ -767,7 +768,7 @@ def enrich_capture_payload(payload):
767
768
  artifact_json = dict(enriched.get('_artifact_json') or {})
768
769
  artifact_errors = dict(enriched.get('_artifact_errors') or {})
769
770
 
770
- for name in ('console.json', 'proof.json'):
771
+ for name in ('console.json', 'proof.json', 'visual-diff.json'):
771
772
  if name in artifact_json or name in artifact_errors:
772
773
  continue
773
774
  item = capture_output_item(enriched, name)
@@ -798,6 +799,10 @@ def enrich_capture_payload(payload):
798
799
  enriched['result'] = result
799
800
  break
800
801
 
802
+ visual_diff_json = artifact_json.get('visual-diff.json')
803
+ if isinstance(visual_diff_json, dict) and not enriched.get('visual_diff'):
804
+ enriched['visual_diff'] = visual_diff_json
805
+
801
806
  return enriched
802
807
 
803
808
 
@@ -55,13 +55,24 @@ MIN_VISUAL_CHANGED_PIXELS = 5000
55
55
  VISUAL_DELTA_PERCENT_KEYS = {
56
56
  'change_pct', 'change_percent', 'changed_percent', 'percent_changed',
57
57
  'diff_percent', 'visual_delta_percent', 'pixel_change_percent',
58
+ 'changepercent', 'changedpercent', 'percentchanged', 'diffpercent',
59
+ 'visualdeltapercent', 'pixelchangepercent', 'difference_percent',
60
+ 'differencepercent', 'diff_percentage', 'diffpercentage',
61
+ 'mismatch_percent', 'mismatchpercent', 'percentage', 'percent',
58
62
  }
59
63
  VISUAL_DELTA_RATIO_KEYS = {
60
64
  'change_ratio', 'changed_ratio', 'diff_ratio', 'visual_delta_ratio',
65
+ 'changeratio', 'changedratio', 'diffratio', 'visualdeltaratio',
66
+ 'difference_ratio', 'differenceratio', 'mismatch_ratio', 'mismatchratio',
61
67
  }
62
68
  VISUAL_CHANGED_PIXEL_KEYS = {
63
69
  'changed_pixels', 'changed_pixel_count', 'changedpixels',
64
70
  'diff_pixels', 'pixel_delta', 'visual_delta_pixels',
71
+ 'diffpixels', 'pixeldelta', 'visualdeltapixels', 'different_pixels',
72
+ 'differentpixels', 'mismatch_pixels', 'mismatchpixels', 'pixels_changed',
73
+ 'pixelschanged', 'diff_pixel_count', 'diffpixelcount',
74
+ 'pixel_diff_count', 'pixeldiffcount', 'different_pixel_count',
75
+ 'differentpixelcount', 'num_different_pixels', 'numdifferentpixels',
65
76
  }
66
77
  VISUAL_TOTAL_PIXEL_KEYS = {
67
78
  'total_pixels', 'total_pixel_count', 'pixel_count', 'totalpixels',
@@ -800,6 +811,7 @@ def extract_visual_delta(payload):
800
811
  payload = enrich_capture_payload(payload)
801
812
  result = payload.get('result') if isinstance(payload, dict) else {}
802
813
  proof_json = payload.get('_proof_json') if isinstance(payload, dict) else {}
814
+ artifact_json = payload.get('_artifact_json') if isinstance(payload, dict) else {}
803
815
  proof_evidence = extract_proof_evidence(payload)
804
816
  screenshot_url = extract_screenshot_url(payload)
805
817
  artifact_summary = summarize_capture_artifacts(payload)
@@ -807,6 +819,11 @@ def extract_visual_delta(payload):
807
819
  payload if isinstance(payload, dict) else {},
808
820
  result if isinstance(result, dict) else {},
809
821
  proof_json if isinstance(proof_json, dict) else {},
822
+ payload.get('visual_diff') if isinstance(payload, dict) and isinstance(payload.get('visual_diff'), dict) else {},
823
+ payload.get('visualDiff') if isinstance(payload, dict) and isinstance(payload.get('visualDiff'), dict) else {},
824
+ result.get('visual_diff') if isinstance(result, dict) and isinstance(result.get('visual_diff'), dict) else {},
825
+ result.get('visualDiff') if isinstance(result, dict) and isinstance(result.get('visualDiff'), dict) else {},
826
+ artifact_json.get('visual-diff.json') if isinstance(artifact_json, dict) and isinstance(artifact_json.get('visual-diff.json'), dict) else {},
810
827
  proof_evidence,
811
828
  ]
812
829
 
@@ -899,6 +916,114 @@ def extract_visual_delta(payload):
899
916
  }
900
917
 
901
918
 
919
+ def visual_delta_baseline_candidate(state, results):
920
+ reference = str(state.get('requested_reference') or state.get('reference') or 'both').strip().lower()
921
+ baseline = (results.get('baseline') or {}) if isinstance(results, dict) else {}
922
+ candidates = []
923
+ if reference in ('before', 'both', ''):
924
+ candidates.append(('before', (baseline.get('before') or {}).get('url') if isinstance(baseline.get('before'), dict) else ''))
925
+ if reference in ('prod', 'production', 'both'):
926
+ candidates.append(('prod', (baseline.get('prod') or {}).get('url') if isinstance(baseline.get('prod'), dict) else ''))
927
+ candidates.extend([
928
+ ('before', state.get('before_cdn') or ''),
929
+ ('prod', state.get('prod_cdn') or ''),
930
+ ])
931
+ for label, url in candidates:
932
+ text = str(url or '').strip()
933
+ if text:
934
+ return {'label': label, 'url': text}
935
+ return {'label': '', 'url': ''}
936
+
937
+
938
+ def add_visual_delta_diagnostic(visual_delta, key, value):
939
+ updated = dict(visual_delta or {})
940
+ diagnostic = dict(updated.get('diagnostic') or {})
941
+ diagnostic[key] = value
942
+ updated['diagnostic'] = diagnostic
943
+ return updated
944
+
945
+
946
+ def measure_visual_delta_against_baseline(state, results, after_payload, current_visual_delta):
947
+ if not visual_delta_applies(state.get('verification_mode')):
948
+ return current_visual_delta
949
+ if isinstance(current_visual_delta, dict) and current_visual_delta.get('status') == 'measured':
950
+ return current_visual_delta
951
+
952
+ baseline = visual_delta_baseline_candidate(state, results)
953
+ before_url = baseline.get('url') or ''
954
+ after_url = extract_screenshot_url(after_payload, 'after-proof') or str(state.get('after_cdn') or '').strip()
955
+ if not before_url or not after_url:
956
+ missing = []
957
+ if not before_url:
958
+ missing.append('baseline screenshot')
959
+ if not after_url:
960
+ missing.append('after screenshot')
961
+ return add_visual_delta_diagnostic(
962
+ current_visual_delta,
963
+ 'visual_diff_fallback',
964
+ {
965
+ 'status': 'skipped',
966
+ 'reason': 'missing ' + ' and '.join(missing),
967
+ 'baseline_label': baseline.get('label') or '',
968
+ 'before_url_present': bool(before_url),
969
+ 'after_url_present': bool(after_url),
970
+ },
971
+ )
972
+
973
+ args = {
974
+ 'url_before': before_url,
975
+ 'url_after': after_url,
976
+ 'delay_ms': 250,
977
+ 'timeout_sec': 60,
978
+ }
979
+ try:
980
+ payload = invoke_retry('riddle_visual_diff', args, retries=2, timeout=180)
981
+ append_capture_diagnostic(state, 'visual_delta', 'riddle_visual_diff', args, payload)
982
+ except Exception as exc:
983
+ return add_visual_delta_diagnostic(
984
+ current_visual_delta,
985
+ 'visual_diff_fallback',
986
+ {
987
+ 'status': 'error',
988
+ 'error': type(exc).__name__ + ': ' + str(exc)[:300],
989
+ 'baseline_label': baseline.get('label') or '',
990
+ },
991
+ )
992
+
993
+ measured = extract_visual_delta(payload)
994
+ if isinstance(measured, dict) and measured.get('status') == 'measured':
995
+ measured['source'] = 'riddle_visual_diff'
996
+ measured['comparison'] = {
997
+ 'baseline_label': baseline.get('label') or '',
998
+ 'before_url': before_url,
999
+ 'after_url': after_url,
1000
+ }
1001
+ if isinstance(payload, dict):
1002
+ diff_url = extract_screenshot_url(payload, 'visual-diff') or extract_screenshot_url(payload, 'diff')
1003
+ if diff_url:
1004
+ measured['comparison']['diff_url'] = diff_url
1005
+ measured['artifact_summary'] = summarize_capture_artifacts(payload)
1006
+ return measured
1007
+
1008
+ updated = add_visual_delta_diagnostic(
1009
+ current_visual_delta,
1010
+ 'visual_diff_fallback',
1011
+ {
1012
+ 'status': 'unmeasured',
1013
+ 'baseline_label': baseline.get('label') or '',
1014
+ 'before_url': before_url,
1015
+ 'after_url': after_url,
1016
+ 'payload_ok': payload.get('ok') if isinstance(payload, dict) else None,
1017
+ 'payload_error': str(payload.get('error') or payload.get('stderr') or '')[:500] if isinstance(payload, dict) else '',
1018
+ 'artifact_summary': summarize_capture_artifacts(payload) if isinstance(payload, dict) else {},
1019
+ },
1020
+ )
1021
+ reason = str(updated.get('reason') or '').strip()
1022
+ suffix = ' Riddle visual_diff fallback ran but did not publish a recognizable numeric delta.'
1023
+ updated['reason'] = (reason + suffix).strip() if reason else suffix.strip()
1024
+ return updated
1025
+
1026
+
902
1027
  def visual_delta_applies(verification_mode):
903
1028
  return screenshot_required_for_mode(verification_mode)
904
1029
 
@@ -1443,6 +1568,7 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
1443
1568
  if visual_delta_applies(state.get('verification_mode'))
1444
1569
  else {'status': 'not_applicable', 'passed': None, 'reason': 'Verification mode does not require visual delta gating.'}
1445
1570
  )
1571
+ visual_delta = measure_visual_delta_against_baseline(state, results, after_payload, visual_delta)
1446
1572
  semantic_context = build_semantic_context(state, results, after_observation, expected_path)
1447
1573
  artifact_contract = artifact_contract_for_mode(state.get('verification_mode'))
1448
1574
  artifact_production = artifact_production_summary(after_payload, supporting)
@@ -569,6 +569,50 @@ def run_verify_quality_ignores_proof_telemetry_console_text():
569
569
  assert unmeasured_delta['diagnostic']['after_screenshot_present'] is True
570
570
  assert 'After screenshot artifact is present' in unmeasured_delta['reason']
571
571
 
572
+ visual_diff_delta = namespace['extract_visual_delta']({
573
+ 'ok': True,
574
+ 'outputs': [{'name': 'visual-diff.json', 'url': 'https://cdn.example.com/visual-diff.json'}],
575
+ '_artifact_json': {
576
+ 'visual-diff.json': {
577
+ 'changePercent': '1.45',
578
+ 'diffPixelCount': 14094,
579
+ 'totalPixels': 972000,
580
+ },
581
+ },
582
+ })
583
+ assert visual_diff_delta['status'] == 'measured'
584
+ assert visual_diff_delta['passed'] is True
585
+ assert visual_diff_delta['change_percent'] == 1.45
586
+ assert visual_diff_delta['changed_pixels'] == 14094
587
+
588
+ fallback_calls = []
589
+
590
+ def fake_invoke_retry(tool, args, retries=3, timeout=180):
591
+ fallback_calls.append({'tool': tool, 'args': args, 'retries': retries, 'timeout': timeout})
592
+ return {
593
+ 'ok': True,
594
+ 'visual_diff': {
595
+ 'diffPercentage': 0.82,
596
+ 'differentPixels': 7970,
597
+ 'totalPixels': 972000,
598
+ },
599
+ 'outputs': [{'name': 'visual-diff.png', 'url': 'https://cdn.example.com/visual-diff.png'}],
600
+ }
601
+
602
+ namespace['invoke_retry'] = fake_invoke_retry
603
+ namespace['append_capture_diagnostic'] = lambda *args, **kwargs: None
604
+ fallback_delta = namespace['measure_visual_delta_against_baseline'](
605
+ {'verification_mode': 'visual', 'requested_reference': 'before', 'before_cdn': 'https://cdn.example.com/before.png'},
606
+ {'baseline': {'before': {'url': 'https://cdn.example.com/before.png'}}},
607
+ {'ok': True, 'screenshots': [{'name': 'after-proof.png', 'url': 'https://cdn.example.com/after.png'}]},
608
+ unmeasured_delta,
609
+ )
610
+ assert fallback_delta['status'] == 'measured'
611
+ assert fallback_delta['source'] == 'riddle_visual_diff'
612
+ assert fallback_delta['comparison']['before_url'] == 'https://cdn.example.com/before.png'
613
+ assert fallback_delta['comparison']['after_url'] == 'https://cdn.example.com/after.png'
614
+ assert fallback_calls[0]['tool'] == 'riddle_visual_diff'
615
+
572
616
  canvas_payload = {
573
617
  'bodyTextLength': 7,
574
618
  'visibleTextSample': 'Luge',