@riddledc/riddle-proof 0.8.12 → 0.8.14

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 (36) hide show
  1. package/dist/advanced/engine-harness.cjs +101 -31
  2. package/dist/advanced/engine-harness.js +1 -1
  3. package/dist/advanced/index.cjs +101 -31
  4. package/dist/advanced/index.d.cts +2 -2
  5. package/dist/advanced/index.d.ts +2 -2
  6. package/dist/advanced/index.js +2 -2
  7. package/dist/advanced/proof-run-core.d.cts +1 -1
  8. package/dist/advanced/proof-run-core.d.ts +1 -1
  9. package/dist/advanced/proof-run-engine.cjs +101 -31
  10. package/dist/advanced/proof-run-engine.d.cts +2 -2
  11. package/dist/advanced/proof-run-engine.d.ts +2 -2
  12. package/dist/advanced/proof-run-engine.js +1 -1
  13. package/dist/{chunk-SZUC4MDN.js → chunk-EGZT3EVL.js} +1 -1
  14. package/dist/{chunk-JBY2SU5U.js → chunk-WJZYRUNV.js} +101 -31
  15. package/dist/cli/index.js +2 -2
  16. package/dist/cli.cjs +101 -31
  17. package/dist/cli.js +2 -2
  18. package/dist/engine-harness.cjs +101 -31
  19. package/dist/engine-harness.js +1 -1
  20. package/dist/index.cjs +101 -31
  21. package/dist/index.js +1 -1
  22. package/dist/{proof-run-core-CrpYH-qH.d.ts → proof-run-core-C8FDUhle.d.cts} +1 -1
  23. package/dist/{proof-run-core-CrpYH-qH.d.cts → proof-run-core-C8FDUhle.d.ts} +1 -1
  24. package/dist/proof-run-core.d.cts +1 -1
  25. package/dist/proof-run-core.d.ts +1 -1
  26. package/dist/{proof-run-engine-h9C1lC0w.d.ts → proof-run-engine-By7oLsF-.d.ts} +4 -4
  27. package/dist/{proof-run-engine-C6vYAZd8.d.cts → proof-run-engine-D80hVFMf.d.cts} +4 -4
  28. package/dist/proof-run-engine.cjs +101 -31
  29. package/dist/proof-run-engine.d.cts +2 -2
  30. package/dist/proof-run-engine.d.ts +2 -2
  31. package/dist/proof-run-engine.js +1 -1
  32. package/package.json +1 -1
  33. package/runtime/lib/verify.py +179 -11
  34. package/runtime/tests/recon_verify_smoke.py +142 -0
  35. package/runtime/tests/trust_boundary_regression.py +6 -0
  36. /package/dist/{chunk-RTLA6CPP.js → chunk-YFRPFV4U.js} +0 -0
@@ -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,23 +2042,64 @@ 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',
2071
+ 'final_route', 'finalRoute',
2072
+ )
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',
2062
2103
  'final_route', 'finalRoute',
2063
2104
  )
