@riddledc/riddle-proof 0.8.38 → 0.8.40

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.
@@ -1,5 +1,5 @@
1
1
  export { b as runner } from '../runner-4LJ5z0D-.cjs';
2
2
  export { l as engineHarness } from '../engine-harness-LBfqbFSe.cjs';
3
3
  export { p as proofRunCore } from '../proof-run-core-B1GeqkR8.cjs';
4
- export { p as proofRunEngine } from '../proof-run-engine-4dM37pEx.cjs';
4
+ export { p as proofRunEngine } from '../proof-run-engine-DeHxtGnW.cjs';
5
5
  import '../types.cjs';
@@ -1,5 +1,5 @@
1
1
  export { b as runner } from '../runner-BdQpOkZD.js';
2
2
  export { l as engineHarness } from '../engine-harness-CMACHP6A.js';
3
3
  export { p as proofRunCore } from '../proof-run-core-B1GeqkR8.js';
4
- export { p as proofRunEngine } from '../proof-run-engine-BqaeqAze.js';
4
+ export { p as proofRunEngine } from '../proof-run-engine-DYfmd8d7.js';
5
5
  import '../types.js';
@@ -1,2 +1,2 @@
1
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from '../proof-run-engine-4dM37pEx.cjs';
1
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from '../proof-run-engine-DeHxtGnW.cjs';
2
2
  import '../proof-run-core-B1GeqkR8.cjs';
@@ -1,2 +1,2 @@
1
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from '../proof-run-engine-BqaeqAze.js';
1
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from '../proof-run-engine-DYfmd8d7.js';
2
2
  import '../proof-run-core-B1GeqkR8.js';
@@ -125,8 +125,10 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
125
125
  "submitRetries",
126
126
  "submitTimeoutMs",
127
127
  "summary",
128
+ "successCriteria",
128
129
  "sync",
129
130
  "timeout",
131
+ "title",
130
132
  "url",
131
133
  "unsubmittedJobRetries",
132
134
  "unsubmittedJobTimeoutMs",
package/dist/cli/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import "../chunk-U44KBAPH.js";
1
+ import "../chunk-MVJBPCZ4.js";
2
2
  import "../chunk-DI2XNGEZ.js";
3
3
  import "../chunk-6KYXX4OE.js";
4
4
  import "../chunk-Z2LCVROU.js";
package/dist/cli.cjs CHANGED
@@ -17667,8 +17667,10 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
17667
17667
  "submitRetries",
17668
17668
  "submitTimeoutMs",
17669
17669
  "summary",
17670
+ "successCriteria",
17670
17671
  "sync",
17671
17672
  "timeout",
17673
+ "title",
17672
17674
  "url",
17673
17675
  "unsubmittedJobRetries",
17674
17676
  "unsubmittedJobTimeoutMs",
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-U44KBAPH.js";
2
+ import "./chunk-MVJBPCZ4.js";
3
3
  import "./chunk-DI2XNGEZ.js";
4
4
  import "./chunk-6KYXX4OE.js";
5
5
  import "./chunk-Z2LCVROU.js";
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
295
+ action: "recon" | "author" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
385
+ action: "recon" | "author" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup";
662
+ action: "recon" | "author" | "ship" | "implement" | "verify" | "setup";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
295
+ action: "recon" | "author" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
385
+ action: "recon" | "author" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup";
662
+ action: "recon" | "author" | "ship" | "implement" | "verify" | "setup";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
@@ -1,2 +1,2 @@
1
1
  import './proof-run-core-B1GeqkR8.cjs';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-4dM37pEx.cjs';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-DeHxtGnW.cjs';
@@ -1,2 +1,2 @@
1
1
  import './proof-run-core-B1GeqkR8.js';
2
- export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-BqaeqAze.js';
2
+ export { R as RiddleProofEngine, c as createRiddleProofEngine, e as executeWorkflow } from './proof-run-engine-DYfmd8d7.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.8.38",
3
+ "version": "0.8.40",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -2610,17 +2610,93 @@ def assertion_item_label(item, fallback):
2610
2610
  return fallback
