@riddledc/riddle-proof 0.8.5 → 0.8.7

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 (41) hide show
  1. package/dist/adapters/codex-exec-agent.cjs +31 -10
  2. package/dist/adapters/codex-exec-agent.js +1 -1
  3. package/dist/adapters/codex.cjs +31 -10
  4. package/dist/adapters/codex.js +1 -1
  5. package/dist/adapters/local-agent.cjs +31 -10
  6. package/dist/adapters/local-agent.js +1 -1
  7. package/dist/advanced/engine-harness.cjs +64 -7
  8. package/dist/advanced/engine-harness.js +2 -2
  9. package/dist/advanced/index.cjs +64 -7
  10. package/dist/advanced/index.js +4 -4
  11. package/dist/advanced/proof-run-core.cjs +63 -6
  12. package/dist/advanced/proof-run-core.js +1 -1
  13. package/dist/advanced/proof-run-engine.cjs +63 -6
  14. package/dist/advanced/proof-run-engine.js +2 -2
  15. package/dist/advanced/runner.js +2 -2
  16. package/dist/{chunk-GMZ57RRY.js → chunk-46DDSZJR.js} +1 -1
  17. package/dist/{chunk-RV6LK7HU.js → chunk-5N5QFI2S.js} +63 -6
  18. package/dist/{chunk-UIJ7X63P.js → chunk-5N6MQCLC.js} +1 -1
  19. package/dist/{chunk-BDFSMWTI.js → chunk-E7ATYSYS.js} +1 -1
  20. package/dist/{chunk-7F5LNUGR.js → chunk-PYCQNK66.js} +31 -10
  21. package/dist/{chunk-OD5UNE57.js → chunk-V6VZ3CAI.js} +2 -2
  22. package/dist/cli/index.js +4 -4
  23. package/dist/cli.cjs +100 -22
  24. package/dist/cli.js +4 -4
  25. package/dist/codex-exec-agent.cjs +31 -10
  26. package/dist/codex-exec-agent.js +1 -1
  27. package/dist/engine-harness.cjs +64 -7
  28. package/dist/engine-harness.js +2 -2
  29. package/dist/index.cjs +100 -22
  30. package/dist/index.js +4 -4
  31. package/dist/local-agent.cjs +31 -10
  32. package/dist/local-agent.js +1 -1
  33. package/dist/proof-run-core.cjs +63 -6
  34. package/dist/proof-run-core.js +1 -1
  35. package/dist/proof-run-engine.cjs +63 -6
  36. package/dist/proof-run-engine.js +2 -2
  37. package/dist/runner.js +2 -2
  38. package/package.json +1 -1
  39. package/runtime/lib/author.py +40 -1
  40. package/runtime/lib/verify.py +123 -1
  41. package/runtime/tests/recon_verify_smoke.py +82 -8
