@riddledc/riddle-proof 0.5.56 → 0.5.57
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/package.json +1 -1
- package/runtime/lib/verify.py +266 -5
- package/runtime/tests/recon_verify_smoke.py +119 -8
package/package.json
CHANGED
package/runtime/lib/verify.py
CHANGED
|
@@ -7,8 +7,9 @@ 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, sys, time
|
|
10
|
+
import json, os, struct, sys, time, zlib
|
|
11
11
|
from urllib.parse import parse_qsl, urlparse
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
12
13
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
14
|
from util import (
|
|
14
15
|
append_capture_diagnostic,
|
|
@@ -935,6 +936,204 @@ def visual_delta_baseline_candidate(state, results):
|
|
|
935
936
|
return {'label': '', 'url': ''}
|
|
936
937
|
|
|
937
938
|
|
|
939
|
+
def image_artifact_url(value):
|
|
940
|
+
text = str(value or '').strip()
|
|
941
|
+
if not text:
|
|
942
|
+
return False
|
|
943
|
+
parsed = urlparse(text)
|
|
944
|
+
if parsed.scheme != 'https':
|
|
945
|
+
return False
|
|
946
|
+
path = parsed.path.lower()
|
|
947
|
+
return path.endswith(IMAGE_EXTENSIONS)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def fetch_url_bytes(url, timeout=20, max_bytes=25 * 1024 * 1024):
|
|
951
|
+
request = Request(
|
|
952
|
+
url,
|
|
953
|
+
headers={
|
|
954
|
+
'User-Agent': 'riddle-proof-visual-delta/1.0',
|
|
955
|
+
'Accept': 'image/png,image/jpeg,image/webp,image/*;q=0.8,*/*;q=0.1',
|
|
956
|
+
},
|
|
957
|
+
)
|
|
958
|
+
with urlopen(request, timeout=timeout) as response:
|
|
959
|
+
content_length = response.headers.get('Content-Length')
|
|
960
|
+
if content_length:
|
|
961
|
+
try:
|
|
962
|
+
if int(content_length) > max_bytes:
|
|
963
|
+
raise ValueError('image artifact exceeds max fetch size')
|
|
964
|
+
except ValueError:
|
|
965
|
+
raise
|
|
966
|
+
except Exception:
|
|
967
|
+
pass
|
|
968
|
+
data = response.read(max_bytes + 1)
|
|
969
|
+
if len(data) > max_bytes:
|
|
970
|
+
raise ValueError('image artifact exceeds max fetch size')
|
|
971
|
+
return data
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _png_paeth(a, b, c):
|
|
975
|
+
p = a + b - c
|
|
976
|
+
pa = abs(p - a)
|
|
977
|
+
pb = abs(p - b)
|
|
978
|
+
pc = abs(p - c)
|
|
979
|
+
if pa <= pb and pa <= pc:
|
|
980
|
+
return a
|
|
981
|
+
if pb <= pc:
|
|
982
|
+
return b
|
|
983
|
+
return c
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def decode_png_rgba(data):
|
|
987
|
+
if not isinstance(data, (bytes, bytearray)) or not data.startswith(b'\x89PNG\r\n\x1a\n'):
|
|
988
|
+
raise ValueError('unsupported image format; expected PNG artifact')
|
|
989
|
+
offset = 8
|
|
990
|
+
width = height = bit_depth = color_type = interlace = None
|
|
991
|
+
idat = []
|
|
992
|
+
while offset + 8 <= len(data):
|
|
993
|
+
length = struct.unpack('>I', data[offset:offset + 4])[0]
|
|
994
|
+
chunk_type = data[offset + 4:offset + 8]
|
|
995
|
+
chunk_data = data[offset + 8:offset + 8 + length]
|
|
996
|
+
offset += 12 + length
|
|
997
|
+
if chunk_type == b'IHDR':
|
|
998
|
+
width, height, bit_depth, color_type, _compression, _filter, interlace = struct.unpack('>IIBBBBB', chunk_data)
|
|
999
|
+
elif chunk_type == b'IDAT':
|
|
1000
|
+
idat.append(chunk_data)
|
|
1001
|
+
elif chunk_type == b'IEND':
|
|
1002
|
+
break
|
|
1003
|
+
if not width or not height or bit_depth != 8 or interlace != 0:
|
|
1004
|
+
raise ValueError('unsupported PNG artifact; requires 8-bit non-interlaced PNG')
|
|
1005
|
+
channels_by_type = {0: 1, 2: 3, 4: 2, 6: 4}
|
|
1006
|
+
channels = channels_by_type.get(color_type)
|
|
1007
|
+
if not channels:
|
|
1008
|
+
raise ValueError('unsupported PNG color type')
|
|
1009
|
+
raw = zlib.decompress(b''.join(idat))
|
|
1010
|
+
stride = width * channels
|
|
1011
|
+
rows = []
|
|
1012
|
+
pos = 0
|
|
1013
|
+
previous = bytearray(stride)
|
|
1014
|
+
for _row in range(height):
|
|
1015
|
+
if pos >= len(raw):
|
|
1016
|
+
raise ValueError('truncated PNG data')
|
|
1017
|
+
filter_type = raw[pos]
|
|
1018
|
+
pos += 1
|
|
1019
|
+
scanline = bytearray(raw[pos:pos + stride])
|
|
1020
|
+
pos += stride
|
|
1021
|
+
for index in range(stride):
|
|
1022
|
+
left = scanline[index - channels] if index >= channels else 0
|
|
1023
|
+
up = previous[index]
|
|
1024
|
+
up_left = previous[index - channels] if index >= channels else 0
|
|
1025
|
+
if filter_type == 1:
|
|
1026
|
+
scanline[index] = (scanline[index] + left) & 0xff
|
|
1027
|
+
elif filter_type == 2:
|
|
1028
|
+
scanline[index] = (scanline[index] + up) & 0xff
|
|
1029
|
+
elif filter_type == 3:
|
|
1030
|
+
scanline[index] = (scanline[index] + ((left + up) // 2)) & 0xff
|
|
1031
|
+
elif filter_type == 4:
|
|
1032
|
+
scanline[index] = (scanline[index] + _png_paeth(left, up, up_left)) & 0xff
|
|
1033
|
+
elif filter_type != 0:
|
|
1034
|
+
raise ValueError('unsupported PNG filter')
|
|
1035
|
+
rows.append(bytes(scanline))
|
|
1036
|
+
previous = scanline
|
|
1037
|
+
rgba = bytearray(width * height * 4)
|
|
1038
|
+
out = 0
|
|
1039
|
+
for row in rows:
|
|
1040
|
+
for index in range(0, len(row), channels):
|
|
1041
|
+
if color_type == 0:
|
|
1042
|
+
gray = row[index]
|
|
1043
|
+
rgba[out:out + 4] = bytes((gray, gray, gray, 255))
|
|
1044
|
+
elif color_type == 2:
|
|
1045
|
+
rgba[out:out + 4] = bytes((row[index], row[index + 1], row[index + 2], 255))
|
|
1046
|
+
elif color_type == 4:
|
|
1047
|
+
gray = row[index]
|
|
1048
|
+
rgba[out:out + 4] = bytes((gray, gray, gray, row[index + 1]))
|
|
1049
|
+
else:
|
|
1050
|
+
rgba[out:out + 4] = bytes((row[index], row[index + 1], row[index + 2], row[index + 3]))
|
|
1051
|
+
out += 4
|
|
1052
|
+
return {'width': width, 'height': height, 'rgba': bytes(rgba)}
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def compare_rgba_images(before_image, after_image, threshold=10):
|
|
1056
|
+
before_width = int(before_image.get('width') or 0)
|
|
1057
|
+
before_height = int(before_image.get('height') or 0)
|
|
1058
|
+
after_width = int(after_image.get('width') or 0)
|
|
1059
|
+
after_height = int(after_image.get('height') or 0)
|
|
1060
|
+
before_rgba = before_image.get('rgba') or b''
|
|
1061
|
+
after_rgba = after_image.get('rgba') or b''
|
|
1062
|
+
overlap_width = min(before_width, after_width)
|
|
1063
|
+
overlap_height = min(before_height, after_height)
|
|
1064
|
+
total_pixels = max(before_width * before_height, after_width * after_height)
|
|
1065
|
+
changed_pixels = total_pixels - (overlap_width * overlap_height)
|
|
1066
|
+
for y in range(overlap_height):
|
|
1067
|
+
before_row = y * before_width * 4
|
|
1068
|
+
after_row = y * after_width * 4
|
|
1069
|
+
for x in range(overlap_width):
|
|
1070
|
+
b = before_row + x * 4
|
|
1071
|
+
a = after_row + x * 4
|
|
1072
|
+
if max(
|
|
1073
|
+
abs(before_rgba[b] - after_rgba[a]),
|
|
1074
|
+
abs(before_rgba[b + 1] - after_rgba[a + 1]),
|
|
1075
|
+
abs(before_rgba[b + 2] - after_rgba[a + 2]),
|
|
1076
|
+
abs(before_rgba[b + 3] - after_rgba[a + 3]),
|
|
1077
|
+
) > threshold:
|
|
1078
|
+
changed_pixels += 1
|
|
1079
|
+
change_percent = (changed_pixels / total_pixels) * 100 if total_pixels else None
|
|
1080
|
+
return {
|
|
1081
|
+
'changed_pixels': changed_pixels,
|
|
1082
|
+
'total_pixels': total_pixels,
|
|
1083
|
+
'change_percent': change_percent,
|
|
1084
|
+
'before_width': before_width,
|
|
1085
|
+
'before_height': before_height,
|
|
1086
|
+
'after_width': after_width,
|
|
1087
|
+
'after_height': after_height,
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def measure_visual_delta_from_image_artifacts(before_url, after_url):
|
|
1092
|
+
if not (image_artifact_url(before_url) and image_artifact_url(after_url)):
|
|
1093
|
+
return {
|
|
1094
|
+
'status': 'skipped',
|
|
1095
|
+
'reason': 'before/after URLs are not direct HTTPS image artifacts',
|
|
1096
|
+
}
|
|
1097
|
+
try:
|
|
1098
|
+
before_image = decode_png_rgba(fetch_url_bytes(before_url))
|
|
1099
|
+
after_image = decode_png_rgba(fetch_url_bytes(after_url))
|
|
1100
|
+
comparison = compare_rgba_images(before_image, after_image)
|
|
1101
|
+
except Exception as exc:
|
|
1102
|
+
return {
|
|
1103
|
+
'status': 'error',
|
|
1104
|
+
'error': type(exc).__name__ + ': ' + str(exc)[:300],
|
|
1105
|
+
}
|
|
1106
|
+
changed_pixels = comparison.get('changed_pixels')
|
|
1107
|
+
total_pixels = comparison.get('total_pixels')
|
|
1108
|
+
percent = comparison.get('change_percent')
|
|
1109
|
+
percent_pass = percent is not None and percent >= MIN_VISUAL_DELTA_PERCENT
|
|
1110
|
+
pixel_pass = changed_pixels is not None and changed_pixels >= MIN_VISUAL_CHANGED_PIXELS
|
|
1111
|
+
passed = bool(percent_pass or pixel_pass)
|
|
1112
|
+
return {
|
|
1113
|
+
'status': 'measured',
|
|
1114
|
+
'passed': passed,
|
|
1115
|
+
'change_percent': round(percent, 4) if percent is not None else None,
|
|
1116
|
+
'changed_pixels': int(changed_pixels) if changed_pixels is not None else None,
|
|
1117
|
+
'total_pixels': int(total_pixels) if total_pixels is not None else None,
|
|
1118
|
+
'min_change_percent': MIN_VISUAL_DELTA_PERCENT,
|
|
1119
|
+
'min_changed_pixels': MIN_VISUAL_CHANGED_PIXELS,
|
|
1120
|
+
'source': 'riddle_artifact_image_diff',
|
|
1121
|
+
'reason': (
|
|
1122
|
+
'Measured visual delta from before/after screenshot artifacts clears the legibility threshold.'
|
|
1123
|
+
if passed else
|
|
1124
|
+
'Measured visual delta from before/after screenshot artifacts is below the legibility threshold.'
|
|
1125
|
+
),
|
|
1126
|
+
'comparison': {
|
|
1127
|
+
'before_url': before_url,
|
|
1128
|
+
'after_url': after_url,
|
|
1129
|
+
'before_width': comparison.get('before_width'),
|
|
1130
|
+
'before_height': comparison.get('before_height'),
|
|
1131
|
+
'after_width': comparison.get('after_width'),
|
|
1132
|
+
'after_height': comparison.get('after_height'),
|
|
1133
|
+
},
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
|
|
938
1137
|
def add_visual_delta_diagnostic(visual_delta, key, value):
|
|
939
1138
|
updated = dict(visual_delta or {})
|
|
940
1139
|
diagnostic = dict(updated.get('diagnostic') or {})
|
|
@@ -970,6 +1169,21 @@ def measure_visual_delta_against_baseline(state, results, after_payload, current
|
|
|
970
1169
|
},
|
|
971
1170
|
)
|
|
972
1171
|
|
|
1172
|
+
artifact_delta = measure_visual_delta_from_image_artifacts(before_url, after_url)
|
|
1173
|
+
append_capture_diagnostic(state, 'visual_delta', 'riddle_artifact_image_diff', {
|
|
1174
|
+
'baseline_label': baseline.get('label') or '',
|
|
1175
|
+
'before_url': before_url,
|
|
1176
|
+
'after_url': after_url,
|
|
1177
|
+
}, artifact_delta)
|
|
1178
|
+
if isinstance(artifact_delta, dict) and artifact_delta.get('status') == 'measured':
|
|
1179
|
+
artifact_delta['comparison']['baseline_label'] = baseline.get('label') or ''
|
|
1180
|
+
return artifact_delta
|
|
1181
|
+
current_visual_delta = add_visual_delta_diagnostic(
|
|
1182
|
+
current_visual_delta,
|
|
1183
|
+
'artifact_image_diff',
|
|
1184
|
+
artifact_delta,
|
|
1185
|
+
)
|
|
1186
|
+
|
|
973
1187
|
args = {
|
|
974
1188
|
'url_before': before_url,
|
|
975
1189
|
'url_after': after_url,
|
|
@@ -1497,6 +1711,39 @@ def build_capture_retry_decision(after_observation, required_baseline_present, p
|
|
|
1497
1711
|
}
|
|
1498
1712
|
|
|
1499
1713
|
|
|
1714
|
+
def build_visual_delta_recovery_decision(verification_mode, visual_delta, visual_delta_blocker=''):
|
|
1715
|
+
if not visual_delta_applies(verification_mode):
|
|
1716
|
+
return None
|
|
1717
|
+
if visual_delta_passes_ship_gate(visual_delta):
|
|
1718
|
+
return None
|
|
1719
|
+
if not isinstance(visual_delta, dict):
|
|
1720
|
+
status = 'missing'
|
|
1721
|
+
else:
|
|
1722
|
+
status = str(visual_delta.get('status') or 'missing')
|
|
1723
|
+
if status == 'measured':
|
|
1724
|
+
return None
|
|
1725
|
+
reason = visual_delta_blocker or visual_delta_blocker_for_mode(verification_mode, visual_delta)
|
|
1726
|
+
diagnostic = visual_delta.get('diagnostic') if isinstance(visual_delta, dict) else {}
|
|
1727
|
+
reasons = [
|
|
1728
|
+
reason or 'Required visual_delta evidence is incomplete.',
|
|
1729
|
+
'Stay in verify so the same run can retry capture/comparison recovery before proof review.',
|
|
1730
|
+
]
|
|
1731
|
+
fallback = diagnostic.get('visual_diff_fallback') if isinstance(diagnostic, dict) else None
|
|
1732
|
+
artifact_diff = diagnostic.get('artifact_image_diff') if isinstance(diagnostic, dict) else None
|
|
1733
|
+
if isinstance(artifact_diff, dict) and artifact_diff.get('error'):
|
|
1734
|
+
reasons.append('Artifact image diff failed: ' + str(artifact_diff.get('error'))[:300])
|
|
1735
|
+
if isinstance(fallback, dict) and fallback.get('error'):
|
|
1736
|
+
reasons.append('Riddle visual_diff fallback failed: ' + str(fallback.get('error'))[:300])
|
|
1737
|
+
return {
|
|
1738
|
+
'decision': 'revise_capture',
|
|
1739
|
+
'summary': reason or 'Verify needs measured visual_delta evidence before proof review.',
|
|
1740
|
+
'recommended_stage': 'verify',
|
|
1741
|
+
'continue_with_stage': 'verify',
|
|
1742
|
+
'reasons': reasons,
|
|
1743
|
+
'visual_delta_status': status,
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
|
|
1500
1747
|
def compact_semantic_list(value, limit):
|
|
1501
1748
|
if not isinstance(value, list):
|
|
1502
1749
|
return []
|
|
@@ -2006,7 +2253,20 @@ if proof_evidence_required_for_mode(s.get('verification_mode')):
|
|
|
2006
2253
|
if proof_evidence_blocker:
|
|
2007
2254
|
summary_lines.append('Structured proof evidence gate: ' + proof_evidence_blocker)
|
|
2008
2255
|
|
|
2009
|
-
|
|
2256
|
+
visual_delta_recovery = build_visual_delta_recovery_decision(
|
|
2257
|
+
s.get('verification_mode'),
|
|
2258
|
+
visual_delta,
|
|
2259
|
+
visual_delta_blocker,
|
|
2260
|
+
)
|
|
2261
|
+
if visual_delta_recovery:
|
|
2262
|
+
summary_lines.append('Visual delta recovery: ' + visual_delta_recovery['summary'])
|
|
2263
|
+
|
|
2264
|
+
has_good_evidence = (
|
|
2265
|
+
required_baseline_present
|
|
2266
|
+
and after_observation.get('valid')
|
|
2267
|
+
and not proof_evidence_blocker
|
|
2268
|
+
and not visual_delta_recovery
|
|
2269
|
+
)
|
|
2010
2270
|
|
|
2011
2271
|
if has_good_evidence:
|
|
2012
2272
|
s['capture_hint_saved'] = record_successful_capture_hint(
|
|
@@ -2044,7 +2304,7 @@ if has_good_evidence:
|
|
|
2044
2304
|
summary_lines.append('Proof assessment: awaiting supervising agent judgment')
|
|
2045
2305
|
summary_lines.append('Proof next stage: supervising agent decides after reviewing the evidence packet')
|
|
2046
2306
|
else:
|
|
2047
|
-
capture_retry = build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker)
|
|
2307
|
+
capture_retry = visual_delta_recovery or build_capture_retry_decision(after_observation, required_baseline_present, proof_evidence_blocker)
|
|
2048
2308
|
s['verify_status'] = 'capture_incomplete'
|
|
2049
2309
|
s['merge_recommendation'] = 'do-not-merge'
|
|
2050
2310
|
s['proof_assessment'] = {}
|
|
@@ -2061,8 +2321,9 @@ else:
|
|
|
2061
2321
|
'continue_with_stage': capture_retry.get('continue_with_stage') or 'author',
|
|
2062
2322
|
'fields_agent_may_update': ['capture_script', 'server_path', 'wait_for_selector', 'proof_plan'],
|
|
2063
2323
|
'instructions': [
|
|
2064
|
-
'The after-proof
|
|
2065
|
-
'Adjust capture_script, server_path, wait_for_selector, and/or proof_plan
|
|
2324
|
+
'The after-proof evidence packet is incomplete, so use the recommended stage before proof review.',
|
|
2325
|
+
'Adjust capture_script, server_path, wait_for_selector, and/or proof_plan only when the recommended stage is author.',
|
|
2326
|
+
'If recommended_stage=verify, rerun verify so capture/comparison recovery can continue without changing proof authorship.',
|
|
2066
2327
|
'If the baseline itself is wrong, return to recon instead of forcing verify to rediscover context.',
|
|
2067
2328
|
],
|
|
2068
2329
|
}
|
|
@@ -3,8 +3,10 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess as sp
|
|
6
|
+
import struct
|
|
6
7
|
import sys
|
|
7
8
|
import tempfile
|
|
9
|
+
import zlib
|
|
8
10
|
from contextlib import contextmanager
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
@@ -35,6 +37,28 @@ def state_console(payload):
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
def png_rgba(width, height, pixels):
|
|
41
|
+
raw_rows = []
|
|
42
|
+
for y in range(height):
|
|
43
|
+
row = bytearray([0])
|
|
44
|
+
for x in range(width):
|
|
45
|
+
row.extend(pixels[(y * width + x) * 4:(y * width + x + 1) * 4])
|
|
46
|
+
raw_rows.append(bytes(row))
|
|
47
|
+
def chunk(kind, data):
|
|
48
|
+
return (
|
|
49
|
+
struct.pack('>I', len(data))
|
|
50
|
+
+ kind
|
|
51
|
+
+ data
|
|
52
|
+
+ struct.pack('>I', zlib.crc32(kind + data) & 0xffffffff)
|
|
53
|
+
)
|
|
54
|
+
return (
|
|
55
|
+
b'\x89PNG\r\n\x1a\n'
|
|
56
|
+
+ chunk(b'IHDR', struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0))
|
|
57
|
+
+ chunk(b'IDAT', zlib.compress(b''.join(raw_rows)))
|
|
58
|
+
+ chunk(b'IEND', b'')
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
38
62
|
def load_module(name: str, path: Path):
|
|
39
63
|
sys.modules.pop(name, None)
|
|
40
64
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
@@ -204,12 +228,13 @@ class FakeRiddle:
|
|
|
204
228
|
],
|
|
205
229
|
}
|
|
206
230
|
if 'after-proof' in script:
|
|
207
|
-
|
|
231
|
+
after_url = 'https://cdn.example.com/after-artifact' if 'noVisualDelta' in script else 'https://cdn.example.com/after.png'
|
|
232
|
+
outputs = [{'name': 'after.png', 'url': after_url}]
|
|
208
233
|
if 'proof-session' in script:
|
|
209
234
|
outputs.append({'name': 'proof-session.json', 'url': 'https://cdn.example.com/proof-session.json'})
|
|
210
|
-
|
|
235
|
+
payload = {
|
|
211
236
|
'ok': True,
|
|
212
|
-
'screenshots': [{'url':
|
|
237
|
+
'screenshots': [{'url': after_url}],
|
|
213
238
|
'outputs': outputs,
|
|
214
239
|
'console': state_console({
|
|
215
240
|
'bodyTextLength': 180,
|
|
@@ -225,6 +250,13 @@ class FakeRiddle:
|
|
|
225
250
|
'largeVisibleElements': [{'tag': 'button', 'text': 'Buy Now'}],
|
|
226
251
|
}),
|
|
227
252
|
}
|
|
253
|
+
if 'noVisualDelta' not in script:
|
|
254
|
+
payload['visual_diff'] = {
|
|
255
|
+
'diffPercentage': 1.2,
|
|
256
|
+
'differentPixels': 12000,
|
|
257
|
+
'totalPixels': 972000,
|
|
258
|
+
}
|
|
259
|
+
return payload
|
|
228
260
|
if 'prod.example.com/pricing' in script:
|
|
229
261
|
return {
|
|
230
262
|
'ok': True,
|
|
@@ -585,6 +617,32 @@ def run_verify_quality_ignores_proof_telemetry_console_text():
|
|
|
585
617
|
assert visual_diff_delta['change_percent'] == 1.45
|
|
586
618
|
assert visual_diff_delta['changed_pixels'] == 14094
|
|
587
619
|
|
|
620
|
+
before_png = png_rgba(2, 1, bytes([
|
|
621
|
+
0, 0, 0, 255,
|
|
622
|
+
0, 0, 0, 255,
|
|
623
|
+
]))
|
|
624
|
+
after_png = png_rgba(2, 1, bytes([
|
|
625
|
+
0, 0, 0, 255,
|
|
626
|
+
255, 255, 255, 255,
|
|
627
|
+
]))
|
|
628
|
+
|
|
629
|
+
def fake_fetch_url_bytes(url, timeout=20, max_bytes=25 * 1024 * 1024):
|
|
630
|
+
if url.endswith('before.png'):
|
|
631
|
+
return before_png
|
|
632
|
+
if url.endswith('after.png'):
|
|
633
|
+
return after_png
|
|
634
|
+
raise AssertionError(f'unexpected image fetch: {url}')
|
|
635
|
+
|
|
636
|
+
namespace['fetch_url_bytes'] = fake_fetch_url_bytes
|
|
637
|
+
artifact_image_delta = namespace['measure_visual_delta_from_image_artifacts'](
|
|
638
|
+
'https://riddle-screenshots.example/before.png',
|
|
639
|
+
'https://riddle-screenshots.example/after.png',
|
|
640
|
+
)
|
|
641
|
+
assert artifact_image_delta['status'] == 'measured'
|
|
642
|
+
assert artifact_image_delta['source'] == 'riddle_artifact_image_diff'
|
|
643
|
+
assert artifact_image_delta['changed_pixels'] == 1
|
|
644
|
+
assert artifact_image_delta['change_percent'] == 50
|
|
645
|
+
|
|
588
646
|
fallback_calls = []
|
|
589
647
|
|
|
590
648
|
def fake_invoke_retry(tool, args, retries=3, timeout=180):
|
|
@@ -601,6 +659,7 @@ def run_verify_quality_ignores_proof_telemetry_console_text():
|
|
|
601
659
|
|
|
602
660
|
namespace['invoke_retry'] = fake_invoke_retry
|
|
603
661
|
namespace['append_capture_diagnostic'] = lambda *args, **kwargs: None
|
|
662
|
+
namespace['fetch_url_bytes'] = lambda *args, **kwargs: (_ for _ in ()).throw(ValueError('image fetch unavailable'))
|
|
604
663
|
fallback_delta = namespace['measure_visual_delta_against_baseline'](
|
|
605
664
|
{'verification_mode': 'visual', 'requested_reference': 'before', 'before_cdn': 'https://cdn.example.com/before.png'},
|
|
606
665
|
{'baseline': {'before': {'url': 'https://cdn.example.com/before.png'}}},
|
|
@@ -1433,8 +1492,9 @@ def run_verify_requests_supervisor_assessment():
|
|
|
1433
1492
|
assert after_verify['proof_assessment_source'] is None
|
|
1434
1493
|
assert after_verify['proof_assessment_request']['status'] == 'needs_supervising_agent_assessment'
|
|
1435
1494
|
visual_delta = after_verify['proof_assessment_request']['visual_delta']
|
|
1436
|
-
assert visual_delta['status'] == '
|
|
1437
|
-
assert visual_delta['passed'] is
|
|
1495
|
+
assert visual_delta['status'] == 'measured'
|
|
1496
|
+
assert visual_delta['passed'] is True
|
|
1497
|
+
assert visual_delta['changed_pixels'] == 12000
|
|
1438
1498
|
semantic_context = after_verify['proof_assessment_request']['semantic_context']
|
|
1439
1499
|
assert semantic_context['route']['expected_path'] == '/pricing'
|
|
1440
1500
|
assert semantic_context['route']['after_observed_path'] == '/pricing'
|
|
@@ -1450,14 +1510,15 @@ def run_verify_requests_supervisor_assessment():
|
|
|
1450
1510
|
assert artifact_production['image_output_count'] >= 1
|
|
1451
1511
|
assert artifact_production['proof_evidence_present'] is False
|
|
1452
1512
|
artifact_usage = after_verify['proof_assessment_request']['artifact_usage']
|
|
1453
|
-
assert artifact_usage['missing_required_signals'] == [
|
|
1513
|
+
assert artifact_usage['missing_required_signals'] == []
|
|
1454
1514
|
assert 'after-capture' in artifact_usage['supervisor_review_signals']
|
|
1455
1515
|
assert 'baseline_context' in artifact_usage['required_signals']
|
|
1456
1516
|
assert 'route_semantics' in artifact_usage['available_signals']
|
|
1457
|
-
assert 'visual_delta
|
|
1517
|
+
assert 'visual_delta' in artifact_usage['available_signals']
|
|
1518
|
+
assert after_verify['proof_assessment_request']['hard_blockers'] == []
|
|
1458
1519
|
assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_contract']['required']['screenshot'] is True
|
|
1459
1520
|
assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_production']['image_output_count'] >= 1
|
|
1460
|
-
assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_usage']['missing_required_signals'] == [
|
|
1521
|
+
assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_usage']['missing_required_signals'] == []
|
|
1461
1522
|
assert 'capture success is not proof' in '\n'.join(after_verify['proof_assessment_request']['instructions'])
|
|
1462
1523
|
assert after_verify['verify_decision_request']['continue_with_stage'] is None
|
|
1463
1524
|
assert after_verify['verify_results']['baseline']['before']['source'] == 'recon'
|
|
@@ -1501,6 +1562,55 @@ def run_verify_requests_supervisor_assessment():
|
|
|
1501
1562
|
shutil.rmtree(tempdir, ignore_errors=True)
|
|
1502
1563
|
|
|
1503
1564
|
|
|
1565
|
+
def run_verify_routes_unmeasured_visual_delta_to_recovery():
|
|
1566
|
+
tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-verify-visual-recovery-'))
|
|
1567
|
+
state_path = tempdir / 'state.json'
|
|
1568
|
+
try:
|
|
1569
|
+
state = base_state(tempdir, reference='before')
|
|
1570
|
+
state.update({
|
|
1571
|
+
'repo': 'example/repo',
|
|
1572
|
+
'recon_status': 'ready_for_proof_plan',
|
|
1573
|
+
'author_status': 'ready',
|
|
1574
|
+
'proof_plan_status': 'ready',
|
|
1575
|
+
'implementation_status': 'changes_detected',
|
|
1576
|
+
'before_cdn': 'https://cdn.example.com/before-artifact',
|
|
1577
|
+
'proof_plan': 'Use the recon-confirmed /pricing route and capture the CTA state once it stabilizes.',
|
|
1578
|
+
'capture_script': "noVisualDelta(); await page.waitForSelector('[data-testid=pricing-cta]'); await saveScreenshot('after-proof');",
|
|
1579
|
+
'wait_for_selector': '[data-testid=pricing-cta]',
|
|
1580
|
+
'recon_results': {
|
|
1581
|
+
'baselines': {
|
|
1582
|
+
'before': {'path': '/pricing', 'url': 'https://cdn.example.com/before-artifact'},
|
|
1583
|
+
},
|
|
1584
|
+
},
|
|
1585
|
+
})
|
|
1586
|
+
write_state(state_path, state)
|
|
1587
|
+
os.environ['RIDDLE_PROOF_STATE_FILE'] = str(state_path)
|
|
1588
|
+
|
|
1589
|
+
fake = FakeRiddle()
|
|
1590
|
+
load_util_with_fake(fake)
|
|
1591
|
+
load_module('verify_unmeasured_visual_delta_recovery', VERIFY_PATH)
|
|
1592
|
+
after_verify = json.loads(state_path.read_text())
|
|
1593
|
+
|
|
1594
|
+
assert after_verify['verify_status'] == 'capture_incomplete'
|
|
1595
|
+
assert after_verify['proof_assessment_request'] == {}
|
|
1596
|
+
decision_request = after_verify['verify_decision_request']
|
|
1597
|
+
assert decision_request['capture_quality']['decision'] == 'revise_capture'
|
|
1598
|
+
assert decision_request['recommended_stage'] == 'verify'
|
|
1599
|
+
assert decision_request['continue_with_stage'] == 'verify'
|
|
1600
|
+
visual_delta = after_verify['evidence_bundle']['after']['visual_delta']
|
|
1601
|
+
assert visual_delta['status'] == 'unmeasured'
|
|
1602
|
+
assert visual_delta['diagnostic']['visual_diff_fallback']['status'] == 'error'
|
|
1603
|
+
assert 'Visual delta recovery' in after_verify['verify_summary']
|
|
1604
|
+
|
|
1605
|
+
return {
|
|
1606
|
+
'ok': True,
|
|
1607
|
+
'verify_status': after_verify['verify_status'],
|
|
1608
|
+
'continue_with_stage': decision_request['continue_with_stage'],
|
|
1609
|
+
}
|
|
1610
|
+
finally:
|
|
1611
|
+
shutil.rmtree(tempdir, ignore_errors=True)
|
|
1612
|
+
|
|
1613
|
+
|
|
1504
1614
|
def run_verify_structured_evidence_without_screenshot():
|
|
1505
1615
|
tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-verify-structured-'))
|
|
1506
1616
|
state_path = tempdir / 'state.json'
|
|
@@ -2142,6 +2252,7 @@ if __name__ == '__main__':
|
|
|
2142
2252
|
'capture_hint_rejects_route_specific_mode_only_match': run_capture_hint_rejects_route_specific_mode_only_match(),
|
|
2143
2253
|
'author_applies_supervisor_packet': run_author_applies_supervisor_packet(),
|
|
2144
2254
|
'verify_requests_supervisor_assessment': run_verify_requests_supervisor_assessment(),
|
|
2255
|
+
'verify_routes_unmeasured_visual_delta_to_recovery': run_verify_routes_unmeasured_visual_delta_to_recovery(),
|
|
2145
2256
|
'verify_structured_evidence_without_screenshot': run_verify_structured_evidence_without_screenshot(),
|
|
2146
2257
|
'verify_audio_requires_proof_evidence': run_verify_audio_requires_proof_evidence(),
|
|
2147
2258
|
'verify_audio_rejects_failed_nested_proof_evidence': run_verify_audio_rejects_failed_nested_proof_evidence(),
|