2611
2611
 
2612
2612
 
2613
- def collect_interaction_failed_assertions(value, prefix='', depth=0):
2613
+ def assertion_route_value_equivalent(expected, actual):
2614
+ expected_candidate = path_candidate(expected)
2615
+ actual_candidate = path_candidate(actual)
2616
+ if expected_candidate and actual_candidate:
2617
+ return normalize_observed_path(expected_candidate) == normalize_observed_path(actual_candidate)
2618
+ return expected == actual
2619
+
2620
+
2621
+ EXPECTED_ACTUAL_ROUTE_KEY_ALIASES = {
2622
+ 'expected_path': ('path', 'pathname', 'terminal_path', 'after_path', 'final_path', 'observed_path', 'actual_path'),
2623
+ 'expected_url': ('url', 'href', 'terminal_url', 'terminal_href', 'after_url', 'after_href', 'final_url', 'final_href', 'observed_url', 'actual_url'),
2624
+ 'expected_href': ('href', 'url', 'terminal_href', 'terminal_url', 'after_href', 'after_url', 'final_href', 'final_url', 'observed_href', 'actual_href'),
2625
+ 'expected_route': ('route', 'path', 'pathname', 'terminal_route', 'terminal_path', 'after_route', 'after_path', 'final_route', 'final_path'),
2626
+ 'expected_terminal_path': ('terminal_path', 'path', 'pathname', 'after_path', 'final_path', 'observed_path', 'actual_path'),
2627
+ 'expected_terminal_url': ('terminal_url', 'url', 'href', 'after_url', 'final_url', 'observed_url', 'actual_url'),
2628
+ 'expected_terminal_href': ('terminal_href', 'href', 'url', 'after_href', 'final_href', 'observed_href', 'actual_href'),
2629
+ 'expected_terminal_route': ('terminal_route', 'terminal_path', 'route', 'path', 'pathname', 'after_route', 'after_path', 'final_route', 'final_path'),
2630
+ 'expected_after_path': ('after_path', 'path', 'pathname', 'terminal_path', 'final_path', 'observed_path', 'actual_path'),
2631
+ 'expected_after_url': ('after_url', 'url', 'href', 'terminal_url', 'final_url', 'observed_url', 'actual_url'),
2632
+ 'expected_after_href': ('after_href', 'href', 'url', 'terminal_href', 'final_href', 'observed_href', 'actual_href'),
2633
+ 'expected_after_route': ('after_route', 'after_path', 'route', 'path', 'pathname', 'terminal_route', 'terminal_path', 'final_route', 'final_path'),
2634
+ 'expected_final_path': ('final_path', 'path', 'pathname', 'terminal_path', 'after_path', 'observed_path', 'actual_path'),
2635
+ 'expected_final_url': ('final_url', 'url', 'href', 'terminal_url', 'after_url', 'observed_url', 'actual_url'),
2636
+ 'expected_final_href': ('final_href', 'href', 'url', 'terminal_href', 'after_href', 'observed_href', 'actual_href'),
2637
+ 'expected_final_route': ('final_route', 'final_path', 'route', 'path', 'pathname', 'terminal_route', 'terminal_path', 'after_route', 'after_path'),
2638
+ }
2639
+
2640
+
2641
+ def assertion_expected_actual_equivalent(expected, actual):
2642
+ if isinstance(expected, str) and isinstance(actual, str):
2643
+ return assertion_route_value_equivalent(expected, actual)
2644
+ if isinstance(expected, dict) and isinstance(actual, dict):
2645
+ for expected_key, expected_value in expected.items():
2646
+ actual_keys = (expected_key,) + EXPECTED_ACTUAL_ROUTE_KEY_ALIASES.get(expected_key, ())
2647
+ matched = False
2648
+ for actual_key in actual_keys:
2649
+ if actual_key not in actual:
2650
+ continue
2651
+ if assertion_expected_actual_equivalent(expected_value, actual.get(actual_key)):
2652
+ matched = True
2653
+ break
2654
+ if not matched:
2655
+ return False
2656
+ return True
2657
+ return expected == actual
2658
+
2659
+
2660
+ def assertion_failure_is_route_equivalent(item):
2661
+ if not isinstance(item, dict):
2662
+ return False
2663
+ has_failure_flag = any(item.get(key) is False for key in INTERACTION_FAILURE_FLAG_KEYS)
2664
+ status = str(item.get('status') or item.get('result') or '').strip().lower()
2665
+ if status in INTERACTION_FAILURE_STATUS_VALUES:
2666
+ has_failure_flag = True
2667
+ if not has_failure_flag or 'expected' not in item or 'actual' not in item:
2668
+ return False
2669
+ return assertion_expected_actual_equivalent(item.get('expected'), item.get('actual'))
2670
+
2671
+
2672
+ def collect_interaction_failed_assertions_internal(value, prefix='', depth=0):
2614
2673
  if depth > 6:
