@riddledc/riddle-proof 0.8.11 → 0.8.13

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 (44) hide show
  1. package/dist/advanced/engine-harness.cjs +97 -12
  2. package/dist/advanced/engine-harness.js +2 -2
  3. package/dist/advanced/index.cjs +98 -12
  4. package/dist/advanced/index.d.cts +2 -2
  5. package/dist/advanced/index.d.ts +2 -2
  6. package/dist/advanced/index.js +4 -4
  7. package/dist/advanced/proof-run-core.cjs +31 -1
  8. package/dist/advanced/proof-run-core.d.cts +1 -1
  9. package/dist/advanced/proof-run-core.d.ts +1 -1
  10. package/dist/advanced/proof-run-core.js +3 -1
  11. package/dist/advanced/proof-run-engine.cjs +46 -12
  12. package/dist/advanced/proof-run-engine.d.cts +2 -2
  13. package/dist/advanced/proof-run-engine.d.ts +2 -2
  14. package/dist/advanced/proof-run-engine.js +2 -2
  15. package/dist/advanced/runner.js +2 -2
  16. package/dist/{chunk-5N5QFI2S.js → chunk-7GZY5PLT.js} +31 -1
  17. package/dist/{chunk-46DDSZJR.js → chunk-JBY2SU5U.js} +18 -12
  18. package/dist/{chunk-5N6MQCLC.js → chunk-NGX4SUQN.js} +1 -1
  19. package/dist/{chunk-BBUO7HM4.js → chunk-RTLA6CPP.js} +53 -1
  20. package/dist/{chunk-2PXL3RDB.js → chunk-SZUC4MDN.js} +1 -1
  21. package/dist/cli/index.js +3 -3
  22. package/dist/cli.cjs +97 -12
  23. package/dist/cli.js +3 -3
  24. package/dist/engine-harness.cjs +97 -12
  25. package/dist/engine-harness.js +2 -2
  26. package/dist/index.cjs +97 -12
  27. package/dist/index.js +3 -3
  28. package/dist/{proof-run-core-CE0jx7wL.d.cts → proof-run-core-C8FDUhle.d.cts} +5 -2
  29. package/dist/{proof-run-core-CE0jx7wL.d.ts → proof-run-core-C8FDUhle.d.ts} +5 -2
  30. package/dist/proof-run-core.cjs +31 -1
  31. package/dist/proof-run-core.d.cts +1 -1
  32. package/dist/proof-run-core.d.ts +1 -1
  33. package/dist/proof-run-core.js +3 -1
  34. package/dist/{proof-run-engine-BomAcXhA.d.ts → proof-run-engine-By7oLsF-.d.ts} +1 -1
  35. package/dist/{proof-run-engine-B7DCPzpK.d.cts → proof-run-engine-D80hVFMf.d.cts} +1 -1
  36. package/dist/proof-run-engine.cjs +46 -12
  37. package/dist/proof-run-engine.d.cts +2 -2
  38. package/dist/proof-run-engine.d.ts +2 -2
  39. package/dist/proof-run-engine.js +2 -2
  40. package/dist/runner.js +2 -2
  41. package/package.json +1 -1
  42. package/runtime/lib/verify.py +266 -22
  43. package/runtime/tests/recon_verify_smoke.py +291 -4
  44. package/runtime/tests/trust_boundary_regression.py +18 -0