2064
2105
  FULL_LOCATION_PATH_KEYS = (
@@ -2087,6 +2128,19 @@ AFTER_STATE_KEYS = (
2087
2128
  'final', 'final_state', 'finalState',
2088
2129
  'expected_final', 'expectedFinal',
2089
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
+ )
2090
2144
  EVIDENCE_CONTAINER_KEYS = (
2091
2145
  'proofEvidence', 'proof_evidence',
2092
2146
  'interactionEvidence', 'interaction_evidence',
@@ -2113,10 +2167,10 @@ def path_candidate(value):
2113
2167
  return ''
2114
2168
 
2115
2169
 
2116
- def record_path_candidate(record, allow_location_keys=False):
2170
+ def record_path_candidate_for_keys(record, keys, allow_location_keys=False):
2117
2171
  if not isinstance(record, dict):
2118
2172
  return ''
2119
- keys = list(EXPLICIT_TERMINAL_PATH_KEYS)
2173
+ keys = list(keys)
2120
2174
  if allow_location_keys:
2121
2175
  keys.extend(LOCATION_PATH_KEYS)
2122
2176
  for key in keys:
@@ -2126,6 +2180,10 @@ def record_path_candidate(record, allow_location_keys=False):
2126
2180
  return ''
2127
2181
 
2128
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
+
2129
2187
  def terminal_path_from_record(record, depth=0):
2130
2188
  if not isinstance(record, dict) or depth > 4:
2131
2189
  return ''
@@ -2168,6 +2226,74 @@ def terminal_path_from_record(record, depth=0):
2168
2226
  return ''
2169
2227
 
2170
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
+
2171
2297
  def text_path_candidate(value):
2172
2298
  if not isinstance(value, str):
2173
2299
  return ''
@@ -2421,17 +2547,20 @@ def interaction_capture_failure_evidence_summary(proof_evidence):
2421
2547
 
2422
2548
  def interaction_terminal_path_from_evidence(proof_evidence):
2423
2549
  for record in proof_evidence_records(proof_evidence):
2424
- candidate = terminal_path_from_record(record)
2550
+ candidate = expected_terminal_path_from_record(record)
2425
2551
  if candidate:
2426
2552
  return candidate, 'proof_evidence_contract'
2427
- 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:
2428
2560
  for record in proof_evidence_records(proof_evidence):
2429
- for key in AFTER_STATE_KEYS:
2430
- value = record.get(key)
2431
- if isinstance(value, dict):
2432
- candidate = record_path_candidate(value, allow_location_keys=True)
2433
- if candidate:
2434
- return candidate, 'proof_evidence_after_state'
2561
+ candidate = observed_terminal_path_from_record(record)
2562
+ if candidate:
2563
+ return candidate, 'proof_evidence_contract'
2435
2564
  return '', ''
2436
2565
 
2437
2566
 
@@ -2469,6 +2598,16 @@ def proof_evidence_should_override_state_terminal_path(state_candidate, evidence
2469
2598
  return False
2470
2599
  if not state_candidate:
2471
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
2472
2611
  if route_matches_expected(state_candidate, evidence_candidate):
2473
2612
  return False
2474
2613
  if interaction_assertions_pass(proof_evidence):
@@ -2953,6 +3092,25 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
2953
3092
  and route_expectation.get('terminal_path')
2954
3093
  and route_expectation_source != 'recon_start_path'
2955
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
+ }
2956
3114
  recommended_stage = 'recon' if 'wrong route' in reason and not authored_terminal_route else 'author'
2957
3115
  mismatch = None
2958
3116
  if recommended_stage == 'recon':
@@ -3742,6 +3900,16 @@ has_judgable_failed_interaction_evidence = (
3742
3900
  and not proof_evidence_blocker
3743
3901
  and not visual_delta_recovery
3744
3902
  )
3903
+ route_expectation = s.get('route_expectation') if isinstance(s.get('route_expectation'), dict) else {}
3904
+ authored_terminal_route_mismatch = (
3905
+ verification_mode in INTERACTION_MODES
3906
+ and 'wrong route' in str(after_observation.get('reason') or '')
3907
+ and bool(route_expectation.get('terminal_path'))
3908
+ and str(route_expectation.get('source') or '') != 'recon_start_path'
3909
+ )
3910
+ if authored_terminal_route_mismatch:
3911
+ has_judgable_failed_interaction_evidence = False
3912
+ summary_lines.append('Structured interaction route gate: authored terminal route mismatch is a terminal capture blocker.')
3745
3913
  has_good_evidence = (
3746
3914
  required_baseline_present
3747
3915
  and (after_observation.get('valid') or has_judgable_failed_interaction_evidence)
@@ -325,6 +325,75 @@ class FakeRiddle:
325
325
  'proof.json': {'script_error': message},
326
326
  },
327
327
  }
328
+ if 'pricingQueryHashStructuredNegativeControl' in script:
329
+ page_state = {
330
+ 'bodyTextLength': 260,
331
+ 'visibleTextSample': 'Pricing One rate Browser Compute Example Costs',
332
+ 'interactiveElements': 8,
333
+ 'visibleInteractiveElements': 8,
334
+ 'pathname': '/pricing/',
335
+ 'search': '',
336
+ 'hash': '',
337
+ 'title': 'Pricing',
338
+ 'buttons': [],
339
+ 'headings': ['Pricing', 'Browser Compute'],
340
+ 'links': [{'text': 'Pricing', 'href': '/pricing/?rp_probe=1#pricing-probe'}],
341
+ 'canvasCount': 0,
342
+ 'largeVisibleElements': [{'tag': 'main', 'text': 'Pricing'}],
343
+ }
344
+ proof_evidence = {
345
+ 'version': 'riddle-proof.interaction.v1',
346
+ 'probe': 'query-hash-dropped-negative-control',
347
+ 'negativeControl': True,
348
+ 'routeExpectationSource': 'capture_script.expectedUrl',
349
+ 'expectedUrl': 'https://riddledc.com/pricing/?rp_probe=1#pricing-probe',
350
+ 'expectedHref': '/pricing/?rp_probe=1#pricing-probe',
351
+ 'intentionalObservedUrl': 'https://riddledc.com/pricing/',
352
+ 'start': {'href': 'https://riddledc.com/', 'pathname': '/', 'search': '', 'hash': ''},
353
+ 'action': {
354
+ 'type': 'rewrite-pricing-link-click-then-drop-query-hash',
355
+ 'afterClickHref': 'https://riddledc.com/pricing/?rp_probe=1#pricing-probe',
356
+ 'afterClickPathname': '/pricing/',
357
+ 'afterClickSearch': '?rp_probe=1',
358
+ 'afterClickHash': '#pricing-probe',
359
+ 'expectedNavigationReached': True,
360
+ },
361
+ 'terminal': {
362
+ 'href': 'https://riddledc.com/pricing/',
363
+ 'pathname': '/pricing/',
364
+ 'search': '',
365
+ 'hash': '',
366
+ },
367
+ 'assertions': {
368
+ 'expectedUrlReachedBeforeDrop': True,
369
+ 'expectedUrlStillPresentAtTerminal': False,
370
+ 'queryDropped': True,
371
+ 'hashDropped': True,
372
+ 'routeExpectationSourceIsCaptureScriptExpectedUrl': True,
373
+ 'shouldTerminalizeAsFailedInteractionCapture': True,
374
+ 'terminalMainVisible': True,
375
+ },
376
+ 'checks': {
377
+ 'routeMatches': False,
378
+ 'specificMismatchDetected': True,
379
+ },
380
+ 'errors': [],
381
+ }
382
+ return {
383
+ 'ok': True,
384
+ 'screenshots': [{'url': 'https://cdn.example.com/pricing-negative-control.png'}],
385
+ 'outputs': [{'name': 'after-pricing-negative-control.png', 'url': 'https://cdn.example.com/pricing-negative-control.png'}],
386
+ 'result': {'pageState': page_state, 'proofEvidence': proof_evidence},
387
+ 'console': [
388
+ 'RIDDLE_PROOF_STATE:' + json.dumps(page_state),
389
+ 'RIDDLE_PROOF_EVIDENCE:' + json.dumps(proof_evidence),
390
+ ],
391
+ 'visual_diff': {
392
+ 'diffPercentage': 1.2,
393
+ 'differentPixels': 12000,
394
+ 'totalPixels': 972000,
395
+ },
396
+ }
328
397
  if 'pricingQueryHashPassesWithPageStateHashGap' in script:
329
398
  page_state = {
330
399
  'bodyTextLength': 260,
@@ -2934,6 +3003,78 @@ def run_verify_interaction_query_hash_pass_uses_proof_evidence_route():
2934
3003
  shutil.rmtree(tempdir, ignore_errors=True)
2935
3004
 
2936
3005
 
3006
+ def run_verify_interaction_explicit_expected_url_blocks_dropped_terminal_route():
3007
+ tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-interaction-explicit-expected-url-mismatch-'))
3008
+ state_path = tempdir / 'state.json'
3009
+ try:
3010
+ state = base_state(tempdir, reference='before')
3011
+ state.update({
3012
+ 'recon_status': 'ready_for_proof_plan',
3013
+ 'author_status': 'ready',
3014
+ 'proof_plan_status': 'ready',
3015
+ 'implementation_status': 'changes_detected',
3016
+ 'verification_mode': 'interaction',
3017
+ 'server_path': '/',
3018
+ 'before_cdn': 'https://cdn.example.com/before-home.png',
3019
+ 'proof_plan': 'Start at /, click Pricing, and intentionally prove the query/hash route mismatch.',
3020
+ 'capture_script': "pricingQueryHashStructuredNegativeControl();",
3021
+ 'supervisor_author_packet': {
3022
+ 'proof_plan': 'Use expectedUrl as the route expectation and return structured evidence for the dropped query/hash terminal URL.',
3023
+ 'capture_script': "pricingQueryHashStructuredNegativeControl();",
3024
+ 'refined_inputs': {
3025
+ 'server_path': '/',
3026
+ },
3027
+ },
3028
+ 'recon_results': {
3029
+ 'baselines': {'before': {'path': '/', 'url': 'https://cdn.example.com/before-home.png'}},
3030
+ },
3031
+ })
3032
+ write_state(state_path, state)
3033
+ os.environ['RIDDLE_PROOF_STATE_FILE'] = str(state_path)
3034
+
3035
+ fake = FakeRiddle()
3036
+ load_util_with_fake(fake)
3037
+ load_module('verify_interaction_explicit_expected_url_blocks_dropped_terminal_route', VERIFY_PATH)
3038
+ after_verify = json.loads(state_path.read_text())
3039
+
3040
+ request = after_verify['verify_decision_request']
3041
+ assert after_verify['verify_status'] == 'capture_incomplete'
3042
+ assert after_verify['merge_recommendation'] == 'do-not-merge'
3043
+ assert after_verify['route_expectation']['source'] == 'proof_evidence_contract'
3044
+ assert after_verify['route_expectation']['expected_path'] == '/pricing?rp_probe=1#pricing-probe'
3045
+ assert after_verify['route_expectation']['expected_query'] == 'rp_probe=1'
3046
+ assert after_verify['route_expectation']['expected_hash'] == '#pricing-probe'
3047
+ assert request['recommended_stage'] is None
3048
+ assert request['continue_with_stage'] is None
3049
+ capture_quality = request['capture_quality']
3050
+ assert capture_quality['decision'] == 'failed_interaction_capture'
3051
+ assert capture_quality['blocking'] is True
3052
+ assert capture_quality['terminal_blocker'] is True
3053
+ assert capture_quality['mismatch']['expected_path'] == '/pricing?rp_probe=1#pricing-probe'
3054
+ assert capture_quality['mismatch']['observed_after_path'] in ('/pricing', '/pricing/')
3055
+ assert 'Interaction proof terminal route mismatch' in capture_quality['summary']
3056
+ assert after_verify['proof_assessment_request'] == {}
3057
+ observation = request['latest_observation']
3058
+ assert observation['valid'] is False
3059
+ assert 'wrong route' in observation['reason']
3060
+ supporting = after_verify['verify_results']['after']['supporting_artifacts']
3061
+ assert supporting['proof_evidence_present'] is True
3062
+ assert supporting['has_structured_payload'] is True
3063
+ route = after_verify['evidence_bundle']['semantic_context']['route']
3064
+ assert route['expected_terminal_query'] == 'rp_probe=1'
3065
+ assert route['expected_terminal_hash'] == '#pricing-probe'
3066
+ assert route['after_observed_path'] == '/pricing'
3067
+ assert route['after_observed_query'] == ''
3068
+ assert route['after_observed_hash'] == ''
3069
+ return {
3070
+ 'ok': True,
3071
+ 'decision': capture_quality['decision'],
3072
+ 'summary': capture_quality['summary'],
3073
+ }
3074
+ finally:
3075
+ shutil.rmtree(tempdir, ignore_errors=True)
3076
+
3077
+
2937
3078
  def run_verify_interaction_thrown_error_terminal_blocker():
2938
3079
  tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-interaction-thrown-error-'))
2939
3080
  state_path = tempdir / 'state.json'
@@ -3416,6 +3557,7 @@ if __name__ == '__main__':
3416
3557
  'verify_interaction_hash_terminal_route_from_proof_evidence': run_verify_interaction_hash_terminal_route_from_proof_evidence(),
3417
3558
  'verify_interaction_authored_query_hash_mismatch_blocks_with_evidence': run_verify_interaction_authored_query_hash_mismatch_blocks_with_evidence(),
3418
3559
  'verify_interaction_query_hash_pass_uses_proof_evidence_route': run_verify_interaction_query_hash_pass_uses_proof_evidence_route(),
3560
+ 'verify_interaction_explicit_expected_url_blocks_dropped_terminal_route': run_verify_interaction_explicit_expected_url_blocks_dropped_terminal_route(),
3419
3561
  'verify_interaction_thrown_error_terminal_blocker': run_verify_interaction_thrown_error_terminal_blocker(),
3420
3562
  'verify_capture_retry_surfaces_script_timeout': run_verify_capture_retry_surfaces_script_timeout(),
3421
3563
  'missing_baseline_guard': run_verify_missing_baseline(),
@@ -55,6 +55,12 @@ CASES = [
55
55
  'function': 'run_verify_interaction_authored_query_hash_mismatch_blocks_with_evidence',
56
56
  'expected_terminal': 'specific_blocker',
57
57
  },
58
+ {
59
+ 'name': 'query-hash-dropped-structured-negative-blocker',
60
+ 'covers': ['query/hash/trailing-slash URLs', 'invalid browser evidence', 'proof-evidence-present'],
61
+ 'function': 'run_verify_interaction_explicit_expected_url_blocks_dropped_terminal_route',
62
+ 'expected_terminal': 'specific_blocker',
63
+ },
58
64
  {
59
65
  'name': 'same-page-hash-pass',
60
66
  'covers': ['same-page hashes'],
File without changes