2615
- return []
2674
+ return [], False
2616
2675
  failures = []
2676
+ ignored_route_equivalent_failure = False
2617
2677
  if isinstance(value, dict):
2678
+ pending_aggregate_failures = []
2679
+ has_assertion_container = any(
2680
+ isinstance(value.get(key), (dict, list))
2681
+ for key in INTERACTION_ASSERTION_CONTAINER_KEYS
2682
+ )
2683
+ route_equivalent_failure = assertion_failure_is_route_equivalent(value)
2618
2684
  for key in INTERACTION_FAILURE_FLAG_KEYS:
2619
2685
  if value.get(key) is False:
2620
- failures.append(failure_label(prefix, key))
2686
+ if route_equivalent_failure or has_assertion_container:
2687
+ ignored_route_equivalent_failure = ignored_route_equivalent_failure or route_equivalent_failure
2688
+ if has_assertion_container and not route_equivalent_failure:
2689
+ pending_aggregate_failures.append(failure_label(prefix, key))
2690
+ else:
2691
+ failures.append(failure_label(prefix, key))
2621
2692
  status = str(value.get('status') or value.get('result') or '').strip().lower()
2622
2693
  if status in INTERACTION_FAILURE_STATUS_VALUES:
2623
- failures.append(failure_label(prefix, assertion_item_label(value, 'status')))
2694
+ if route_equivalent_failure or has_assertion_container:
2695
+ ignored_route_equivalent_failure = ignored_route_equivalent_failure or route_equivalent_failure
2696
+ if has_assertion_container and not route_equivalent_failure:
2697
+ pending_aggregate_failures.append(failure_label(prefix, assertion_item_label(value, 'status')))
2698
+ else:
2699
+ failures.append(failure_label(prefix, assertion_item_label(value, 'status')))
2624
2700
  for key in INTERACTION_ASSERTION_CONTAINER_KEYS:
2625
2701
  checks = value.get(key)
2626
2702
  container_prefix = failure_label(prefix, key)
@@ -2629,39 +2705,55 @@ def collect_interaction_failed_assertions(value, prefix='', depth=0):
2629
2705
  if check_value is False:
2630
2706
  failures.append(failure_label(container_prefix, check_key))
2631
2707
  elif isinstance(check_value, dict):