@@ -505,6 +505,25 @@ function visualDeltaShipGateReason(state = {}) {
505
505
  if (reason) return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof: ${reason}`;
506
506
  return `visual_delta.status=${status} blocks ready_to_ship for visual/UI proof`;
507
507
  }
508
+ function proofAssessmentHardBlockersForState(state = {}) {
509
+ const request = objectValue(state?.proof_assessment_request);
510
+ const blockers = [];
511
+ const add = (value) => {
512
+ if (typeof value !== "string") return;
513
+ const trimmed = value.trim();
514
+ if (trimmed && !blockers.includes(trimmed)) blockers.push(trimmed);
515
+ };
516
+ if (Array.isArray(request.hard_blockers)) {
517
+ for (const blocker of request.hard_blockers) add(blocker);
518
+ }
519
+ add(state?.structured_interaction_capture_failure_summary);
520
+ add(state?.structured_interaction_failure_summary);
521
+ const mergeRecommendation = String(state?.merge_recommendation || "").trim();
522
+ if (mergeRecommendation === "do-not-merge" && blockers.length) {
523
+ add("merge_recommendation=do-not-merge because the proof bundle contains hard blockers.");
524
+ }
525
+ return blockers;
526
+ }
508
527
  function visualDeltaEvidenceIssueCode(state = {}, blocker = "") {
509
528
  const visualDelta = visualDeltaForState(state || {});
510
529
  const status = String(visualDelta.status || "").trim();
@@ -539,6 +558,7 @@ function validateShipGate(state = {}) {
539
558
  const visualDelta = visualDeltaForState(state);
540
559
  const visualDeltaRequired = visualDeltaRequiredForState(state);
541
560
  const visualDeltaBlocker = visualDeltaShipGateReason(state);
561
+ const hardBlockers = proofAssessmentHardBlockersForState(state);
542
562
  const reasons = [];
543
563
  if (!["before", "prod", "both"].includes(reference)) {
544
564
  reasons.push(`reference must be before, prod, or both; got ${reference}`);
@@ -570,6 +590,9 @@ function validateShipGate(state = {}) {
570
590
  if (visualDeltaBlocker) {
571
591
  reasons.push(visualDeltaBlocker);
572
592
  }
593
+ for (const blocker of hardBlockers) {
594
+ reasons.push(`proof hard blocker prevents ready_to_ship: ${blocker}`);
595
+ }
573
596
  return {
574
597
  ok: reasons.length === 0,
575
598
  reasons,
@@ -586,7 +609,8 @@ function validateShipGate(state = {}) {
586
609
  proof_assessment_source: proofAssessment.source,
587
610
  visual_delta_required: visualDeltaRequired,
588
611
  visual_delta_status: typeof visualDelta.status === "string" ? visualDelta.status : null,
589
- visual_delta_passed: typeof visualDelta.passed === "boolean" ? visualDelta.passed : null
612
+ visual_delta_passed: typeof visualDelta.passed === "boolean" ? visualDelta.passed : null,
613
+ hard_blockers: hardBlockers
590
614
  }
591
615
  };
592
616
  }
@@ -700,6 +724,10 @@ var CHECKPOINT_CONTRACT_SPECS = {
700
724
  }],
701
725
  required_state: ["verify_decision_request"]
702
726
  },
727
+ verify_capture_blocked: {
728
+ purpose: "Verify capture produced conclusive failed browser evidence and should stop instead of retrying proof authoring.",
729
+ required_state: ["verify_decision_request"]
730
+ },
703
731
  verify_supervisor_judgment: {
704
732
  purpose: "Supervising agent judges whether captured evidence proves the change is ready to ship.",
705
733
  accepted_inputs: [{
@@ -1284,14 +1312,18 @@ function verifyAssessment(state) {
1284
1312
  };
1285
1313
  }
1286
1314
  if (state?.verify_status === "capture_incomplete") {
1315
+ const captureQuality = verifyDecision?.capture_quality || {};
1316
+ const terminalBlocker = captureQuality?.terminal_blocker === true || captureQuality?.blocking === true;
1317
+ const recommendedStage = terminalBlocker ? null : verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author";
1318
+ const continueWithStage = terminalBlocker ? null : verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author";
1287
1319
  return {
1288
- decision: verifyDecision?.capture_quality?.decision || "revise_capture",
1320
+ decision: captureQuality?.decision || "revise_capture",
1289
1321
  summary: verifyDecision?.summary || "Verify needs another internal capture iteration before the evidence can be judged.",
1290
- recommendedStage: verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author",
1291
- continueWithStage: verifyDecision?.continue_with_stage || verifyDecision?.recommended_stage || "author",
1322
+ recommendedStage,
1323
+ continueWithStage,
1292
1324
  escalationTarget: "agent",
1293
- reasons: Array.isArray(verifyDecision?.capture_quality?.reasons) ? verifyDecision.capture_quality.reasons : [],
1294
- raw: verifyDecision?.capture_quality || verifyDecision,
1325
+ reasons: Array.isArray(captureQuality?.reasons) ? captureQuality.reasons : [],
1326
+ raw: captureQuality || verifyDecision,
1295
1327
  source: "workflow_capture"
1296
1328
  };
1297
1329
  }
@@ -2511,7 +2543,9 @@ ${implementRes.stderr || ""}`;
2511
2543
  convergenceSignals
2512
2544
  };
