@riddledc/riddle-proof 0.7.80 → 0.7.81
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.
- package/README.md +12 -0
- package/dist/checkpoint.js +2 -2
- package/dist/{chunk-EXF7V6TJ.js → chunk-7F5LNUGR.js} +1 -1
- package/dist/{chunk-33XO42CY.js → chunk-ABQQLRTS.js} +1 -1
- package/dist/{chunk-JRFKVTKY.js → chunk-ENWYM4L7.js} +2 -2
- package/dist/{chunk-F7BNWDF6.js → chunk-MPPLZ35C.js} +4 -4
- package/dist/{chunk-4DM3OTWH.js → chunk-UYB7ABWU.js} +3 -2
- package/dist/{chunk-DUFDZJOF.js → chunk-VY4Y5U57.js} +1 -0
- package/dist/{chunk-3UHWI3FO.js → chunk-ZHEFYLII.js} +14 -2
- package/dist/cli.cjs +14 -0
- package/dist/cli.js +6 -6
- package/dist/codex-exec-agent.js +2 -2
- package/dist/engine-harness.cjs +14 -0
- package/dist/engine-harness.js +5 -5
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/local-agent.js +2 -2
- package/dist/openclaw.js +4 -4
- package/dist/result.cjs +1 -0
- package/dist/result.js +1 -1
- package/dist/run-card.cjs +12 -0
- package/dist/run-card.js +3 -3
- package/dist/runner.cjs +1 -0
- package/dist/runner.js +5 -5
- package/dist/state.cjs +13 -0
- package/dist/state.js +4 -4
- package/dist/types.d.cts +21 -1
- package/dist/types.d.ts +21 -1
- package/package.json +1 -1
- package/runtime/lib/preflight.py +7 -0
- package/runtime/lib/recon.py +40 -6
- package/runtime/lib/util.py +151 -3
- package/runtime/lib/verify.py +216 -32
- package/runtime/tests/recon_verify_smoke.py +96 -0
package/runtime/lib/verify.py
CHANGED
|
@@ -7,7 +7,7 @@ while good captures produce a structured evidence packet that the supervising ag
|
|
|
7
7
|
must assess before the wrapper routes back into author/implement/recon work or ship.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import json, os, struct, sys, time, zlib
|
|
10
|
+
import json, os, re, struct, sys, time, zlib
|
|
11
11
|
from urllib.parse import parse_qsl, urlparse
|
|
12
12
|
from urllib.request import Request, urlopen
|
|
13
13
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
@@ -18,6 +18,7 @@ from util import (
|
|
|
18
18
|
build_visual_proof_session,
|
|
19
19
|
capture_proof_session_seed,
|
|
20
20
|
capture_static_preview,
|
|
21
|
+
capture_viewport_matrix_status,
|
|
21
22
|
enrich_capture_payload,
|
|
22
23
|
has_auth_context,
|
|
23
24
|
invoke,
|
|
@@ -31,6 +32,9 @@ from util import (
|
|
|
31
32
|
should_use_static_preview,
|
|
32
33
|
proof_session_output_url,
|
|
33
34
|
summarize_capture_artifacts,
|
|
35
|
+
viewport_matrix_return_js,
|
|
36
|
+
viewport_matrix_screenshot_js,
|
|
37
|
+
viewport_matrix_setup_js,
|
|
34
38
|
)
|
|
35
39
|
import subprocess as sp
|
|
36
40
|
|
|
@@ -55,6 +59,15 @@ PLAYABILITY_MODES = {'playable', 'gameplay', 'game'}
|
|
|
55
59
|
PROOF_EVIDENCE_REQUIRED_MODES = {'audio'}
|
|
56
60
|
MIN_VISUAL_DELTA_PERCENT = 0.5
|
|
57
61
|
MIN_VISUAL_CHANGED_PIXELS = 5000
|
|
62
|
+
TARGETED_VISUAL_DELTA_PERCENT = 0.02
|
|
63
|
+
TARGETED_VISUAL_CHANGED_PIXELS = 250
|
|
64
|
+
TARGETED_SEMANTIC_STOPWORDS = {
|
|
65
|
+
'about', 'after', 'agent', 'again', 'before', 'browser', 'button',
|
|
66
|
+
'change', 'changed', 'click', 'copy', 'delta', 'does', 'from', 'into',
|
|
67
|
+
'layout', 'mode', 'page', 'proof', 'route', 'screen', 'shows', 'small',
|
|
68
|
+
'target', 'that', 'this', 'until', 'update', 'user', 'verify', 'view',
|
|
69
|
+
'visual', 'with',
|
|
70
|
+
}
|
|
58
71
|
VISUAL_DELTA_PERCENT_KEYS = {
|
|
59
72
|
'change_pct', 'change_percent', 'changed_percent', 'percent_changed',
|
|
60
73
|
'diff_percent', 'visual_delta_percent', 'pixel_change_percent',
|
|
@@ -133,7 +146,7 @@ def audit_current_capture_url(state, prod_url, expected_path):
|
|
|
133
146
|
|
|
134
147
|
|
|
135
148
|
def capture_current_target(state, target_url, label, capture_script, timeout=300):
|
|
136
|
-
script = build_capture_script(target_url, capture_script, label, state.get('wait_for_selector', ''))
|
|
149
|
+
script = build_capture_script(target_url, capture_script, label, state.get('wait_for_selector', ''), state.get('viewport_matrix'))
|
|
137
150
|
args = {'script': script, 'timeout_sec': 60}
|
|
138
151
|
apply_auth_context(state, args)
|
|
139
152
|
shot = invoke_retry('riddle_script', args, retries=3, timeout=max(timeout, 120))
|
|
@@ -312,9 +325,10 @@ def abort_capture_failure(state, results, expected_path, message, raw_payload):
|
|
|
312
325
|
raise SystemExit(summary)
|
|
313
326
|
|
|
314
327
|
|
|
315
|
-
def build_probe_capture_script(base_script='', verification_mode='proof', proof_session_seed=None):
|
|
328
|
+
def build_probe_capture_script(base_script='', verification_mode='proof', proof_session_seed=None, viewport_matrix=None):
|
|
316
329
|
pieces = []
|
|
317
330
|
script = (base_script or '').strip()
|
|
331
|
+
pieces.extend(viewport_matrix_setup_js(viewport_matrix))
|
|
318
332
|
pieces.append('let __riddleProofCaptureScriptError = null;')
|
|
319
333
|
pieces.append('let __riddleProofCaptureScriptResult = null;')
|
|
320
334
|
if script:
|
|
@@ -374,7 +388,10 @@ def build_probe_capture_script(base_script='', verification_mode='proof', proof_
|
|
|
374
388
|
' catch (err) { console.log(' + json.dumps(PROOF_EVIDENCE_PREFIX) + ' + JSON.stringify({ serialization_error: String(err) })); }',
|
|
375
389
|
'}',
|
|
376
390
|
])
|
|
377
|
-
|
|
391
|
+
viewport_screenshot_lines = viewport_matrix_screenshot_js('after-proof', viewport_matrix)
|
|
392
|
+
if auto_screenshot_for_mode(verification_mode) and viewport_screenshot_lines:
|
|
393
|
+
pieces.extend(viewport_screenshot_lines)
|
|
394
|
+
elif auto_screenshot_for_mode(verification_mode) and not capture_script_saves_screenshot(script):
|
|
378
395
|
pieces.append("await saveScreenshot('after-proof');")
|
|
379
396
|
if isinstance(proof_session_seed, dict):
|
|
380
397
|
pieces.append(
|
|
@@ -383,7 +400,7 @@ def build_probe_capture_script(base_script='', verification_mode='proof', proof_
|
|
|
383
400
|
'); } catch {}'
|
|
384
401
|
)
|
|
385
402
|
pieces.append('if (__riddleProofCaptureScriptError) throw __riddleProofCaptureScriptError;')
|
|
386
|
-
pieces.append('return { pageState, proofEvidence: __riddleProofEvidenceValue };')
|
|
403
|
+
pieces.append('return { pageState, proofEvidence: __riddleProofEvidenceValue, viewportMatrix: ' + viewport_matrix_return_js() + ' };')
|
|
387
404
|
return ' '.join(pieces)
|
|
388
405
|
|
|
389
406
|
|
|
@@ -885,7 +902,144 @@ def find_metric_value(value, key_names, depth=0):
|
|
|
885
902
|
return None
|
|
886
903
|
|
|
887
904
|
|
|
888
|
-
def
|
|
905
|
+
def targeted_semantic_tokens(value, limit=24):
|
|
906
|
+
text = ''
|
|
907
|
+
try:
|
|
908
|
+
text = json.dumps(value, sort_keys=True) if isinstance(value, (dict, list)) else str(value or '')
|
|
909
|
+
except Exception:
|
|
910
|
+
text = str(value or '')
|
|
911
|
+
tokens = []
|
|
912
|
+
for token in re.findall(r'[a-z0-9]+', text.lower()):
|
|
913
|
+
if len(token) < 4 or token in TARGETED_SEMANTIC_STOPWORDS or token in tokens:
|
|
914
|
+
continue
|
|
915
|
+
tokens.append(token)
|
|
916
|
+
if len(tokens) >= limit:
|
|
917
|
+
break
|
|
918
|
+
return tokens
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def semantic_visible_text_for_delta(state=None, semantic_context=None):
|
|
922
|
+
chunks = []
|
|
923
|
+
after = (semantic_context or {}).get('after') if isinstance(semantic_context, dict) else {}
|
|
924
|
+
for source in [after or {}]:
|
|
925
|
+
if not isinstance(source, dict):
|
|
926
|
+
continue
|
|
927
|
+
for key in ('visible_text_sample', 'title'):
|
|
928
|
+
if source.get(key):
|
|
929
|
+
chunks.append(str(source.get(key)))
|
|
930
|
+
for key in ('headings', 'buttons', 'links', 'large_visible_elements'):
|
|
931
|
+
value = source.get(key)
|
|
932
|
+
try:
|
|
933
|
+
chunks.append(json.dumps(value, sort_keys=True))
|
|
934
|
+
except Exception:
|
|
935
|
+
chunks.append(str(value))
|
|
936
|
+
return ' '.join(chunks).lower()
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def targeted_assertion_text_sources(value, depth=0):
|
|
940
|
+
if depth > 8:
|
|
941
|
+
return []
|
|
942
|
+
sources = []
|
|
943
|
+
if isinstance(value, list):
|
|
944
|
+
for item in value:
|
|
945
|
+
sources.extend(targeted_assertion_text_sources(item, depth + 1))
|
|
946
|
+
return sources
|
|
947
|
+
if not isinstance(value, dict):
|
|
948
|
+
return sources
|
|
949
|
+
|
|
950
|
+
assertion_type = str(value.get('type') or value.get('kind') or '').strip().lower()
|
|
951
|
+
text_keys = (
|
|
952
|
+
'text', 'contains', 'pattern', 'label', 'copy', 'expected',
|
|
953
|
+
'expected_text', 'expectedText', 'visible_text', 'visibleText',
|
|
954
|
+
)
|
|
955
|
+
text_values = [
|
|
956
|
+
value.get(key)
|
|
957
|
+
for key in text_keys
|
|
958
|
+
if isinstance(value.get(key), str) and value.get(key).strip()
|
|
959
|
+
]
|
|
960
|
+
type_is_textual = any(marker in assertion_type for marker in (
|
|
961
|
+
'text', 'copy', 'visible', 'contains', 'label', 'heading', 'button', 'link', 'badge',
|
|
962
|
+
))
|
|
963
|
+
if text_values and (type_is_textual or not assertion_type):
|
|
964
|
+
sources.extend(text_values)
|
|
965
|
+
|
|
966
|
+
for key in ('assertions', 'checks', 'expected', 'match', 'matches'):
|
|
967
|
+
nested = value.get(key)
|
|
968
|
+
if isinstance(nested, (dict, list)):
|
|
969
|
+
sources.extend(targeted_assertion_text_sources(nested, depth + 1))
|
|
970
|
+
return sources
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def visual_delta_semantic_support(state=None, semantic_context=None):
|
|
974
|
+
state = state if isinstance(state, dict) else {}
|
|
975
|
+
visible_text = semantic_visible_text_for_delta(state, semantic_context)
|
|
976
|
+
if not visible_text:
|
|
977
|
+
return {
|
|
978
|
+
'supported': False,
|
|
979
|
+
'matched_tokens': [],
|
|
980
|
+
'reason': 'no semantic text sample was available for targeted visual-delta thresholds',
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
assertion_sources = targeted_assertion_text_sources(state.get('parsed_assertions'))
|
|
984
|
+
assertion_tokens = []
|
|
985
|
+
for source in assertion_sources:
|
|
986
|
+
for token in targeted_semantic_tokens(source):
|
|
987
|
+
if token not in assertion_tokens:
|
|
988
|
+
assertion_tokens.append(token)
|
|
989
|
+
if not assertion_tokens:
|
|
990
|
+
return {
|
|
991
|
+
'supported': False,
|
|
992
|
+
'matched_tokens': [],
|
|
993
|
+
'reason': 'targeted visual-delta thresholds require text/visible assertions for the intended small change',
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
matched_assertion_tokens = [token for token in assertion_tokens if token in visible_text]
|
|
997
|
+
context_tokens = []
|
|
998
|
+
for source in (state.get('success_criteria'), state.get('change_request'), state.get('proof_plan')):
|
|
999
|
+
for token in targeted_semantic_tokens(source):
|
|
1000
|
+
if token not in context_tokens:
|
|
1001
|
+
context_tokens.append(token)
|
|
1002
|
+
matched_context_tokens = [token for token in context_tokens if token in visible_text]
|
|
1003
|
+
return {
|
|
1004
|
+
'supported': len(matched_assertion_tokens) >= 1,
|
|
1005
|
+
'matched_tokens': (matched_assertion_tokens + [token for token in matched_context_tokens if token not in matched_assertion_tokens])[:12],
|
|
1006
|
+
'matched_assertion_tokens': matched_assertion_tokens[:12],
|
|
1007
|
+
'matched_context_tokens': matched_context_tokens[:12],
|
|
1008
|
+
'reason': 'text assertions matched visible after evidence' if matched_assertion_tokens else 'no text assertion tokens matched visible after evidence',
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def visual_delta_thresholds_for_context(state=None, semantic_context=None):
|
|
1013
|
+
semantic = visual_delta_semantic_support(state, semantic_context)
|
|
1014
|
+
if semantic.get('supported'):
|
|
1015
|
+
return {
|
|
1016
|
+
'mode': 'targeted_semantic',
|
|
1017
|
+
'min_change_percent': TARGETED_VISUAL_DELTA_PERCENT,
|
|
1018
|
+
'min_changed_pixels': TARGETED_VISUAL_CHANGED_PIXELS,
|
|
1019
|
+
'default_min_change_percent': MIN_VISUAL_DELTA_PERCENT,
|
|
1020
|
+
'default_min_changed_pixels': MIN_VISUAL_CHANGED_PIXELS,
|
|
1021
|
+
'semantic_support': semantic,
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
'mode': 'default',
|
|
1025
|
+
'min_change_percent': MIN_VISUAL_DELTA_PERCENT,
|
|
1026
|
+
'min_changed_pixels': MIN_VISUAL_CHANGED_PIXELS,
|
|
1027
|
+
'semantic_support': semantic,
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def visual_delta_pass_reason(source_label, passed, thresholds):
|
|
1032
|
+
mode = (thresholds or {}).get('mode')
|
|
1033
|
+
if passed and mode == 'targeted_semantic':
|
|
1034
|
+
return source_label + ' clears the targeted-change threshold with matching semantic/text evidence.'
|
|
1035
|
+
if passed:
|
|
1036
|
+
return source_label + ' clears the legibility threshold.'
|
|
1037
|
+
if mode == 'targeted_semantic':
|
|
1038
|
+
return source_label + ' is below even the targeted-change threshold; capture success alone is not proof.'
|
|
1039
|
+
return source_label + ' is below the legibility threshold; capture success alone is not proof.'
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def extract_visual_delta(payload, state=None, semantic_context=None):
|
|
889
1043
|
payload = enrich_capture_payload(payload)
|
|
890
1044
|
result = payload.get('result') if isinstance(payload, dict) else {}
|
|
891
1045
|
proof_json = payload.get('_proof_json') if isinstance(payload, dict) else {}
|
|
@@ -975,8 +1129,11 @@ def extract_visual_delta(payload):
|
|
|
975
1129
|
},
|
|
976
1130
|
}
|
|
977
1131
|
|
|
978
|
-
|
|
979
|
-
|
|
1132
|
+
thresholds = visual_delta_thresholds_for_context(state, semantic_context)
|
|
1133
|
+
min_percent = thresholds['min_change_percent']
|
|
1134
|
+
min_pixels = thresholds['min_changed_pixels']
|
|
1135
|
+
percent_pass = percent is not None and percent >= min_percent
|
|
1136
|
+
pixel_pass = changed_pixels is not None and changed_pixels >= min_pixels
|
|
980
1137
|
passed = percent_pass or pixel_pass
|
|
981
1138
|
return {
|
|
982
1139
|
'status': 'measured',
|
|
@@ -984,13 +1141,11 @@ def extract_visual_delta(payload):
|
|
|
984
1141
|
'change_percent': round(percent, 4) if percent is not None else None,
|
|
985
1142
|
'changed_pixels': int(changed_pixels) if changed_pixels is not None else None,
|
|
986
1143
|
'total_pixels': int(total_pixels) if total_pixels is not None else None,
|
|
987
|
-
'min_change_percent':
|
|
988
|
-
'min_changed_pixels':
|
|
989
|
-
'
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
'Measured visual delta is below the legibility threshold; capture success alone is not proof.'
|
|
993
|
-
),
|
|
1144
|
+
'min_change_percent': min_percent,
|
|
1145
|
+
'min_changed_pixels': min_pixels,
|
|
1146
|
+
'threshold_mode': thresholds.get('mode'),
|
|
1147
|
+
'semantic_support': thresholds.get('semantic_support'),
|
|
1148
|
+
'reason': visual_delta_pass_reason('Measured visual delta', passed, thresholds),
|
|
994
1149
|
}
|
|
995
1150
|
|
|
996
1151
|
|
|
@@ -1165,7 +1320,7 @@ def compare_rgba_images(before_image, after_image, threshold=10):
|
|
|
1165
1320
|
}
|
|
1166
1321
|
|
|
1167
1322
|
|
|
1168
|
-
def measure_visual_delta_from_image_artifacts(before_url, after_url):
|
|
1323
|
+
def measure_visual_delta_from_image_artifacts(before_url, after_url, state=None, semantic_context=None):
|
|
1169
1324
|
if not (image_artifact_url(before_url) and image_artifact_url(after_url)):
|
|
1170
1325
|
return {
|
|
1171
1326
|
'status': 'skipped',
|
|
@@ -1183,8 +1338,11 @@ def measure_visual_delta_from_image_artifacts(before_url, after_url):
|
|
|
1183
1338
|
changed_pixels = comparison.get('changed_pixels')
|
|
1184
1339
|
total_pixels = comparison.get('total_pixels')
|
|
1185
1340
|
percent = comparison.get('change_percent')
|
|
1186
|
-
|
|
1187
|
-
|
|
1341
|
+
thresholds = visual_delta_thresholds_for_context(state, semantic_context)
|
|
1342
|
+
min_percent = thresholds['min_change_percent']
|
|
1343
|
+
min_pixels = thresholds['min_changed_pixels']
|
|
1344
|
+
percent_pass = percent is not None and percent >= min_percent
|
|
1345
|
+
pixel_pass = changed_pixels is not None and changed_pixels >= min_pixels
|
|
1188
1346
|
passed = bool(percent_pass or pixel_pass)
|
|
1189
1347
|
return {
|
|
1190
1348
|
'status': 'measured',
|
|
@@ -1192,14 +1350,12 @@ def measure_visual_delta_from_image_artifacts(before_url, after_url):
|
|
|
1192
1350
|
'change_percent': round(percent, 4) if percent is not None else None,
|
|
1193
1351
|
'changed_pixels': int(changed_pixels) if changed_pixels is not None else None,
|
|
1194
1352
|
'total_pixels': int(total_pixels) if total_pixels is not None else None,
|
|
1195
|
-
'min_change_percent':
|
|
1196
|
-
'min_changed_pixels':
|
|
1353
|
+
'min_change_percent': min_percent,
|
|
1354
|
+
'min_changed_pixels': min_pixels,
|
|
1355
|
+
'threshold_mode': thresholds.get('mode'),
|
|
1356
|
+
'semantic_support': thresholds.get('semantic_support'),
|
|
1197
1357
|
'source': 'riddle_artifact_image_diff',
|
|
1198
|
-
'reason': (
|
|
1199
|
-
'Measured visual delta from before/after screenshot artifacts clears the legibility threshold.'
|
|
1200
|
-
if passed else
|
|
1201
|
-
'Measured visual delta from before/after screenshot artifacts is below the legibility threshold.'
|
|
1202
|
-
),
|
|
1358
|
+
'reason': visual_delta_pass_reason('Measured visual delta from before/after screenshot artifacts', passed, thresholds),
|
|
1203
1359
|
'comparison': {
|
|
1204
1360
|
'before_url': before_url,
|
|
1205
1361
|
'after_url': after_url,
|
|
@@ -1219,7 +1375,7 @@ def add_visual_delta_diagnostic(visual_delta, key, value):
|
|
|
1219
1375
|
return updated
|
|
1220
1376
|
|
|
1221
1377
|
|
|
1222
|
-
def measure_visual_delta_against_baseline(state, results, after_payload, current_visual_delta):
|
|
1378
|
+
def measure_visual_delta_against_baseline(state, results, after_payload, current_visual_delta, semantic_context=None):
|
|
1223
1379
|
if not visual_delta_required_for_state(state):
|
|
1224
1380
|
return current_visual_delta
|
|
1225
1381
|
if isinstance(current_visual_delta, dict) and current_visual_delta.get('status') == 'measured':
|
|
@@ -1246,7 +1402,7 @@ def measure_visual_delta_against_baseline(state, results, after_payload, current
|
|
|
1246
1402
|
},
|
|
1247
1403
|
)
|
|
1248
1404
|
|
|
1249
|
-
artifact_delta = measure_visual_delta_from_image_artifacts(before_url, after_url)
|
|
1405
|
+
artifact_delta = measure_visual_delta_from_image_artifacts(before_url, after_url, state, semantic_context)
|
|
1250
1406
|
append_capture_diagnostic(state, 'visual_delta', 'riddle_artifact_image_diff', {
|
|
1251
1407
|
'baseline_label': baseline.get('label') or '',
|
|
1252
1408
|
'before_url': before_url,
|
|
@@ -1281,7 +1437,7 @@ def measure_visual_delta_against_baseline(state, results, after_payload, current
|
|
|
1281
1437
|
},
|
|
1282
1438
|
)
|
|
1283
1439
|
|
|
1284
|
-
measured = extract_visual_delta(payload)
|
|
1440
|
+
measured = extract_visual_delta(payload, state, semantic_context)
|
|
1285
1441
|
if isinstance(measured, dict) and measured.get('status') == 'measured':
|
|
1286
1442
|
measured['source'] = 'riddle_visual_diff'
|
|
1287
1443
|
measured['comparison'] = {
|
|
@@ -1895,6 +2051,8 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1895
2051
|
proof_evidence = extract_proof_evidence(after_payload)
|
|
1896
2052
|
playability_assessment = assess_playability_evidence(proof_evidence)
|
|
1897
2053
|
playability_evidence = extract_playability_evidence(proof_evidence)
|
|
2054
|
+
semantic_context = build_semantic_context(state, results, after_observation, expected_path)
|
|
2055
|
+
viewport_matrix = (results.get('after') or {}).get('viewport_matrix') or capture_viewport_matrix_status(state, after_payload, 'after-proof')
|
|
1898
2056
|
if audit_no_diff_mode(state):
|
|
1899
2057
|
visual_delta = {
|
|
1900
2058
|
'status': 'not_applicable',
|
|
@@ -1903,12 +2061,11 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1903
2061
|
}
|
|
1904
2062
|
else:
|
|
1905
2063
|
visual_delta = (
|
|
1906
|
-
extract_visual_delta(after_payload)
|
|
2064
|
+
extract_visual_delta(after_payload, state, semantic_context)
|
|
1907
2065
|
if visual_delta_applies(state.get('verification_mode'))
|
|
1908
2066
|
else {'status': 'not_applicable', 'passed': None, 'reason': 'Verification mode does not require visual delta gating.'}
|
|
1909
2067
|
)
|
|
1910
|
-
visual_delta = measure_visual_delta_against_baseline(state, results, after_payload, visual_delta)
|
|
1911
|
-
semantic_context = build_semantic_context(state, results, after_observation, expected_path)
|
|
2068
|
+
visual_delta = measure_visual_delta_against_baseline(state, results, after_payload, visual_delta, semantic_context)
|
|
1912
2069
|
artifact_contract = artifact_contract_for_state(state)
|
|
1913
2070
|
artifact_production = artifact_production_summary(after_payload, supporting)
|
|
1914
2071
|
artifact_usage = artifact_usage_summary(
|
|
@@ -1948,6 +2105,7 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1948
2105
|
'semantic_context': semantic_context,
|
|
1949
2106
|
'artifact_contract': artifact_contract,
|
|
1950
2107
|
'artifact_usage': artifact_usage,
|
|
2108
|
+
'viewport_matrix': viewport_matrix,
|
|
1951
2109
|
},
|
|
1952
2110
|
status='evidence_captured' if after_observation.get('valid') else 'capture_incomplete',
|
|
1953
2111
|
)
|
|
@@ -1958,6 +2116,7 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1958
2116
|
'required_baseline_present': required_baseline_present,
|
|
1959
2117
|
'baseline': results.get('baseline') or {},
|
|
1960
2118
|
'semantic_context': semantic_context,
|
|
2119
|
+
'viewport_matrix': viewport_matrix,
|
|
1961
2120
|
'artifact_contract': artifact_contract,
|
|
1962
2121
|
'artifact_production': artifact_production,
|
|
1963
2122
|
'artifact_usage': artifact_usage,
|
|
@@ -2023,6 +2182,7 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
|
|
|
2023
2182
|
semantic_context,
|
|
2024
2183
|
evidence_basis,
|
|
2025
2184
|
)
|
|
2185
|
+
viewport_matrix = (evidence_bundle or {}).get('viewport_matrix') if isinstance(evidence_bundle, dict) else None
|
|
2026
2186
|
if isinstance(evidence_bundle, dict):
|
|
2027
2187
|
evidence_bundle['artifact_contract'] = artifact_contract
|
|
2028
2188
|
evidence_bundle['artifact_production'] = artifact_production
|
|
@@ -2071,6 +2231,7 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
|
|
|
2071
2231
|
'supporting_artifacts': supporting,
|
|
2072
2232
|
'visual_delta': visual_delta,
|
|
2073
2233
|
'semantic_context': semantic_context,
|
|
2234
|
+
'viewport_matrix': viewport_matrix,
|
|
2074
2235
|
'evidence_bundle': evidence_bundle or {},
|
|
2075
2236
|
'evidence_basis': evidence_basis,
|
|
2076
2237
|
'artifact_contract': artifact_contract,
|
|
@@ -2124,7 +2285,7 @@ expected_path = (
|
|
|
2124
2285
|
)
|
|
2125
2286
|
verification_mode = normalized_verification_mode(s.get('verification_mode'))
|
|
2126
2287
|
proof_session_seed = capture_proof_session_seed(s, expected_path)
|
|
2127
|
-
probe_capture_script = build_probe_capture_script(capture_script, verification_mode, proof_session_seed)
|
|
2288
|
+
probe_capture_script = build_probe_capture_script(capture_script, verification_mode, proof_session_seed, s.get('viewport_matrix'))
|
|
2128
2289
|
results = {
|
|
2129
2290
|
'baseline': {
|
|
2130
2291
|
'reference': reference,
|
|
@@ -2297,9 +2458,30 @@ else:
|
|
|
2297
2458
|
results['after'] = {'screenshots': [{'url': capture.get('url', '')}] if capture.get('url') else [], 'raw': capture.get('raw')}
|
|
2298
2459
|
s['after_cdn'] = capture.get('url', '')
|
|
2299
2460
|
|
|
2461
|
+
after_viewport_matrix = capture_viewport_matrix_status(s, after_payload, 'after-proof')
|
|
2300
2462
|
after_observation = evaluate_capture_quality(after_payload, expected_path, verification_mode)
|
|
2463
|
+
details = after_observation.get('details') if isinstance(after_observation.get('details'), dict) else {}
|
|
2464
|
+
details['viewport_matrix'] = after_viewport_matrix
|
|
2465
|
+
after_observation['details'] = details
|
|
2466
|
+
if after_viewport_matrix.get('status') == 'incomplete':
|
|
2467
|
+
missing_names = [
|
|
2468
|
+
str(item.get('name') or item.get('slug') or '').strip()
|
|
2469
|
+
for item in after_viewport_matrix.get('missing') or []
|
|
2470
|
+
if str(item.get('name') or item.get('slug') or '').strip()
|
|
2471
|
+
]
|
|
2472
|
+
missing_text = ', '.join(missing_names[:8]) or 'requested viewport screenshots'
|
|
2473
|
+
reason = 'missing requested viewport evidence: ' + missing_text
|
|
2474
|
+
after_observation['valid'] = False
|
|
2475
|
+
after_observation['telemetry_ready'] = False
|
|
2476
|
+
after_observation['reason'] = (
|
|
2477
|
+
(after_observation.get('reason') + '; ' + reason)
|
|
2478
|
+
if after_observation.get('reason') and after_observation.get('reason') != 'ok'
|
|
2479
|
+
else reason
|
|
2480
|
+
)
|
|
2481
|
+
s['viewport_matrix_status'] = after_viewport_matrix
|
|
2301
2482
|
results['after']['observation'] = after_observation
|
|
2302
2483
|
results['after']['supporting_artifacts'] = collect_supporting_artifacts(after_payload)
|
|
2484
|
+
results['after']['viewport_matrix'] = after_viewport_matrix
|
|
2303
2485
|
record_verify_phase('capture', 'completed', 'After-proof capture completed.')
|
|
2304
2486
|
|
|
2305
2487
|
# Structured proof summary
|
|
@@ -2338,6 +2520,8 @@ if existing_before:
|
|
|
2338
2520
|
if existing_prod:
|
|
2339
2521
|
summary_lines.append('Prod baseline (recon): ' + existing_prod)
|
|
2340
2522
|
summary_lines.append('After screenshot: ' + (s.get('after_cdn') or '(none)'))
|
|
2523
|
+
if after_viewport_matrix.get('status') not in ('not_requested', ''):
|
|
2524
|
+
summary_lines.append('Viewport matrix: ' + after_viewport_matrix.get('status', 'unknown') + ' (' + str(len(after_viewport_matrix.get('executed') or [])) + '/' + str(len(after_viewport_matrix.get('requested') or [])) + ' captured)')
|
|
2341
2525
|
summary_lines.append('Expected proof path from recon: ' + expected_path)
|
|
2342
2526
|
summary_lines.append('After observation: ' + after_observation['reason'])
|
|
2343
2527
|
supporting = results['after'].get('supporting_artifacts') or {}
|
|
@@ -592,6 +592,77 @@ def run_verify_quality_ignores_proof_telemetry_console_text():
|
|
|
592
592
|
assert weak_delta['passed'] is False
|
|
593
593
|
assert 'below the legibility threshold' in weak_delta['reason']
|
|
594
594
|
|
|
595
|
+
targeted_delta = namespace['extract_visual_delta'](
|
|
596
|
+
{
|
|
597
|
+
'ok': True,
|
|
598
|
+
'result': {
|
|
599
|
+
'proofEvidence': {
|
|
600
|
+
'change_pct': '0.0487',
|
|
601
|
+
'changed_pixels': 2351,
|
|
602
|
+
'width': 1280,
|
|
603
|
+
'height': 900,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
'change_request': 'Add the Riddle Proof recovery smoke badge',
|
|
609
|
+
'success_criteria': 'The recovery smoke badge is visible after verify.',
|
|
610
|
+
'parsed_assertions': [{'kind': 'text_visible', 'text': 'recovery smoke badge'}],
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
'after': {
|
|
614
|
+
'visible_text_sample': 'Home page Riddle Proof recovery smoke badge',
|
|
615
|
+
'headings': ['Home page'],
|
|
616
|
+
'buttons': ['Start'],
|
|
617
|
+
'links': [],
|
|
618
|
+
},
|
|
619
|
+
'route': {'after_observed_path': '/'},
|
|
620
|
+
},
|
|
621
|
+
)
|
|
622
|
+
assert targeted_delta['status'] == 'measured'
|
|
623
|
+
assert targeted_delta['passed'] is True
|
|
624
|
+
assert targeted_delta['threshold_mode'] == 'targeted_semantic'
|
|
625
|
+
assert targeted_delta['min_changed_pixels'] == 250
|
|
626
|
+
assert 'targeted-change threshold' in targeted_delta['reason']
|
|
627
|
+
|
|
628
|
+
unmatched_targeted_delta = namespace['extract_visual_delta'](
|
|
629
|
+
{
|
|
630
|
+
'ok': True,
|
|
631
|
+
'result': {
|
|
632
|
+
'proofEvidence': {
|
|
633
|
+
'change_pct': '0.0487',
|
|
634
|
+
'changed_pixels': 2351,
|
|
635
|
+
'width': 1280,
|
|
636
|
+
'height': 900,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{'change_request': 'Add the billing badge'},
|
|
641
|
+
{'after': {'visible_text_sample': 'Home page unchanged copy'}},
|
|
642
|
+
)
|
|
643
|
+
assert unmatched_targeted_delta['status'] == 'measured'
|
|
644
|
+
assert unmatched_targeted_delta['passed'] is False
|
|
645
|
+
assert unmatched_targeted_delta['threshold_mode'] == 'default'
|
|
646
|
+
|
|
647
|
+
generic_page_token_delta = namespace['extract_visual_delta'](
|
|
648
|
+
{
|
|
649
|
+
'ok': True,
|
|
650
|
+
'result': {
|
|
651
|
+
'proofEvidence': {
|
|
652
|
+
'change_pct': '0.0487',
|
|
653
|
+
'changed_pixels': 2351,
|
|
654
|
+
'width': 1280,
|
|
655
|
+
'height': 900,
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
{'change_request': 'Polish the pricing page CTA'},
|
|
660
|
+
{'after': {'visible_text_sample': 'Pricing plans for teams'}},
|
|
661
|
+
)
|
|
662
|
+
assert generic_page_token_delta['status'] == 'measured'
|
|
663
|
+
assert generic_page_token_delta['passed'] is False
|
|
664
|
+
assert generic_page_token_delta['threshold_mode'] == 'default'
|
|
665
|
+
|
|
595
666
|
unmeasured_delta = namespace['extract_visual_delta']({
|
|
596
667
|
'ok': True,
|
|
597
668
|
'screenshots': [{'name': 'after-proof.png', 'url': 'https://cdn.example.com/after-proof.png'}],
|
|
@@ -617,6 +688,31 @@ def run_verify_quality_ignores_proof_telemetry_console_text():
|
|
|
617
688
|
assert visual_diff_delta['change_percent'] == 1.45
|
|
618
689
|
assert visual_diff_delta['changed_pixels'] == 14094
|
|
619
690
|
|
|
691
|
+
viewport_status = namespace['capture_viewport_matrix_status'](
|
|
692
|
+
{
|
|
693
|
+
'viewport_matrix': [
|
|
694
|
+
{'name': 'phone', 'width': 390, 'height': 844},
|
|
695
|
+
{'name': 'ipad', 'width': 820, 'height': 1180},
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
'outputs': [
|
|
700
|
+
{'name': 'after-proof-phone.png', 'url': 'https://cdn.example.com/phone.png'},
|
|
701
|
+
{'name': 'after-proof-ipad.png', 'url': 'https://cdn.example.com/ipad.png'},
|
|
702
|
+
],
|
|
703
|
+
},
|
|
704
|
+
'after-proof',
|
|
705
|
+
)
|
|
706
|
+
assert viewport_status['status'] == 'complete'
|
|
707
|
+
assert [item['name'] for item in viewport_status['executed']] == ['phone', 'ipad']
|
|
708
|
+
missing_viewport_status = namespace['capture_viewport_matrix_status'](
|
|
709
|
+
{'viewport_matrix': [{'name': 'phone', 'width': 390, 'height': 844}]},
|
|
710
|
+
{'outputs': [{'name': 'after-proof-desktop.png', 'url': 'https://cdn.example.com/desktop.png'}]},
|
|
711
|
+
'after-proof',
|
|
712
|
+
)
|
|
713
|
+
assert missing_viewport_status['status'] == 'incomplete'
|
|
714
|
+
assert missing_viewport_status['missing'][0]['name'] == 'phone'
|
|
715
|
+
|
|
620
716
|
before_png = png_rgba(2, 1, bytes([
|
|
621
717
|
0, 0, 0, 255,
|
|
622
718
|
0, 0, 0, 255,
|