2632
- nested = collect_interaction_failed_assertions(
2708
+ nested, nested_ignored = collect_interaction_failed_assertions_internal(
2633
2709
  check_value,
2634
2710
  failure_label(container_prefix, check_key),
2635
2711
  depth + 1,
2636
2712
  )
2637
2713
  failures.extend(nested)
2714
+ ignored_route_equivalent_failure = ignored_route_equivalent_failure or nested_ignored
2638
2715
  elif isinstance(check_value, list):
2639
- failures.extend(collect_interaction_failed_assertions(
2716
+ nested, nested_ignored = collect_interaction_failed_assertions_internal(
2640
2717
  check_value,
2641
2718
  failure_label(container_prefix, check_key),
2642
2719
  depth + 1,
2643
- ))
2720
+ )
2721
+ failures.extend(nested)
2722
+ ignored_route_equivalent_failure = ignored_route_equivalent_failure or nested_ignored
2644
2723
  elif isinstance(checks, list):
2645
2724
  for index, item in enumerate(checks):
2646
2725
  if item is False:
2647
2726
  failures.append(failure_label(container_prefix, str(index)))
2648
2727
  elif isinstance(item, dict):
2649
2728
  item_label = assertion_item_label(item, str(index))
2650
- failures.extend(collect_interaction_failed_assertions(
2729
+ nested, nested_ignored = collect_interaction_failed_assertions_internal(
2651
2730
  item,
2652
2731
  failure_label(container_prefix, item_label),
2653
2732
  depth + 1,
2654
- ))
2733
+ )
2734
+ failures.extend(nested)
2735
+ ignored_route_equivalent_failure = ignored_route_equivalent_failure or nested_ignored
2655
2736
  for key in EVIDENCE_CONTAINER_KEYS:
2656
2737
  nested = value.get(key)
2657
2738
  if isinstance(nested, (dict, list)):
2658
- failures.extend(collect_interaction_failed_assertions(nested, failure_label(prefix, key), depth + 1))
2739
+ nested_failures, nested_ignored = collect_interaction_failed_assertions_internal(nested, failure_label(prefix, key), depth + 1)
2740
+ failures.extend(nested_failures)
2741
+ ignored_route_equivalent_failure = ignored_route_equivalent_failure or nested_ignored
2742
+ if pending_aggregate_failures and not failures and not ignored_route_equivalent_failure:
2743
+ failures.extend(pending_aggregate_failures)
2659
2744
  elif isinstance(value, list):
2660
2745
  for index, item in enumerate(value):
2661
2746
  if item is False:
2662
2747
  failures.append(failure_label(prefix, str(index)))
2663
2748
  elif isinstance(item, (dict, list)):
2664
- failures.extend(collect_interaction_failed_assertions(item, prefix, depth + 1))
2749
+ nested_failures, nested_ignored = collect_interaction_failed_assertions_internal(item, prefix, depth + 1)
2750
+ failures.extend(nested_failures)
2751
+ ignored_route_equivalent_failure = ignored_route_equivalent_failure or nested_ignored
2752
+ return failures, ignored_route_equivalent_failure
2753
+
2754
+
2755
+ def collect_interaction_failed_assertions(value, prefix='', depth=0):
2756
+ failures, _ignored_route_equivalent_failure = collect_interaction_failed_assertions_internal(value, prefix, depth)
2665
2757
  deduped = []
2666
2758
  seen = set()
2667
2759
  for failure in failures:
@@ -106,6 +106,7 @@ class FakeRiddle:
106
106
  'pricingQueryHashDropsTerminal',
107
107
  'pricingQueryHashStructuredNegativeControl',
108
108
  'pricingQueryHashPassesWithPageStateHashGap',
109
+ 'clickedProofNavigationTrailingSlashFalseAssertions',
109
110
  'clickedProofNavigation',
110
111
  'clickedHomeNavigation',
111
112
  'skipLinkTimeout',
@@ -621,6 +622,74 @@ class FakeRiddle:
621
622
  'RIDDLE_PROOF_EVIDENCE:' + json.dumps(proof_evidence),
622
623
  ],
623
624
  }