2513
2545
  if (verifyStatus !== "evidence_captured") {
2514
- if ((verifyContinueWithStage || verifyRecommendedStage || "author") === "author") {
2546
+ const captureQuality = verifyDecisionRequest?.capture_quality || {};
2547
+ const captureTerminalBlocker = captureQuality?.terminal_blocker === true || captureQuality?.blocking === true;
2548
+ if (!captureTerminalBlocker && (verifyContinueWithStage || verifyRecommendedStage || "author") === "author") {
2515
2549
  updateState(config.statePath, (currentState) => {
2516
2550
  currentState.author_status = "needs_authoring";
2517
2551
  currentState.proof_plan_status = "needs_authoring";
@@ -2519,7 +2553,7 @@ ${implementRes.stderr || ""}`;
2519
2553
  });
2520
2554
  state = readState(config.statePath);
2521
2555
  }
2522
- const checkpointName = "verify_capture_retry";
2556
+ const checkpointName = captureTerminalBlocker ? "verify_capture_blocked" : "verify_capture_retry";
2523
2557
  const summary = stringValue(proofAssessment.summary) || "Verify ran, but the proof packet still needs internal capture-plan work before it should ship.";
2524
2558
  recordAttempt("verify", "checkpoint", summary, {
2525
2559
  autoApproved: verifyRes.autoApproved || false,
@@ -2532,11 +2566,11 @@ ${implementRes.stderr || ""}`;
2532
2566
  summary,
2533
2567
  {
2534
2568
  ok: true,
2535
- nextActions: ["inspect_after_capture", "continue_internal_loop_with_checkpoint", "return_to_recon_if_baseline_is_wrong"],
2569
+ nextActions: captureTerminalBlocker ? ["inspect_after_capture", "report_specific_browser_evidence_blocker", "start_a_new_run_after_the_product_or_script_is_fixed"] : ["inspect_after_capture", "continue_internal_loop_with_checkpoint", "return_to_recon_if_baseline_is_wrong"],
2536
2570
  advanceOptions: verifyLoopAdvanceOptions,
2537
- recommendedAdvanceStage: verifyRecommendedStage || "author",
2538
- continueWithStage: verifyContinueWithStage || "author",
2539
- blocking: false,
2571
+ recommendedAdvanceStage: captureTerminalBlocker ? null : verifyRecommendedStage || "author",
2572
+ continueWithStage: captureTerminalBlocker ? null : verifyContinueWithStage || "author",
2573
+ blocking: captureTerminalBlocker,
2540
2574
  details: verifyDetails,
2541
2575
  verifyStatus,
2542
2576
  verifySummary,
@@ -1,2 +1,2 @@
1
- import './proof-run-core-CE0jx7wL.cjs';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-B7DCPzpK.cjs';
1
+ import './proof-run-core-C8FDUhle.cjs';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-D80hVFMf.cjs';
@@ -1,2 +1,2 @@
1
- import './proof-run-core-CE0jx7wL.js';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-BomAcXhA.js';
1
+ import './proof-run-core-C8FDUhle.js';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-By7oLsF-.js';
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createRiddleProofEngine,
3
3
  executeWorkflow
4
- } from "./chunk-46DDSZJR.js";
5
- import "./chunk-5N5QFI2S.js";
4
+ } from "./chunk-JBY2SU5U.js";
5
+ import "./chunk-7GZY5PLT.js";
6
6
  import "./chunk-MLKGABMK.js";
7
7
  export {
8
8
  createRiddleProofEngine,
package/dist/runner.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  runRiddleProof
3
- } from "./chunk-5N6MQCLC.js";
3
+ } from "./chunk-NGX4SUQN.js";
4
4
  import "./chunk-YZUVEJ5B.js";
5
5
  import "./chunk-FMOYUYH2.js";
6
- import "./chunk-5N5QFI2S.js";
6
+ import "./chunk-7GZY5PLT.js";
7
7
  import "./chunk-4FOHZ7JG.js";
8
8
  import "./chunk-VY4Y5U57.js";
9
9
  import "./chunk-MLKGABMK.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.8.11",
3
+ "version": "0.8.13",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -2029,7 +2029,7 @@ def interaction_proof_route_match(expected_path, proof_evidence):
2029
2029
  return None
2030
2030
  for record in proof_evidence_records_deep(proof_evidence):
2031
2031
  flag = explicit_route_match_flag(record)
2032
- candidate = terminal_path_from_record(record)
2032
+ candidate = observed_terminal_path_from_record(record)
2033
2033
  if candidate and route_matches_expected(expected, candidate):
2034
2034
  return {
2035
2035
  'matched': True,
@@ -2042,26 +2042,84 @@ def interaction_proof_route_match(expected_path, proof_evidence):
2042
2042
 
2043
2043
 
2044
2044
  EXPLICIT_TERMINAL_PATH_KEYS = (
2045
+ 'expected_url', 'expectedUrl',
2046
+ 'expected_href', 'expectedHref',
2045
2047
  'expected_terminal_path', 'expectedTerminalPath',
2046
2048
  'expected_terminal_url', 'expectedTerminalUrl',
2049
+ 'expected_terminal_href', 'expectedTerminalHref',
2047
2050
  'expected_terminal_route', 'expectedTerminalRoute',
2051
+ 'expected_route', 'expectedRoute',
2048
2052
  'terminal_path', 'terminalPath',
2049
2053
  'terminal_url', 'terminalUrl',
2054
+ 'terminal_href', 'terminalHref',
2050
2055
  'terminal_route', 'terminalRoute',
2051
2056
  'expected_after_path', 'expectedAfterPath',
2052
2057
  'expected_after_url', 'expectedAfterUrl',
2058
+ 'expected_after_href', 'expectedAfterHref',
2053
2059
  'expected_after_route', 'expectedAfterRoute',
2054
2060
  'after_path', 'afterPath',
2055
2061
  'after_url', 'afterUrl',
2062
+ 'after_href', 'afterHref',
2056
2063
  'after_route', 'afterRoute',
2057
2064
  'expected_final_path', 'expectedFinalPath',
2058
2065
  'expected_final_url', 'expectedFinalUrl',
2066
+ 'expected_final_href', 'expectedFinalHref',
2059
2067
  'expected_final_route', 'expectedFinalRoute',
2060
2068
  'final_path', 'finalPath',
2061
2069
  'final_url', 'finalUrl',
2070
+ 'final_href', 'finalHref',
2062
2071
  'final_route', 'finalRoute',
2063
2072
  )
2064
- LOCATION_PATH_KEYS = ('path', 'pathname', 'route', 'url', 'href')
2073
+ EXPECTED_TERMINAL_PATH_KEYS = (
2074
+ 'expected_url', 'expectedUrl',
2075
+ 'expected_href', 'expectedHref',
2076
+ 'expected_path', 'expectedPath',
2077
+ 'expected_route', 'expectedRoute',
2078
+ 'expected_terminal_path', 'expectedTerminalPath',
2079
+ 'expected_terminal_url', 'expectedTerminalUrl',
2080
+ 'expected_terminal_href', 'expectedTerminalHref',
2081
+ 'expected_terminal_route', 'expectedTerminalRoute',
2082
+ 'expected_after_path', 'expectedAfterPath',
2083
+ 'expected_after_url', 'expectedAfterUrl',
2084
+ 'expected_after_href', 'expectedAfterHref',
2085
+ 'expected_after_route', 'expectedAfterRoute',
2086
+ 'expected_final_path', 'expectedFinalPath',
2087
+ 'expected_final_url', 'expectedFinalUrl',
2088
+ 'expected_final_href', 'expectedFinalHref',
2089
+ 'expected_final_route', 'expectedFinalRoute',
2090
+ )
2091
+ OBSERVED_TERMINAL_PATH_KEYS = (
2092
+ 'terminal_path', 'terminalPath',
2093
+ 'terminal_url', 'terminalUrl',
2094
+ 'terminal_href', 'terminalHref',
2095
+ 'terminal_route', 'terminalRoute',
2096
+ 'after_path', 'afterPath',
2097
+ 'after_url', 'afterUrl',
2098
+ 'after_href', 'afterHref',
2099
+ 'after_route', 'afterRoute',
2100
+ 'final_path', 'finalPath',
2101
+ 'final_url', 'finalUrl',
2102
+ 'final_href', 'finalHref',
2103
+ 'final_route', 'finalRoute',
2104
+ )
2105
+ FULL_LOCATION_PATH_KEYS = (
2106
+ 'url', 'href',
2107
+ 'hrefNormalized', 'href_normalized',
2108
+ 'terminalUrl', 'terminal_url',
2109
+ 'afterUrl', 'after_url',
2110
+ 'finalUrl', 'final_url',
2111
+ 'currentUrl', 'current_url',
2112
+ 'pathWithSearchAndHash', 'path_with_search_and_hash',
2113
+ 'fullPath', 'full_path',
2114
+ )
2115
+ PARTIAL_LOCATION_PATH_KEYS = (
2116
+ 'route',
2117
+ 'path',
2118
+ 'pathname',
2119
+ 'normalizedPath', 'normalized_path',
2120
+ 'rawPath', 'raw_path',
2121
+ )
2122
+ LOCATION_PATH_KEYS = FULL_LOCATION_PATH_KEYS + PARTIAL_LOCATION_PATH_KEYS
2065
2123
  AFTER_STATE_KEYS = (
2066
2124
  'after', 'after_state', 'afterState',
2067
2125
  'expected_after', 'expectedAfter',
@@ -2070,6 +2128,19 @@ AFTER_STATE_KEYS = (
2070
2128
  'final', 'final_state', 'finalState',
2071
2129
  'expected_final', 'expectedFinal',
2072
2130
  )
2131
+ EXPECTED_STATE_KEYS = (
2132
+ 'expected', 'expectation',
2133
+ 'route_expectation', 'routeExpectation',
2134
+ 'expected_after', 'expectedAfter',
2135
+ 'expected_terminal', 'expectedTerminal',
2136
+ 'expected_final', 'expectedFinal',
2137
+ )
2138
+ OBSERVED_STATE_KEYS = (
2139
+ 'observed', 'actual',
2140
+ 'after', 'after_state', 'afterState',
2141
+ 'terminal', 'terminal_state', 'terminalState',
2142
+ 'final', 'final_state', 'finalState',
2143
+ )
2073
2144
  EVIDENCE_CONTAINER_KEYS = (
2074
2145
  'proofEvidence', 'proof_evidence',
2075
2146
  'interactionEvidence', 'interaction_evidence',
@@ -2096,10 +2167,10 @@ def path_candidate(value):
2096
2167
  return ''
2097
2168
 
2098
2169
 
2099
- def record_path_candidate(record, allow_location_keys=False):
2170
+ def record_path_candidate_for_keys(record, keys, allow_location_keys=False):
2100
2171
  if not isinstance(record, dict):
2101
2172
  return ''
2102
- keys = list(EXPLICIT_TERMINAL_PATH_KEYS)
2173
+ keys = list(keys)
2103
2174
  if allow_location_keys:
2104
2175
  keys.extend(LOCATION_PATH_KEYS)
2105
2176
  for key in keys:
@@ -2109,6 +2180,10 @@ def record_path_candidate(record, allow_location_keys=False):
2109
2180
  return ''
2110
2181
 
2111
2182
 
2183
+ def record_path_candidate(record, allow_location_keys=False):
2184
+ return record_path_candidate_for_keys(record, EXPLICIT_TERMINAL_PATH_KEYS, allow_location_keys)
2185
+
2186
+
2112
2187
  def terminal_path_from_record(record, depth=0):
2113
2188
  if not isinstance(record, dict) or depth > 4:
2114
2189
  return ''
@@ -2151,6 +2226,74 @@ def terminal_path_from_record(record, depth=0):
2151
2226
  return ''
2152
2227
 
2153
2228
 
2229
+ def expected_terminal_path_from_record(record, depth=0):
2230
+ if not isinstance(record, dict) or depth > 4:
2231
+ return ''
2232
+ candidate = record_path_candidate_for_keys(record, EXPECTED_TERMINAL_PATH_KEYS)
2233
+ if candidate:
2234
+ return candidate
2235
+ for key in EXPECTED_STATE_KEYS:
2236
+ value = record.get(key)
2237
+ if isinstance(value, dict):
2238
+ candidate = (
2239
+ record_path_candidate_for_keys(value, EXPECTED_TERMINAL_PATH_KEYS, allow_location_keys=True)
2240
+ or expected_terminal_path_from_record(value, depth + 1)
2241
+ )
2242
+ if candidate:
2243
+ return candidate
2244
+ elif isinstance(value, list):
2245
+ for item in value:
2246
+ candidate = expected_terminal_path_from_record(item, depth + 1)
2247
+ if candidate:
2248
+ return candidate
2249
+ for key in EVIDENCE_CONTAINER_KEYS:
2250
+ value = record.get(key)
2251
+ if isinstance(value, dict):
2252
+ candidate = expected_terminal_path_from_record(value, depth + 1)
2253
+ if candidate:
2254
+ return candidate
2255
+ elif isinstance(value, list):
2256
+ for item in value:
2257
+ candidate = expected_terminal_path_from_record(item, depth + 1)
2258
+ if candidate:
2259
+ return candidate
2260
+ return ''
2261
+
2262
+
2263
+ def observed_terminal_path_from_record(record, depth=0):
2264
+ if not isinstance(record, dict) or depth > 4:
2265
+ return ''
2266
+ candidate = record_path_candidate_for_keys(record, OBSERVED_TERMINAL_PATH_KEYS)
2267
+ if candidate:
2268
+ return candidate
2269
+ for key in OBSERVED_STATE_KEYS:
2270
+ value = record.get(key)
2271
+ if isinstance(value, dict):
2272
+ candidate = (
2273
+ record_path_candidate_for_keys(value, OBSERVED_TERMINAL_PATH_KEYS, allow_location_keys=True)
2274
+ or observed_terminal_path_from_record(value, depth + 1)
2275
+ )
2276
+ if candidate:
2277
+ return candidate
2278
+ elif isinstance(value, list):
2279
+ for item in value:
2280
+ candidate = observed_terminal_path_from_record(item, depth + 1)
2281
+ if candidate:
2282
+ return candidate
2283
+ for key in EVIDENCE_CONTAINER_KEYS:
2284
+ value = record.get(key)
2285
+ if isinstance(value, dict):
2286
+ candidate = observed_terminal_path_from_record(value, depth + 1)
2287
+ if candidate:
2288
+ return candidate
2289
+ elif isinstance(value, list):
2290
+ for item in value:
2291
+ candidate = observed_terminal_path_from_record(item, depth + 1)
2292
+ if candidate:
2293
+ return candidate
2294
+ return ''
2295
+
2296
+
2154
2297
  def text_path_candidate(value):
2155
2298
  if not isinstance(value, str):
2156
2299
  return ''
@@ -2383,19 +2526,41 @@ def failed_interaction_evidence_summary(proof_evidence):
2383
2526
  return summary
2384
2527
 
2385
2528
 
2529
+ def interaction_capture_failure_evidence_summary(proof_evidence):
2530
+ for record in proof_evidence_records_deep(proof_evidence):
2531
+ if not isinstance(record, dict):
2532
+ continue
2533
+ version = str(record.get('version') or '').strip()
2534
+ source = str(record.get('source') or '').strip()
2535
+ if version != 'riddle-proof.interaction.capture-failure.v1' and source != 'verify_capture_failure':
2536
+ continue
2537
+ summary = str(record.get('evidence_summary') or '').strip() or 'Interaction capture failed before usable authored proof evidence was emitted.'
2538
+ failures = collect_interaction_failed_assertions(record)
2539
+ if failures:
2540
+ summary += ' Failed checks: ' + ', '.join(failures[:8]) + '.'
2541
+ error = str(record.get('capture_error') or record.get('error') or '').strip()
2542
+ if error:
2543
+ summary += ' Capture script error: ' + error[:300]
2544
+ return summary
2545
+ return ''
2546
+
2547
+
2386
2548
  def interaction_terminal_path_from_evidence(proof_evidence):
2387
2549
  for record in proof_evidence_records(proof_evidence):
2388
- candidate = terminal_path_from_record(record)
2550
+ candidate = expected_terminal_path_from_record(record)
2389
2551
  if candidate:
2390
2552
  return candidate, 'proof_evidence_contract'
2391
- if interaction_assertions_pass(proof_evidence):
2553
+ route_evidence_passed = interaction_assertions_pass(proof_evidence)
2554
+ if not route_evidence_passed:
2555
+ for record in proof_evidence_records_deep(proof_evidence):
2556
+ if interaction_assertions_pass(record) or explicit_route_match_flag(record) is True:
2557
+ route_evidence_passed = True
2558
+ break
2559
+ if route_evidence_passed:
2392
2560
  for record in proof_evidence_records(proof_evidence):
2393
- for key in AFTER_STATE_KEYS:
2394
- value = record.get(key)
2395
- if isinstance(value, dict):
2396
- candidate = record_path_candidate(value, allow_location_keys=True)
2397
- if candidate:
2398
- return candidate, 'proof_evidence_after_state'
2561
+ candidate = observed_terminal_path_from_record(record)
2562
+ if candidate:
2563
+ return candidate, 'proof_evidence_contract'
2399
2564
  return '', ''
2400
2565
 
2401
2566
 
@@ -2428,6 +2593,33 @@ def interaction_terminal_path_from_state(state):
2428
2593
  return '', ''
2429
2594
 
2430
2595
 
2596
+ def proof_evidence_should_override_state_terminal_path(state_candidate, evidence_candidate, proof_evidence):
2597
+ if not evidence_candidate:
2598
+ return False
2599
+ if not state_candidate:
2600
+ return True
2601
+ state_parts = route_parts(state_candidate)
2602
+ evidence_parts = route_parts(evidence_candidate)
2603
+ if (
2604
+ state_parts.get('pathname') == evidence_parts.get('pathname')
2605
+ and (
2606
+ (evidence_parts.get('query') and not state_parts.get('query'))
2607
+ or (evidence_parts.get('hash') and not state_parts.get('hash'))
2608
+ )
2609
+ ):
2610
+ return True
2611
+ if route_matches_expected(state_candidate, evidence_candidate):
2612
+ return False
2613
+ if interaction_assertions_pass(proof_evidence):
2614
+ return True
2615
+ for record in proof_evidence_records_deep(proof_evidence):
2616
+ if interaction_assertions_pass(record):
2617
+ return True
2618
+ if explicit_route_match_flag(record) is True:
2619
+ return True
2620
+ return False
2621
+
2622
+
2431
2623
  def expected_path_for_verify(state, start_path, proof_evidence):
2432
2624
  mode = normalized_verification_mode(state.get('verification_mode'))
2433
2625
  normalized_start = normalize_observed_path(start_path) or '/'
@@ -2445,9 +2637,14 @@ def expected_path_for_verify(state, start_path, proof_evidence):
2445
2637
  'expected_query': start_parts['query'],
2446
2638
  'expected_hash': start_parts['hash'],
2447
2639
  }
2448
- candidate, source = interaction_terminal_path_from_state(state)
2640
+ state_candidate, state_source = interaction_terminal_path_from_state(state)
2641
+ evidence_candidate, evidence_source = interaction_terminal_path_from_evidence(proof_evidence)
2642
+ if proof_evidence_should_override_state_terminal_path(state_candidate, evidence_candidate, proof_evidence):
2643
+ candidate, source = evidence_candidate, evidence_source
2644
+ else:
2645
+ candidate, source = state_candidate, state_source
2449
2646
  if not candidate:
2450
- candidate, source = interaction_terminal_path_from_evidence(proof_evidence)
2647
+ candidate, source = evidence_candidate, evidence_source
2451
2648
  expected = candidate or normalized_start
2452
2649
  expected_parts = route_parts(expected)
2453
2650
  return expected, {
@@ -2838,10 +3035,17 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2838
3035
 
2839
3036
  if proof_evidence_blocker:
2840
3037
  reasons.append(proof_evidence_blocker)
2841
- decision = 'missing_proof_evidence'
3038
+ interaction_capture_blocker = (
3039
+ proof_evidence_blocker.startswith('Interaction capture ')
3040
+ or 'Interaction capture failed before usable authored proof evidence was emitted' in proof_evidence_blocker
3041
+ or 'Interaction capture reached a different terminal route' in proof_evidence_blocker
3042
+ )
3043
+ decision = 'failed_interaction_capture' if interaction_capture_blocker else 'missing_proof_evidence'
2842
3044
  if 'proof_evidence_present=false' in proof_evidence_blocker:
2843
3045
  decision = 'failed_proof_evidence'
2844
3046
  reasons.append('The capture reached usable page context, but the proof evidence explicitly failed its own required audio gate.')
3047
+ elif interaction_capture_blocker:
3048
+ reasons.append('The capture produced conclusive structured interaction-failure evidence, so this run should block with that specific browser evidence instead of re-authoring in a loop.')
2845
3049
  else:
2846
3050
  reasons.append('The capture reached usable page context, but the proof script did not emit the structured evidence required for this verification mode.')
2847
3051
  if route_mismatch:
@@ -2852,7 +3056,10 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2852
3056
  (route_mismatch.get('observed_after_path') or '(unknown)') +
2853
3057
  '.'
2854
3058
  )
2855
- reasons.append('Return to author so the capture script can expose passing proof evidence before verify asks for a supervising-agent judgment.')
3059
+ if interaction_capture_blocker:
3060
+ reasons.append('Do not ask the authoring loop to infer a new route; the captured browser evidence is the terminal blocker.')
3061
+ else:
3062
+ reasons.append('Return to author so the capture script can expose passing proof evidence before verify asks for a supervising-agent judgment.')
2856
3063
  summary = proof_evidence_blocker
2857
3064
  if route_mismatch:
2858
3065
  summary += (
@@ -2868,8 +3075,10 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2868
3075
  return {
2869
3076
  'decision': decision,
2870
3077
  'summary': summary,
2871
- 'recommended_stage': 'author',
2872
- 'continue_with_stage': 'author',
3078
+ 'recommended_stage': None if interaction_capture_blocker else 'author',
3079
+ 'continue_with_stage': None if interaction_capture_blocker else 'author',
3080
+ 'blocking': bool(interaction_capture_blocker),
3081
+ 'terminal_blocker': bool(interaction_capture_blocker),
2873
3082
  'reasons': reasons,
2874
3083
  'mismatch': route_mismatch,
2875
3084
  }
@@ -2883,6 +3092,25 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2883
3092
  and route_expectation.get('terminal_path')
2884
3093
  and route_expectation_source != 'recon_start_path'
2885
3094
  )
3095
+ if authored_terminal_route:
3096
+ expected = route_mismatch.get('expected_path') or ''
3097
+ observed = route_mismatch.get('observed_after_path') or ''
3098
+ summary = 'Interaction proof terminal route mismatch: expected ' + (expected or '(unknown)') + ', got ' + (observed or '(unknown)') + '.'
3099
+ if error_messages:
3100
+ reasons.append('Capture script error: ' + error_messages[0][:500])
3101
+ summary += ' Capture script error: ' + error_messages[0][:300]
3102
+ reasons.append('Route mismatch: expected after capture path ' + (expected or '(unknown)') + ', observed ' + (observed or '(unknown)') + '.')
3103
+ reasons.append('The terminal route came from authored interaction proof evidence, so the captured browser evidence is the terminal blocker instead of an author retry loop.')
3104
+ return {
3105
+ 'decision': 'failed_interaction_capture',
3106
+ 'summary': summary,
3107
+ 'recommended_stage': None,
3108
+ 'continue_with_stage': None,
3109
+ 'blocking': True,
3110
+ 'terminal_blocker': True,
3111
+ 'reasons': reasons,
3112
+ 'mismatch': route_mismatch,
3113
+ }
2886
3114
  recommended_stage = 'recon' if 'wrong route' in reason and not authored_terminal_route else 'author'
2887
3115
  mismatch = None
2888
3116
  if recommended_stage == 'recon':
@@ -3645,12 +3873,17 @@ if proof_evidence_required_for_mode(s.get('verification_mode')):
3645
3873
  summary_lines.append('Structured proof evidence gate: ' + proof_evidence_blocker)
3646
3874
 
3647
3875
  structured_interaction_failure_summary = ''
3876
+ structured_interaction_capture_failure_summary = ''
3648
3877
  proof_evidence = evidence_bundle.get('proof_evidence')
3649
3878
  if verification_mode in INTERACTION_MODES and proof_evidence is not None:
3650
3879
  structured_interaction_failure_summary = failed_interaction_evidence_summary(proof_evidence)
3880
+ structured_interaction_capture_failure_summary = interaction_capture_failure_evidence_summary(proof_evidence)
3651
3881
  if structured_interaction_failure_summary:
3652
3882
  summary_lines.append('Structured interaction evidence gate: ' + structured_interaction_failure_summary)
3883
+ if structured_interaction_capture_failure_summary:
3884
+ summary_lines.append('Structured interaction capture blocker: ' + structured_interaction_capture_failure_summary)
3653
3885
  s['structured_interaction_failure_summary'] = structured_interaction_failure_summary
3886
+ s['structured_interaction_capture_failure_summary'] = structured_interaction_capture_failure_summary
3654
3887
 
3655
3888
  visual_delta_recovery = build_visual_delta_recovery_decision(
3656
3889
  s.get('verification_mode'),
@@ -3662,6 +3895,7 @@ if visual_delta_recovery:
3662
3895
 
3663
3896
  has_judgable_failed_interaction_evidence = (
3664
3897
  bool(structured_interaction_failure_summary)
3898
+ and not structured_interaction_capture_failure_summary
3665
3899
  and required_baseline_present
3666
3900
  and not proof_evidence_blocker
3667
3901
  and not visual_delta_recovery
@@ -3670,6 +3904,7 @@ has_good_evidence = (
3670
3904
  required_baseline_present
3671
3905
  and (after_observation.get('valid') or has_judgable_failed_interaction_evidence)
3672
3906
  and not proof_evidence_blocker
3907
+ and not structured_interaction_capture_failure_summary
3673
3908
  and not visual_delta_recovery
3674
3909
  )
3675
3910
 
@@ -3726,7 +3961,12 @@ if has_good_evidence:
3726
3961
  summary_lines.append('Proof assessment: awaiting supervising agent judgment')
3727
3962
  summary_lines.append('Proof next stage: supervising agent decides after reviewing the evidence packet')
3728
3963
  else:
3729
- capture_retry = build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker, s.get('route_expectation') or {})
3964
+ capture_retry = build_capture_retry_decision(
3965
+ after_observation,
3966
+ required_baseline_present,
3967
+ proof_evidence_blocker or structured_interaction_capture_failure_summary,
3968
+ s.get('route_expectation') or {},
3969
+ )
3730
3970
  if visual_delta_recovery:
3731
3971
  observation_reason = str(after_observation.get('reason') or '')
3732
3972
  observation_details = after_observation.get('details') if isinstance(after_observation.get('details'), dict) else {}
@@ -3735,6 +3975,7 @@ else:
3735
3975
  or 'console/runtime errors' in observation_reason
3736
3976
  or (observation_details.get('capture_error_messages') or [])
3737
3977
  or proof_evidence_blocker
3978
+ or structured_interaction_capture_failure_summary
3738
3979
  )
3739
3980
  if has_primary_capture_failure:
3740
3981
  capture_retry['visual_delta_recovery'] = visual_delta_recovery
@@ -3742,6 +3983,9 @@ else:
3742
3983
  else:
3743
3984
  capture_retry = visual_delta_recovery
3744
3985
  next_stage_options = ['author', 'verify', 'recon'] if no_implementation_mode else ['author', 'verify', 'implement', 'recon']
3986
+ capture_terminal_blocker = bool(capture_retry.get('terminal_blocker') or capture_retry.get('blocking'))
3987
+ recommended_stage = None if capture_terminal_blocker else (capture_retry.get('recommended_stage') or 'author')
3988
+ continue_with_stage = None if capture_terminal_blocker else (capture_retry.get('continue_with_stage') or 'author')
3745
3989
  s['verify_status'] = 'capture_incomplete'
3746
3990
  s['merge_recommendation'] = 'do-not-merge'
3747
3991
  s['proof_assessment'] = {}
@@ -3756,8 +4000,8 @@ else:
3756
4000
  'latest_observation': after_observation,
3757
4001
  'capture_quality': capture_retry,
3758
4002
  'next_stage_options': next_stage_options,
3759
- 'recommended_stage': capture_retry.get('recommended_stage') or 'author',
3760
- 'continue_with_stage': capture_retry.get('continue_with_stage') or 'author',
4003
+ 'recommended_stage': recommended_stage,
4004
+ 'continue_with_stage': continue_with_stage,
3761
4005
  'fields_agent_may_update': ['capture_script', 'server_path', 'wait_for_selector', 'proof_plan'],
3762
4006
  'instructions': [
3763
4007
  'The after-proof evidence packet is incomplete, so use the recommended stage before proof review.',
@@ -3767,7 +4011,7 @@ else:
3767
4011
  ],
3768
4012
  }
3769
4013
  summary_lines.append('Proof assessment: not yet possible because the after capture is still incomplete')
3770
- summary_lines.append('Proof next stage: ' + str(capture_retry.get('recommended_stage') or 'author'))
4014
+ summary_lines.append('Proof next stage: blocked' if capture_terminal_blocker else 'Proof next stage: ' + str(recommended_stage or 'author'))
3771
4015
 
3772
4016
  s['verify_summary'] = '\n'.join(summary_lines)
3773
4017
  s['proof_summary'] = s['verify_summary']