@@ -215,6 +215,56 @@ function writeState(statePath, state) {
215
215
  function normalizeOptionalString(value) {
216
216
  return typeof value === "string" ? value.trim() : void 0;
217
217
  }
218
+ var INTERACTION_VERIFICATION_MODES = /* @__PURE__ */ new Set(["interaction", "interactive", "user_flow", "user-flow", "workflow"]);
219
+ function normalizeRoutePath(value) {
220
+ const raw = typeof value === "string" ? value.trim() : "";
221
+ if (!raw) return "";
222
+ try {
223
+ const url = /^https?:\/\//i.test(raw) ? new URL(raw) : new URL(raw.startsWith("/") || raw.startsWith("?") || raw.startsWith("#") ? raw : `/${raw}`, "https://riddle-proof.local");
224
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
225
+ return `${pathname}${url.search}${url.hash}`;
226
+ } catch {
227
+ const hashSplit = raw.split("#");
228
+ const beforeHash = hashSplit.shift() || "";
229
+ const hash = hashSplit.length ? `#${hashSplit.join("#")}` : "";
230
+ const querySplit = beforeHash.split("?");
231
+ const rawPath = querySplit.shift() || "";
232
+ const query = querySplit.length ? `?${querySplit.join("?")}` : "";
233
+ const pathname = `/${rawPath}`.replace(/\/+/g, "/").replace(/\/+$/, "") || "/";
234
+ return `${pathname}${query}${hash}`;
235
+ }
236
+ }
237
+ function isInteractionVerificationMode(value) {
238
+ return INTERACTION_VERIFICATION_MODES.has(typeof value === "string" ? value.trim().toLowerCase() : "");
239
+ }
240
+ function stringRecordValue(record, key) {
241
+ if (!record || typeof record !== "object") return "";
242
+ const value = record[key];
243
+ return typeof value === "string" ? value.trim() : "";
244
+ }
245
+ function appendStateWarning(state, key, warning) {
246
+ const existing = Array.isArray(state[key]) ? state[key].filter((item) => typeof item === "string") : [];
247
+ if (!existing.includes(warning)) state[key] = [...existing, warning];
248
+ }
249
+ function interactionStartPathForAuthorPacket(state, parsed, refined) {
250
+ return normalizeRoutePath(
251
+ stringRecordValue(state, "expected_start_path") || stringRecordValue(refined, "expected_start_path") || stringRecordValue(parsed.interaction_contract, "start_path") || stringRecordValue(parsed.proof_contract, "start_path") || stringRecordValue(state, "server_path") || "/"
252
+ ) || "/";
253
+ }
254
+ function authorPacketServerPath(state, parsed, refined, serverPath, expectedTerminalPath) {
255
+ if (!isInteractionVerificationMode(state.verification_mode)) return serverPath;
256
+ const startPath = interactionStartPathForAuthorPacket(state, parsed, refined);
257
+ state.expected_start_path = startPath;
258
+ if (expectedTerminalPath && normalizeRoutePath(serverPath) === normalizeRoutePath(expectedTerminalPath) && normalizeRoutePath(serverPath) !== startPath) {
259
+ appendStateWarning(
260
+ state,
261
+ "author_warnings",
262
+ "Supervisor packet refined_inputs.server_path matched the terminal interaction route; kept the recon start route for capture."
263
+ );
264
+ return startPath;
265
+ }
266
+ return serverPath;
267
+ }
218
268
  function knownEnvironmentIssuesFromNotes(notes) {
219
269
  const text = notes.toLowerCase();
220
270
  const issues = [];
@@ -873,17 +923,24 @@ function mergeStateFromParams(statePath, params) {
873
923
  state.proof_contract = parsed.proof_contract;
874
924
  }
875
925
  const refined = parsed?.refined_inputs || {};
926
+ const expectedTerminalPath = normalizeOptionalString(
927
+ typeof refined?.expected_terminal_path === "string" ? refined.expected_terminal_path : typeof parsed?.expected_terminal_path === "string" ? parsed.expected_terminal_path : ""
928
+ ) || "";
876
929
  if (typeof refined?.server_path === "string") {
877
- state.server_path = normalizeOptionalString(refined.server_path) || "";
930
+ const refinedServerPath = normalizeOptionalString(refined.server_path) || "";
931
+ state.server_path = authorPacketServerPath(
932
+ state,
933
+ parsed,
934
+ refined,
935
+ refinedServerPath,
936
+ expectedTerminalPath
937
+ );
878
938
  state.server_path_source = "supervising_agent";
879
939
  }
880
940
  if (typeof refined?.wait_for_selector === "string") state.wait_for_selector = normalizeOptionalString(refined.wait_for_selector) || "";
881
941
  if (typeof refined?.reference === "string" && refined.reference.trim()) state.reference = refined.reference.trim();
882
- if (typeof refined?.expected_terminal_path === "string") {
883
- state.expected_terminal_path = normalizeOptionalString(refined.expected_terminal_path) || "";
884
- }
885
- if (typeof parsed?.expected_terminal_path === "string") {
886
- state.expected_terminal_path = normalizeOptionalString(parsed.expected_terminal_path) || "";
942
+ if (expectedTerminalPath) {
943
+ state.expected_terminal_path = expectedTerminalPath;
887
944
  }
888
945
  if (typeof parsed?.confidence === "string") state.supervisor_author_confidence = normalizeOptionalString(parsed.confidence) || null;
889
946
  if (parsed?.rationale !== void 0) state.supervisor_author_rationale = parsed.rationale;
@@ -26,7 +26,7 @@ import {
26
26
  visualDeltaShipGateReason,
27
27
  workflowFile,
28
28
  writeState
29
- } from "./chunk-RV6LK7HU.js";
29
+ } from "./chunk-5N5QFI2S.js";
30
30
  import "./chunk-MLKGABMK.js";
31
31
  export {
32
32
  BUNDLED_RIDDLE_PROOF_DIR,
@@ -195,6 +195,56 @@ function writeState(statePath, state) {
195
195
  function normalizeOptionalString(value) {
196
196
  return typeof value === "string" ? value.trim() : void 0;
197
197
  }
198
+ var INTERACTION_VERIFICATION_MODES = /* @__PURE__ */ new Set(["interaction", "interactive", "user_flow", "user-flow", "workflow"]);
199
+ function normalizeRoutePath(value) {
200
+ const raw = typeof value === "string" ? value.trim() : "";
201
+ if (!raw) return "";
202
+ try {
203
+ const url = /^https?:\/\//i.test(raw) ? new URL(raw) : new URL(raw.startsWith("/") || raw.startsWith("?") || raw.startsWith("#") ? raw : `/${raw}`, "https://riddle-proof.local");
204
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
205
+ return `${pathname}${url.search}${url.hash}`;
206
+ } catch {
207
+ const hashSplit = raw.split("#");
208
+ const beforeHash = hashSplit.shift() || "";
209
+ const hash = hashSplit.length ? `#${hashSplit.join("#")}` : "";
210
+ const querySplit = beforeHash.split("?");
211
+ const rawPath = querySplit.shift() || "";
212
+ const query = querySplit.length ? `?${querySplit.join("?")}` : "";
213
+ const pathname = `/${rawPath}`.replace(/\/+/g, "/").replace(/\/+$/, "") || "/";
214
+ return `${pathname}${query}${hash}`;
215
+ }
216
+ }
217
+ function isInteractionVerificationMode(value) {
218
+ return INTERACTION_VERIFICATION_MODES.has(typeof value === "string" ? value.trim().toLowerCase() : "");
219
+ }
220
+ function stringRecordValue(record, key) {
221
+ if (!record || typeof record !== "object") return "";
222
+ const value = record[key];
223
+ return typeof value === "string" ? value.trim() : "";
224
+ }
225
+ function appendStateWarning(state, key, warning) {
226
+ const existing = Array.isArray(state[key]) ? state[key].filter((item) => typeof item === "string") : [];
227
+ if (!existing.includes(warning)) state[key] = [...existing, warning];
228
+ }
229
+ function interactionStartPathForAuthorPacket(state, parsed, refined) {
230
+ return normalizeRoutePath(
231
+ stringRecordValue(state, "expected_start_path") || stringRecordValue(refined, "expected_start_path") || stringRecordValue(parsed.interaction_contract, "start_path") || stringRecordValue(parsed.proof_contract, "start_path") || stringRecordValue(state, "server_path") || "/"
232
+ ) || "/";
233
+ }
234
+ function authorPacketServerPath(state, parsed, refined, serverPath, expectedTerminalPath) {
235
+ if (!isInteractionVerificationMode(state.verification_mode)) return serverPath;
236
+ const startPath = interactionStartPathForAuthorPacket(state, parsed, refined);
237
+ state.expected_start_path = startPath;
238
+ if (expectedTerminalPath && normalizeRoutePath(serverPath) === normalizeRoutePath(expectedTerminalPath) && normalizeRoutePath(serverPath) !== startPath) {
239
+ appendStateWarning(
240
+ state,
241
+ "author_warnings",
242
+ "Supervisor packet refined_inputs.server_path matched the terminal interaction route; kept the recon start route for capture."
243
+ );
244
+ return startPath;
245
+ }
246
+ return serverPath;
247
+ }
198
248
  function knownEnvironmentIssuesFromNotes(notes) {
199
249
  const text = notes.toLowerCase();
200
250
  const issues = [];
@@ -853,17 +903,24 @@ function mergeStateFromParams(statePath, params) {
853
903
  state.proof_contract = parsed.proof_contract;
854
904
  }
855
905
  const refined = parsed?.refined_inputs || {};
906
+ const expectedTerminalPath = normalizeOptionalString(
907
+ typeof refined?.expected_terminal_path === "string" ? refined.expected_terminal_path : typeof parsed?.expected_terminal_path === "string" ? parsed.expected_terminal_path : ""
908
+ ) || "";
856
909
  if (typeof refined?.server_path === "string") {
857
- state.server_path = normalizeOptionalString(refined.server_path) || "";
910
+ const refinedServerPath = normalizeOptionalString(refined.server_path) || "";
911
+ state.server_path = authorPacketServerPath(
912
+ state,
913
+ parsed,
914
+ refined,
915
+ refinedServerPath,
916
+ expectedTerminalPath
917
+ );
858
918
  state.server_path_source = "supervising_agent";
859
919
  }
860
920
  if (typeof refined?.wait_for_selector === "string") state.wait_for_selector = normalizeOptionalString(refined.wait_for_selector) || "";
861
921
  if (typeof refined?.reference === "string" && refined.reference.trim()) state.reference = refined.reference.trim();
862
- if (typeof refined?.expected_terminal_path === "string") {
863
- state.expected_terminal_path = normalizeOptionalString(refined.expected_terminal_path) || "";
864
- }
865
- if (typeof parsed?.expected_terminal_path === "string") {
866
- state.expected_terminal_path = normalizeOptionalString(parsed.expected_terminal_path) || "";
922
+ if (expectedTerminalPath) {
923
+ state.expected_terminal_path = expectedTerminalPath;
867
924
  }
868
925
  if (typeof parsed?.confidence === "string") state.supervisor_author_confidence = normalizeOptionalString(parsed.confidence) || null;
869
926
  if (parsed?.rationale !== void 0) state.supervisor_author_rationale = parsed.rationale;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createRiddleProofEngine,
3
3
  executeWorkflow
4
- } from "./chunk-GMZ57RRY.js";
5
- import "./chunk-RV6LK7HU.js";
4
+ } from "./chunk-46DDSZJR.js";
5
+ import "./chunk-5N5QFI2S.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-UIJ7X63P.js";
3
+ } from "./chunk-5N6MQCLC.js";
4
4
  import "./chunk-YZUVEJ5B.js";
5
5
  import "./chunk-FMOYUYH2.js";
6
- import "./chunk-RV6LK7HU.js";
6
+ import "./chunk-5N5QFI2S.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.5",
3
+ "version": "0.8.7",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -9,6 +9,7 @@ Instead it does two things:
9
9
  import json
10
10
  import os
11
11
  import sys
12
+ from urllib.parse import urlparse
12
13
 
13
14
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
15
  from util import load_state, save_state
@@ -34,6 +35,31 @@ def normalize_path(value):
34
35
  return path
35
36
 
36
37
 
38
+ def normalize_route_path(value):
39
+ raw = (value or '').strip()
40
+ if not raw:
41
+ return ''
42
+ parsed = urlparse(raw)
43
+ path = parsed.path or raw
44
+ query = parsed.query or ''
45
+ fragment = parsed.fragment or ''
46
+ if '?' in path:
47
+ path, query_tail = path.split('?', 1)
48
+ query = query or query_tail.split('#', 1)[0]
49
+ if '#' in path:
50
+ path, fragment_tail = path.split('#', 1)
51
+ fragment = fragment or fragment_tail
52
+ if not path.startswith('/'):
53
+ path = '/' + path.lstrip('/')
54
+ path = path.rstrip('/') or '/'
55
+ return path + (('?' + query) if query else '') + (('#' + fragment) if fragment else '')
56
+
57
+
58
+ def is_interaction_mode(state):
59
+ mode = (state.get('verification_mode') or '').strip().lower()
60
+ return mode in ('interaction', 'interactive', 'user_flow', 'user-flow', 'workflow')
61
+
62
+
37
63
  def first_non_empty(*values):
38
64
  for value in values:
39
65
  if isinstance(value, str) and value.strip():
@@ -158,6 +184,7 @@ def author_request_payload(state, reference, baselines, current_plan, hypothesis
158
184
  'Keep capture_script concise Playwright statements.',
159
185
  'For visual/UI proof, include saveScreenshot(\'after-proof\') exactly once.',
160
186
  'For interaction proof, preserve the interaction contract and name the expected terminal route/state separately from the initial route.',
187
+ 'For interaction proof, return a JSON-serializable evidence object with start route/state, terminal route/state, action, assertions, and matched UI text; catch waitForURL or selector timeouts and record them as failed assertions instead of throwing before evidence is emitted.',
161
188
  '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 return a JSON-serializable evidence object with playability or playability_evidence version riddle-proof.playability.v1.',
162
189
  'For data/audio/log/metric/custom proof, screenshots are optional; collect measurements inside page.evaluate, assign the result to an evidence variable, and return that evidence object from capture_script.',
163
190
  'Do not assign globalThis.__riddleProofEvidence, window.__riddleProofEvidence, or self.__riddleProofEvidence in the worker context. Avoid global evidence assignment unless it is inside page.evaluate for compatibility with older packets.',
@@ -283,6 +310,17 @@ expected_terminal_path = normalize_path(first_non_empty(
283
310
  supervisor_packet.get('expected_after_path'),
284
311
  s.get('expected_terminal_path'),
285
312
  ))
313
+ author_warnings = []
314
+ if is_interaction_mode(s):
315
+ interaction_start_path = normalize_route_path(first_non_empty(s.get('expected_start_path'), default_path, s.get('server_path'), '/')) or '/'
316
+ refined_route = normalize_route_path(refined_path)
317
+ terminal_route = normalize_route_path(expected_terminal_path)
318
+ if terminal_route and refined_route == terminal_route and refined_route != interaction_start_path:
319
+ refined_path = interaction_start_path
320
+ author_warnings.append(
321
+ 'Supervisor packet refined_inputs.server_path matched the terminal interaction route; kept the recon start route for capture.'
322
+ )
323
+ s['expected_start_path'] = interaction_start_path
286
324
  confidence = provided_payload['confidence'] if provided_payload['confidence'] in ('high', 'medium', 'low') else 'medium'
287
325
  rationale = sanitize_rationale(provided_payload['rationale'])
288
326
  summary = provided_payload['summary'] or 'Supervising agent supplied the proof packet from recon observations.'
@@ -300,6 +338,7 @@ authored_packet = {
300
338
  'interaction_contract': provided_payload['interaction_contract'],
301
339
  'proof_contract': provided_payload['proof_contract'],
302
340
  'rationale': rationale,
341
+ 'warnings': author_warnings,
303
342
  'confidence': confidence,
304
343
  'mode': 'supervising_agent',
305
344
  'model': ('supervising-agent:' + RUNTIME_MODEL_HINT) if RUNTIME_MODEL_HINT else 'supervising-agent',
@@ -327,7 +366,7 @@ s['author_mode'] = 'supervising_agent'
327
366
  s['author_model'] = authored_packet['model']
328
367
  s['author_confidence'] = confidence
329
368
  s['author_rationale'] = rationale
330
- s['author_warnings'] = []
369
+ s['author_warnings'] = author_warnings
331
370
  s['author_runtime_model_hint'] = RUNTIME_MODEL_HINT
332
371
  s['author_packet'] = authored_packet
333
372
  s['author_summary'] = summary
@@ -558,6 +558,83 @@ def extract_proof_evidence(payload):
558
558
  return evidence
559
559
 
560
560
 
561
+ def attach_interaction_capture_failure_evidence(state, payload, expected_path, after_observation):
562
+ if not isinstance(payload, dict):
563
+ return payload, None
564
+ mode = normalized_verification_mode(state.get('verification_mode'))
565
+ if mode not in INTERACTION_MODES:
566
+ return payload, None
567
+ if extract_proof_evidence(payload) is not None:
568
+ return payload, None
569
+
570
+ details = after_observation.get('details') if isinstance(after_observation, dict) else {}
571
+ if not isinstance(details, dict):
572
+ details = {}
573
+ expected = normalize_observed_path(expected_path)
574
+ observed_raw = str(details.get('observed_path_raw') or details.get('observed_path') or '').strip()
575
+ observed = normalize_observed_path(observed_raw)
576
+ route_matches = route_matches_expected(expected, observed_raw or observed) if expected and (observed_raw or observed) else None
577
+ error_messages = [
578
+ str(item).strip()
579
+ for item in (details.get('capture_error_messages') or [])
580
+ if str(item).strip()
581
+ ]
582
+ if route_matches is not False and not error_messages:
583
+ return payload, None
584
+
585
+ route_expectation = state.get('route_expectation') if isinstance(state.get('route_expectation'), dict) else {}
586
+ expected_parts = route_parts(expected)
587
+ observed_parts = route_parts(observed_raw or observed)
588
+ evidence = {
589
+ 'version': 'riddle-proof.interaction.capture-failure.v1',
590
+ 'synthetic': True,
591
+ 'source': 'verify_capture_failure',
592
+ 'mode': mode,
593
+ 'passed': False,
594
+ 'proofReady': False,
595
+ 'authored_proof_evidence_present': False,
596
+ 'evidence_summary': 'Interaction capture failed before the authored script emitted structured proof evidence.',
597
+ 'expected': {
598
+ 'path': expected,
599
+ 'pathname': expected_parts.get('pathname') or '',
600
+ 'query': expected_parts.get('query') or '',
601
+ 'hash': expected_parts.get('hash') or '',
602
+ },
603
+ 'observed': {
604
+ 'path': observed,
605
+ 'raw_path': observed_raw,
606
+ 'pathname': observed_parts.get('pathname') or '',
607
+ 'query': observed_parts.get('query') or '',
608
+ 'hash': observed_parts.get('hash') or '',
609
+ },
610
+ 'checks': {
611
+ 'scriptCompleted': len(error_messages) == 0,
612
+ 'routeMatches': bool(route_matches),
613
+ 'authoredEvidenceReturned': False,
614
+ },
615
+ 'route_expectation_source': route_expectation.get('source') or '',
616
+ }
617
+ if error_messages:
618
+ evidence['capture_error'] = error_messages[0][:1000]
619
+ if route_matches is False:
620
+ evidence['evidence_summary'] = (
621
+ 'Interaction capture reached a different terminal route than expected before authored proof evidence was emitted.'
622
+ )
623
+
624
+ enriched = enrich_capture_payload(payload)
625
+ patched = dict(enriched)
626
+ result = dict(patched.get('result') or {})
627
+ result['proofEvidence'] = evidence
628
+ patched['result'] = result
629
+ console = list(patched.get('console') or [])
630
+ try:
631
+ console.append(PROOF_EVIDENCE_PREFIX + json.dumps(evidence, sort_keys=True))
632
+ except Exception:
633
+ pass
634
+ patched['console'] = console
635
+ return patched, evidence
636
+
637
+
561
638
  def proof_evidence_records(value):
562
639
  if isinstance(value, dict):
563
640
  return [value]
@@ -1918,16 +1995,22 @@ def route_parts(value):
1918
1995
 
1919
1996
  EXPLICIT_TERMINAL_PATH_KEYS = (
1920
1997
  'expected_terminal_path', 'expectedTerminalPath',
1998
+ 'expected_terminal_url', 'expectedTerminalUrl',
1921
1999
  'expected_terminal_route', 'expectedTerminalRoute',
1922
2000
  'terminal_path', 'terminalPath',
2001
+ 'terminal_url', 'terminalUrl',
1923
2002
  'terminal_route', 'terminalRoute',
1924
2003
  'expected_after_path', 'expectedAfterPath',
2004
+ 'expected_after_url', 'expectedAfterUrl',
1925
2005
  'expected_after_route', 'expectedAfterRoute',
1926
2006
  'after_path', 'afterPath',
2007
+ 'after_url', 'afterUrl',
1927
2008
  'after_route', 'afterRoute',
1928
2009
  'expected_final_path', 'expectedFinalPath',
2010
+ 'expected_final_url', 'expectedFinalUrl',
1929
2011
  'expected_final_route', 'expectedFinalRoute',
1930
2012
  'final_path', 'finalPath',
2013
+ 'final_url', 'finalUrl',
1931
2014
  'final_route', 'finalRoute',
1932
2015
  )
1933
2016
  LOCATION_PATH_KEYS = ('path', 'pathname', 'route', 'url', 'href')
@@ -1939,6 +2022,11 @@ AFTER_STATE_KEYS = (
1939
2022
  'final', 'final_state', 'finalState',
1940
2023
  'expected_final', 'expectedFinal',
1941
2024
  )
2025
+ EVIDENCE_CONTAINER_KEYS = (
2026
+ 'proofEvidence', 'proof_evidence',
2027
+ 'interactionEvidence', 'interaction_evidence',
2028
+ 'evidence',
2029
+ )
1942
2030
  CONTRACT_STATE_KEYS = (
1943
2031
  'interaction_contract', 'interactionContract',
1944
2032
  'proof_contract', 'proofContract',
@@ -1990,6 +2078,17 @@ def terminal_path_from_record(record, depth=0):
1990
2078
  candidate = terminal_path_from_record(item, depth + 1)
1991
2079
  if candidate:
1992
2080
  return candidate
2081
+ for key in EVIDENCE_CONTAINER_KEYS:
2082
+ value = record.get(key)
2083
+ if isinstance(value, dict):
2084
+ candidate = terminal_path_from_record(value, depth + 1)
2085
+ if candidate:
2086
+ return candidate
2087
+ elif isinstance(value, list):
2088
+ for item in value:
2089
+ candidate = terminal_path_from_record(item, depth + 1)
2090
+ if candidate:
2091
+ return candidate
1993
2092
  for key in CONTRACT_STATE_KEYS:
1994
2093
  value = record.get(key)
1995
2094
  if isinstance(value, dict):
@@ -2004,11 +2103,25 @@ def terminal_path_from_record(record, depth=0):
2004
2103
  return ''
2005
2104
 
2006
2105
 
2106
+ def text_path_candidate(value):
2107
+ if not isinstance(value, str):
2108
+ return ''
2109
+ raw = value.strip().rstrip('.,;:)]}')
2110
+ return path_candidate(raw)
2111
+
2112
+
2007
2113
  def terminal_path_from_text(value):
2008
2114
  if not isinstance(value, str):
2009
2115
  return ''
2010
2116
  for match in re.findall(r"""['"`](/[^'"`\s]+[?#][^'"`\s]*)['"`]""", value):
2011
- candidate = path_candidate(match)
2117
+ candidate = text_path_candidate(match)
2118
+ if candidate:
2119
+ return candidate
2120
+ context_pattern = re.compile(
2121
+ r"""(?is)\b(?:expected\s+(?:terminal|after|final)|terminal|after|final)\b[^/\r\n]{0,120}['"`]?(/[^'"`\s,;)]*)"""
2122
+ )
2123
+ for match in context_pattern.findall(value):
2124
+ candidate = text_path_candidate(match)
2012
2125
  if candidate:
2013
2126
  return candidate
2014
2127
  return ''
@@ -3113,6 +3226,15 @@ after_observation = evaluate_capture_quality(after_payload, expected_path, verif
3113
3226
  details = after_observation.get('details') if isinstance(after_observation.get('details'), dict) else {}
3114
3227
  details['viewport_matrix'] = after_viewport_matrix
3115
3228
  after_observation['details'] = details
3229
+ after_payload, synthetic_interaction_failure_evidence = attach_interaction_capture_failure_evidence(s, after_payload, expected_path, after_observation)
3230
+ if synthetic_interaction_failure_evidence is not None:
3231
+ s['synthetic_interaction_failure_evidence'] = synthetic_interaction_failure_evidence
3232
+ if isinstance(results.get('after'), dict):
3233
+ results['after']['raw'] = after_payload
3234
+ after_observation = evaluate_capture_quality(after_payload, expected_path, verification_mode)
3235
+ details = after_observation.get('details') if isinstance(after_observation.get('details'), dict) else {}
3236
+ details['viewport_matrix'] = after_viewport_matrix
3237
+ after_observation['details'] = details
3116
3238
  if after_viewport_matrix.get('status') == 'incomplete':
3117
3239
  missing_names = [
3118
3240
  str(item.get('name') or item.get('slug') or '').strip()
@@ -340,14 +340,18 @@ class FakeRiddle:
340
340
  'largeVisibleElements': [{'tag': 'h1', 'text': 'Proof'}],
341
341
  }
342
342
  proof_evidence = {
343
- 'before': {'path': '/'},
344
- 'action': 'clicked Proof',
345
- 'after': {'path': '/proof/'},
346
- 'assertions': {
347
- 'startedOnHome': True,
348
- 'clickedProofNavigation': True,
349
- 'terminalPathIsProof': True,
350
- 'proofContentVisible': True,
343
+ 'proofEvidence': {
344
+ 'version': 'riddle-proof.interaction.v1',
345
+ 'start': {'href': 'https://riddledc.com/'},
346
+ 'action': {'type': 'click', 'target': 'Proof'},
347
+ 'terminal': {'href': 'https://riddledc.com/proof/'},
348
+ 'afterUrl': 'https://riddledc.com/proof/',
349
+ 'assertions': {
350
+ 'startedOnHome': True,
351
+ 'clickedProofNavigation': True,
352
+ 'terminalPathIsProof': True,
353
+ 'proofContentVisible': True,
354
+ },
351
355
  },
352
356
  }
353
357
  return {
@@ -1921,6 +1925,63 @@ def run_author_applies_supervisor_packet():
1921
1925
  shutil.rmtree(tempdir, ignore_errors=True)
1922
1926
 
1923
1927
 
1928
+ def run_author_keeps_interaction_start_route():
1929
+ tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-supervisor-interaction-start-'))
1930
+ state_path = tempdir / 'state.json'
1931
+ try:
1932
+ state = base_state(tempdir, reference='before')
1933
+ state.update({
1934
+ 'recon_status': 'ready_for_proof_plan',
1935
+ 'verification_mode': 'interaction',
1936
+ 'server_path': '/',
1937
+ 'expected_start_path': '/',
1938
+ 'before_cdn': 'https://cdn.example.com/before-home.png',
1939
+ 'recon_results': {
1940
+ 'baselines': {'before': {'path': '/', 'url': 'https://cdn.example.com/before-home.png'}},
1941
+ 'current_plan': {'target_path': '/'},
1942
+ },
1943
+ 'author_request': {
1944
+ 'current_plan': {'target_path': '/'},
1945
+ 'observed_baselines': {'before': {'path': '/', 'url': 'https://cdn.example.com/before-home.png'}},
1946
+ },
1947
+ 'supervisor_author_packet': {
1948
+ 'proof_plan': 'Start at /, click Proof, and verify the terminal /proof/ route.',
1949
+ 'capture_script': "clickedProofNavigation(); await saveScreenshot('after-proof');",
1950
+ 'refined_inputs': {
1951
+ 'server_path': '/proof/',
1952
+ 'expected_terminal_path': '/proof/',
1953
+ 'wait_for_selector': '',
1954
+ 'reference': 'before',
1955
+ },
1956
+ 'rationale': ['The interaction starts on home and terminates on Proof.'],
1957
+ 'confidence': 'high',
1958
+ 'summary': 'Supervisor supplied the interaction proof packet.',
1959
+ },
1960
+ })
1961
+ write_state(state_path, state)
1962
+ os.environ['RIDDLE_PROOF_STATE_FILE'] = str(state_path)
1963
+
1964
+ fake = FakeRiddle()
1965
+ load_util_with_fake(fake)
1966
+ load_module('author_supervisor_interaction_start', AUTHOR_PATH)
1967
+ after_author = json.loads(state_path.read_text())
1968
+
1969
+ assert after_author['author_status'] == 'ready'
1970
+ assert after_author['server_path'] == '/'
1971
+ assert after_author['expected_start_path'] == '/'
1972
+ assert after_author['expected_terminal_path'] == '/proof/'
1973
+ assert after_author['author_packet']['refined_inputs']['server_path'] == '/'
1974
+ assert after_author['author_warnings']
1975
+ assert 'terminal interaction route' in after_author['author_warnings'][0]
1976
+ return {
1977
+ 'ok': True,
1978
+ 'server_path': after_author['server_path'],
1979
+ 'expected_terminal_path': after_author['expected_terminal_path'],
1980
+ }
1981
+ finally:
1982
+ shutil.rmtree(tempdir, ignore_errors=True)
1983
+
1984
+
1924
1985
  def run_verify_requests_supervisor_assessment():
1925
1986
  tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-verify-supervisor-'))
1926
1987
  state_path = tempdir / 'state.json'
@@ -2580,6 +2641,18 @@ def run_verify_interaction_authored_query_hash_mismatch_returns_author():
2580
2641
  assert capture_quality['mismatch']['observed_after_path'] == '/pricing/'
2581
2642
  assert 'page.waitForURL: Timeout 15000ms exceeded' in capture_quality['summary']
2582
2643
  assert any('capture plan should be revised' in reason for reason in capture_quality['reasons'])
2644
+ supporting = after_verify['verify_results']['after']['supporting_artifacts']
2645
+ assert supporting['proof_evidence_present'] is True
2646
+ assert supporting['has_structured_payload'] is True
2647
+ synthetic_evidence = after_verify['evidence_bundle']['proof_evidence']
2648
+ assert synthetic_evidence['version'] == 'riddle-proof.interaction.capture-failure.v1'
2649
+ assert synthetic_evidence['passed'] is False
2650
+ assert synthetic_evidence['authored_proof_evidence_present'] is False
2651
+ assert synthetic_evidence['checks']['routeMatches'] is False
2652
+ assert synthetic_evidence['expected']['query'] == 'rp_probe=1'
2653
+ assert synthetic_evidence['expected']['hash'] == '#pricing-probe'
2654
+ assert synthetic_evidence['observed']['path'] == '/pricing'
2655
+ assert 'page.waitForURL: Timeout 15000ms exceeded' in synthetic_evidence['capture_error']
2583
2656
  return {
2584
2657
  'ok': True,
2585
2658
  'summary': capture_quality['summary'],
@@ -2997,6 +3070,7 @@ if __name__ == '__main__':
2997
3070
  'recon_hint_root_preference': run_recon_prefers_hint_root_over_single_route_literal(),
2998
3071
  'capture_hint_rejects_route_specific_mode_only_match': run_capture_hint_rejects_route_specific_mode_only_match(),
2999
3072
  'author_applies_supervisor_packet': run_author_applies_supervisor_packet(),
3073
+ 'author_keeps_interaction_start_route': run_author_keeps_interaction_start_route(),
3000
3074
  'verify_requests_supervisor_assessment': run_verify_requests_supervisor_assessment(),
3001
3075
  'verify_routes_unmeasured_visual_delta_to_recovery': run_verify_routes_unmeasured_visual_delta_to_recovery(),
3002
3076
  'verify_structured_evidence_without_screenshot': run_verify_structured_evidence_without_screenshot(),