625
+ if 'clickedProofNavigationTrailingSlashFalseAssertions' in script:
626
+ page_state = {
627
+ 'bodyTextLength': 4113,
628
+ 'visibleTextSample': 'RIDDLE PROOF Turn a URL into evidence an agent can cite.',
629
+ 'interactiveElements': 43,
630
+ 'visibleInteractiveElements': 42,
631
+ 'pathname': '/proof/',
632
+ 'href': 'https://riddledc.com/proof/',
633
+ 'title': 'Riddle Proof',
634
+ 'buttons': [],
635
+ 'headings': ['Riddle Proof'],
636
+ 'links': [{'text': 'Proof', 'href': '/proof/'}],
637
+ 'canvasCount': 0,
638
+ 'largeVisibleElements': [{'tag': 'main', 'text': 'Riddle Proof'}],
639
+ }
640
+ proof_evidence = {
641
+ 'version': 'riddle-proof.interaction.v1',
642
+ 'reference': 'prod',
643
+ 'start': {'url': 'https://riddledc.com/', 'path': '/'},
644
+ 'action': 'click visible Proof nav link',
645
+ 'matchedUiText': 'Proof',
646
+ 'matchedHref': 'https://riddledc.com/proof/',
647
+ 'terminal': {'url': 'https://riddledc.com/proof/', 'path': '/proof/'},
648
+ 'assertions': [
649
+ {'name': 'start_path_is_root', 'expected': '/', 'actual': '/', 'passed': True},
650
+ {'name': 'proof_nav_link_visible', 'expected': 'visible Proof link', 'actual': 'Proof', 'passed': True},
651
+ {
652
+ 'name': 'terminal_url_exact',
653
+ 'expected': 'https://riddledc.com/proof/',
654
+ 'actual': 'https://riddledc.com/proof/',
655
+ 'passed': True,
656
+ },
657
+ {'name': 'terminal_path_exact', 'expected': '/proof', 'actual': '/proof/', 'passed': False},
658
+ {
659
+ 'name': 'interaction_contract_preserved',
660
+ 'expected': {
661
+ 'start_path': '/',
662
+ 'expected_terminal_path': '/proof',
663
+ 'expected_url': 'https://riddledc.com/proof/',
664
+ 'action': 'click visible Proof nav link',
665
+ },
666
+ 'actual': {
667
+ 'start_path': '/',
668
+ 'terminal_path': '/proof/',
669
+ 'terminal_url': 'https://riddledc.com/proof/',
670
+ 'action': 'click visible Proof nav link',
671
+ },
672
+ 'passed': False,
673
+ },
674
+ ],
675
+ 'errors': [],
676
+ 'passed': False,
677
+ }
678
+ return {
679
+ 'ok': True,
680
+ 'screenshots': [{'url': 'https://cdn.example.com/proof-after.png'}],
681
+ 'outputs': [{'name': 'after-proof.png', 'url': 'https://cdn.example.com/proof-after.png'}],
682
+ 'result': {'pageState': page_state, 'proofEvidence': proof_evidence},
683
+ 'console': [
684
+ 'RIDDLE_PROOF_STATE:' + json.dumps(page_state),
685
+ 'RIDDLE_PROOF_EVIDENCE:' + json.dumps(proof_evidence),
686
+ ],
687
+ 'visual_diff': {
688
+ 'diffPercentage': 1.2,
689
+ 'differentPixels': 12000,
690
+ 'totalPixels': 972000,
691
+ },
692
+ }
624
693
  if 'clickedProofNavigationOcLiveShape' in script:
625
694
  page_state = {
626
695
  'bodyTextLength': 4113,
@@ -3198,6 +3267,72 @@ def run_verify_interaction_iife_structured_evidence_without_screenshot():
3198
3267
  shutil.rmtree(tempdir, ignore_errors=True)
3199
3268
 
3200
3269
 
3270
+ def run_verify_interaction_trailing_slash_false_assertions_are_normalized():
3271
+ tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-interaction-trailing-slash-assertions-'))
3272
+ state_path = tempdir / 'state.json'
3273
+ try:
3274
+ state = base_state(tempdir, reference='prod', prod_url='https://riddledc.com/')
3275
+ state.update({
3276
+ 'remote_audit': True,
3277
+ 'workspace_kind': 'remote_audit',
3278
+ 'mode': 'server',
3279
+ 'recon_status': 'ready_for_proof_plan',
3280
+ 'author_status': 'ready',
3281
+ 'proof_plan_status': 'ready',
3282
+ 'implementation_status': 'not_required',
3283
+ 'implementation_mode': 'none',
3284
+ 'require_diff': False,
3285
+ 'allow_code_changes': False,
3286
+ 'verification_mode': 'interaction',
3287
+ 'server_path': '/',
3288
+ 'expected_start_path': '/',
3289
+ 'expected_terminal_path': '/proof',
3290
+ 'requested_expected_terminal_path': '/proof',
3291
+ 'proof_plan': 'Start at /, click Proof, and verify the terminal URL https://riddledc.com/proof/.',
3292
+ 'capture_script': "clickedProofNavigationTrailingSlashFalseAssertions(); await saveScreenshot('after-proof');",
3293
+ 'supervisor_author_packet': {
3294
+ 'proof_plan': 'Click Proof and prove the terminal route.',
3295
+ 'capture_script': "clickedProofNavigationTrailingSlashFalseAssertions(); await saveScreenshot('after-proof');",
3296
+ 'refined_inputs': {
3297
+ 'server_path': '/',
3298
+ 'expected_terminal_path': '/proof',
3299
+ },
3300
+ },
3301
+ 'recon_results': {'baselines': {}, 'mode': 'remote_audit'},
3302
+ })
3303
+ write_state(state_path, state)
3304
+ os.environ['RIDDLE_PROOF_STATE_FILE'] = str(state_path)
3305
+
3306
+ fake = FakeRiddle()
3307
+ load_util_with_fake(fake)
3308
+ load_module('verify_interaction_trailing_slash_false_assertions', VERIFY_PATH)
3309
+ after_verify = json.loads(state_path.read_text())
3310
+
3311
+ assert after_verify['verify_status'] == 'evidence_captured'
3312
+ assert after_verify['merge_recommendation'] == 'pending-supervisor-judgment'
3313
+ assert after_verify['structured_interaction_failure_summary'] == ''
3314
+ assert 'failed assertion' not in after_verify['proof_summary']
3315
+ observation = after_verify['verify_results']['after']['observation']
3316
+ assert observation['valid'] is True
3317
+ assert observation['details']['proof_evidence_route_matched'] is True
3318
+ assert observation['details']['observed_path_source'] == 'proof_evidence'
3319
+ assert 'wrong route' not in observation['reason']
3320
+ request = after_verify['verify_decision_request']
3321
+ assert request['structured_interaction_failure_summary'] == ''
3322
+ assert request['recommended_stage'] is None
3323
+ route = after_verify['proof_assessment_request']['semantic_context']['route']
3324
+ assert route['expected_after_path'] == '/proof'
3325
+ assert route['after_observed_path'] == '/proof'
3326
+ assert route['after_observed_path_raw'] == '/proof'
3327
+ return {
3328
+ 'ok': True,
3329
+ 'expected_path': after_verify['route_expectation']['expected_path'],
3330
+ 'after_observed_path': route['after_observed_path'],
3331
+ }
3332
+ finally:
3333
+ shutil.rmtree(tempdir, ignore_errors=True)
3334
+
3335
+
3201
3336
  def run_verify_interaction_proof_evidence_overrides_stale_expected_path():
3202
3337
  tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-interaction-stale-route-'))
3203
3338
  state_path = tempdir / 'state.json'
@@ -4238,6 +4373,7 @@ if __name__ == '__main__':
4238
4373
  'remote_interaction_audit_verify_rejects_default_capture': run_remote_interaction_audit_verify_rejects_default_capture(),
4239
4374
  'verify_interaction_terminal_route_from_proof_evidence': run_verify_interaction_terminal_route_from_proof_evidence(),
4240
4375
  'verify_interaction_iife_structured_evidence_without_screenshot': run_verify_interaction_iife_structured_evidence_without_screenshot(),
4376
+ 'verify_interaction_trailing_slash_false_assertions_are_normalized': run_verify_interaction_trailing_slash_false_assertions_are_normalized(),
4241
4377
  'verify_interaction_proof_evidence_overrides_stale_expected_path': run_verify_interaction_proof_evidence_overrides_stale_expected_path(),
4242
4378
  'verify_interaction_proof_plan_placeholder_uses_live_evidence': run_verify_interaction_proof_plan_placeholder_uses_live_evidence(),
4243
4379
  'verify_interaction_reverse_terminal_route_from_proof_evidence': run_verify_interaction_reverse_terminal_route_from_proof_evidence(),