@riddledc/riddle-proof 0.5.32 → 0.5.34
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/dist/{chunk-R4BPJNAM.js → chunk-4YCWZVBN.js} +101 -8
- package/dist/{chunk-LIDYNU7Q.js → chunk-7R6ZQE3X.js} +39 -2
- package/dist/{chunk-TMMKRKY5.js → chunk-J2MERROF.js} +1 -0
- package/dist/{chunk-F6VFKS7K.js → chunk-MQ2BHHLX.js} +2 -2
- package/dist/{chunk-7ZJAUEUN.js → chunk-OASB3CYU.js} +6 -1
- package/dist/chunk-ODORKNSO.js +121 -0
- package/dist/engine-harness.cjs +143 -9
- package/dist/engine-harness.js +4 -3
- package/dist/index.cjs +271 -11
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +24 -7
- package/dist/openclaw.cjs +5 -0
- package/dist/openclaw.js +2 -2
- package/dist/proof-run-core.cjs +104 -8
- package/dist/proof-run-core.d.cts +24 -1
- package/dist/proof-run-core.d.ts +24 -1
- package/dist/proof-run-core.js +7 -1
- package/dist/proof-run-engine.cjs +101 -8
- package/dist/proof-run-engine.d.cts +24 -0
- package/dist/proof-run-engine.d.ts +24 -0
- package/dist/proof-run-engine.js +4 -1
- package/dist/proof-session.cjs +151 -0
- package/dist/proof-session.d.cts +37 -0
- package/dist/proof-session.d.ts +37 -0
- package/dist/proof-session.js +18 -0
- package/dist/result.cjs +1 -0
- package/dist/result.js +1 -1
- package/dist/runner.cjs +6 -0
- package/dist/runner.js +3 -3
- package/dist/state.cjs +5 -0
- package/dist/state.js +2 -2
- package/dist/types.d.cts +67 -1
- package/dist/types.d.ts +67 -1
- package/package.json +7 -2
- package/runtime/lib/preflight.py +42 -2
- package/runtime/lib/ship.py +55 -1
- package/runtime/lib/util.py +237 -0
- package/runtime/lib/verify.py +93 -6
- package/runtime/tests/recon_verify_smoke.py +181 -3
package/runtime/lib/preflight.py
CHANGED
|
@@ -8,7 +8,16 @@ capture_script: optional at setup and recon; required before verify.
|
|
|
8
8
|
|
|
9
9
|
import json, os, re, time, uuid, sys
|
|
10
10
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
-
from util import
|
|
11
|
+
from util import (
|
|
12
|
+
STATE_FILE,
|
|
13
|
+
ARGS_FILE,
|
|
14
|
+
apply_proof_session_defaults,
|
|
15
|
+
invoke,
|
|
16
|
+
load_package_json,
|
|
17
|
+
load_proof_session_source,
|
|
18
|
+
save_state,
|
|
19
|
+
validate_proof_session_resume,
|
|
20
|
+
)
|
|
12
21
|
|
|
13
22
|
def truthy(value):
|
|
14
23
|
return str(value or '').strip().lower() in ('1', 'true', 'yes', 'y', 'on')
|
|
@@ -40,6 +49,19 @@ if not os.path.exists(args_file):
|
|
|
40
49
|
with open(args_file) as f:
|
|
41
50
|
s = json.load(f)
|
|
42
51
|
|
|
52
|
+
resume_session_source = (s.get('resume_session') or '').strip()
|
|
53
|
+
if resume_session_source:
|
|
54
|
+
parent_proof_session = load_proof_session_source(resume_session_source)
|
|
55
|
+
s['parent_proof_session'] = parent_proof_session
|
|
56
|
+
s['parent_proof_session_id'] = parent_proof_session.get('session_id')
|
|
57
|
+
s['proof_session_resume'] = {
|
|
58
|
+
'status': 'loaded',
|
|
59
|
+
'source': 'inline_json' if resume_session_source.startswith('{') else resume_session_source,
|
|
60
|
+
'parent_session_id': parent_proof_session.get('session_id'),
|
|
61
|
+
'parent_fingerprint': parent_proof_session.get('fingerprint'),
|
|
62
|
+
'applied_fields': apply_proof_session_defaults(s, parent_proof_session),
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
mode = (s.get('mode') or '').strip().lower()
|
|
44
66
|
reference = s.get('reference', 'both')
|
|
45
67
|
requested_reference = reference
|
|
@@ -107,6 +129,8 @@ if raw_assertions:
|
|
|
107
129
|
except Exception as e:
|
|
108
130
|
raise SystemExit('assertions_json is not valid JSON: ' + str(e))
|
|
109
131
|
s['parsed_assertions'] = parsed_assertions
|
|
132
|
+
s['viewport_matrix'] = parse_json_arg('viewport_matrix_json', (dict, list), None)
|
|
133
|
+
s['deterministic_setup'] = parse_json_arg('deterministic_setup_json', (dict, list), None)
|
|
110
134
|
|
|
111
135
|
# Generate branch if not provided. The riddle-proof/* namespace is reserved for
|
|
112
136
|
# temporary proof worktrees, so never let a user-supplied branch use it as the
|
|
@@ -173,6 +197,16 @@ s['stage'] = 'preflight'
|
|
|
173
197
|
s['status'] = 'ready' if not missing else 'needs_input'
|
|
174
198
|
s['missing'] = missing
|
|
175
199
|
|
|
200
|
+
if s.get('parent_proof_session') and not missing:
|
|
201
|
+
try:
|
|
202
|
+
s['proof_session_resume'] = {
|
|
203
|
+
**(s.get('proof_session_resume') or {}),
|
|
204
|
+
**validate_proof_session_resume(s, route=s.get('server_path') or ''),
|
|
205
|
+
}
|
|
206
|
+
except SystemExit:
|
|
207
|
+
save_state(s)
|
|
208
|
+
raise
|
|
209
|
+
|
|
176
210
|
# Auth context can be supplied directly for public-plugin use, while use_auth
|
|
177
211
|
# remains a private/configured Cognito helper for Riddle-owned environments.
|
|
178
212
|
explicit_local_storage = parse_json_arg('auth_localStorage_json', (dict,), {})
|
|
@@ -231,7 +265,7 @@ print('RIDDLE PROOF — PREFLIGHT (' + mode.upper() + ' / ' + reference.upper()
|
|
|
231
265
|
print('=' * 50)
|
|
232
266
|
display_keys = ['repo', 'branch', 'target_branch', 'ship_target_branch', 'base_branch', 'before_ref', 'change_request', 'commit_message',
|
|
233
267
|
'reference', 'verification_mode', 'success_criteria', 'prod_url', 'build_command',
|
|
234
|
-
'allow_static_preview_fallback']
|
|
268
|
+
'allow_static_preview_fallback', 'resume_session', 'target_image_url', 'target_image_hash']
|
|
235
269
|
if mode == 'server':
|
|
236
270
|
display_keys += ['server_image', 'server_command', 'server_port', 'server_path']
|
|
237
271
|
if s.get('auth_localStorage'):
|
|
@@ -247,6 +281,12 @@ for k in display_keys:
|
|
|
247
281
|
print(k + ': ' + v)
|
|
248
282
|
if parsed_assertions is not None:
|
|
249
283
|
print('assertions_json: parsed')
|
|
284
|
+
if s.get('viewport_matrix') is not None:
|
|
285
|
+
print('viewport_matrix_json: parsed')
|
|
286
|
+
if s.get('deterministic_setup') is not None:
|
|
287
|
+
print('deterministic_setup_json: parsed')
|
|
288
|
+
if s.get('proof_session_resume'):
|
|
289
|
+
print('proof_session_resume: ' + str((s.get('proof_session_resume') or {}).get('status') or 'loaded'))
|
|
250
290
|
if not (s.get('capture_script') or '').strip():
|
|
251
291
|
print('NOTE: capture_script can be added later after recon and before verify.')
|
|
252
292
|
if reference_note:
|
package/runtime/lib/ship.py
CHANGED
|
@@ -9,6 +9,10 @@ from util import load_state, save_state, invoke, git
|
|
|
9
9
|
|
|
10
10
|
DISCORD_API = 'https://discord.com/api/v10'
|
|
11
11
|
SHIP_NOISE_PATHS = ('.codex', '.oc-smoke')
|
|
12
|
+
VISUAL_FIRST_MODES = {
|
|
13
|
+
'visual', 'render', 'interaction', 'ui', 'layout', 'screenshot',
|
|
14
|
+
'canvas', 'animation',
|
|
15
|
+
}
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
def read_json_file(path):
|
|
@@ -293,7 +297,11 @@ def record_ship_report(state, marked_ready=None):
|
|
|
293
297
|
def proof_assessment_is_ready(state):
|
|
294
298
|
assessment = state.get('proof_assessment') or {}
|
|
295
299
|
source = str(assessment.get('source') or state.get('proof_assessment_source') or '').strip().lower()
|
|
296
|
-
return
|
|
300
|
+
return (
|
|
301
|
+
source in ('supervising_agent', 'supervisor')
|
|
302
|
+
and assessment.get('decision') == 'ready_to_ship'
|
|
303
|
+
and not visual_delta_ship_blocker(state)
|
|
304
|
+
)
|
|
297
305
|
|
|
298
306
|
|
|
299
307
|
def effective_merge_recommendation(state):
|
|
@@ -310,6 +318,49 @@ def after_evidence_bundle(state):
|
|
|
310
318
|
return after if isinstance(after, dict) else {}
|
|
311
319
|
|
|
312
320
|
|
|
321
|
+
def normalized_verification_mode(state):
|
|
322
|
+
bundle = state.get('evidence_bundle') or {}
|
|
323
|
+
if isinstance(bundle, dict) and str(bundle.get('verification_mode') or '').strip():
|
|
324
|
+
return str(bundle.get('verification_mode')).strip().lower()
|
|
325
|
+
return str(state.get('verification_mode') or 'proof').strip().lower() or 'proof'
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def visual_delta_required_for_ship(state):
|
|
329
|
+
bundle = state.get('evidence_bundle') or {}
|
|
330
|
+
contract = bundle.get('artifact_contract') if isinstance(bundle, dict) else {}
|
|
331
|
+
required = contract.get('required') if isinstance(contract, dict) else {}
|
|
332
|
+
if isinstance(required, dict) and required.get('visual_delta') is True:
|
|
333
|
+
return True
|
|
334
|
+
return normalized_verification_mode(state) in VISUAL_FIRST_MODES
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def visual_delta_for_state(state):
|
|
338
|
+
after = after_evidence_bundle(state)
|
|
339
|
+
visual_delta = after.get('visual_delta') if isinstance(after, dict) else None
|
|
340
|
+
if isinstance(visual_delta, dict):
|
|
341
|
+
return visual_delta
|
|
342
|
+
request = state.get('proof_assessment_request') or {}
|
|
343
|
+
visual_delta = request.get('visual_delta') if isinstance(request, dict) else None
|
|
344
|
+
return visual_delta if isinstance(visual_delta, dict) else {}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def visual_delta_ship_blocker(state):
|
|
348
|
+
if not visual_delta_required_for_ship(state):
|
|
349
|
+
return ''
|
|
350
|
+
visual_delta = visual_delta_for_state(state)
|
|
351
|
+
if visual_delta.get('status') == 'measured' and visual_delta.get('passed') is True:
|
|
352
|
+
return ''
|
|
353
|
+
status = str(visual_delta.get('status') or 'missing')
|
|
354
|
+
if status == 'unmeasured':
|
|
355
|
+
return 'visual_delta.status=unmeasured blocks ready_to_ship for visual/UI proof'
|
|
356
|
+
if status == 'measured' and visual_delta.get('passed') is False:
|
|
357
|
+
return 'visual_delta.status=measured but visual_delta.passed=false blocks ready_to_ship for visual/UI proof'
|
|
358
|
+
reason = str(visual_delta.get('reason') or '').strip()
|
|
359
|
+
if reason:
|
|
360
|
+
return f'visual_delta.status={status} blocks ready_to_ship for visual/UI proof: {reason}'
|
|
361
|
+
return f'visual_delta.status={status} blocks ready_to_ship for visual/UI proof'
|
|
362
|
+
|
|
363
|
+
|
|
313
364
|
def state_has_after_evidence(state):
|
|
314
365
|
if (state.get('after_cdn') or '').strip():
|
|
315
366
|
return True
|
|
@@ -581,6 +632,9 @@ if reference in ('prod', 'both'):
|
|
|
581
632
|
raise SystemExit('prod_url is required when reference=' + reference + ' before ship.')
|
|
582
633
|
if not prod_cdn:
|
|
583
634
|
raise SystemExit('prod_cdn is required before ship. Run recon/verify again and preserve the approved prod baseline.')
|
|
635
|
+
visual_delta_blocker = visual_delta_ship_blocker(s)
|
|
636
|
+
if visual_delta_blocker:
|
|
637
|
+
raise SystemExit(visual_delta_blocker + '. Rerun verify with measured before/after visual delta or return a non-shipping proof assessment.')
|
|
584
638
|
if proof_source not in ('supervising_agent', 'supervisor') or proof_assessment.get('decision') != 'ready_to_ship':
|
|
585
639
|
raise SystemExit('Supervising-agent proof_assessment.decision=ready_to_ship is required before ship.')
|
|
586
640
|
|
package/runtime/lib/util.py
CHANGED
|
@@ -20,6 +20,8 @@ CAPTURE_DIAGNOSTIC_VERSION = 'riddle-proof.capture-diagnostic.v1'
|
|
|
20
20
|
DEBUG_STRING_LIMIT = 2000
|
|
21
21
|
CAPTURE_HINT_CACHE_VERSION = 'riddle-proof.capture-hints.v1'
|
|
22
22
|
CAPTURE_HINT_CACHE_LIMIT = 12
|
|
23
|
+
PROOF_SESSION_VERSION = 'riddle-proof.visual-session.v1'
|
|
24
|
+
PROOF_SESSION_FINGERPRINT_VERSION = 'riddle-proof.visual-session.fingerprint.v1'
|
|
23
25
|
SENSITIVE_KEY_FRAGMENTS = (
|
|
24
26
|
'authorization',
|
|
25
27
|
'apikey',
|
|
@@ -306,6 +308,241 @@ def compact_debug_value(value, limit=DEBUG_STRING_LIMIT):
|
|
|
306
308
|
return value
|
|
307
309
|
|
|
308
310
|
|
|
311
|
+
def stable_json(value):
|
|
312
|
+
return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def sha256_text(value):
|
|
316
|
+
text = str(value or '').strip()
|
|
317
|
+
if not text:
|
|
318
|
+
return ''
|
|
319
|
+
return hashlib.sha256(text.encode('utf-8')).hexdigest()
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def parse_optional_json(value, field_name):
|
|
323
|
+
if value in (None, ''):
|
|
324
|
+
return None
|
|
325
|
+
if isinstance(value, (dict, list)):
|
|
326
|
+
return value
|
|
327
|
+
if not isinstance(value, str):
|
|
328
|
+
raise SystemExit(field_name + ' must be JSON.')
|
|
329
|
+
text = value.strip()
|
|
330
|
+
if not text:
|
|
331
|
+
return None
|
|
332
|
+
try:
|
|
333
|
+
return json.loads(text)
|
|
334
|
+
except Exception as exc:
|
|
335
|
+
raise SystemExit(field_name + ' is not valid JSON: ' + str(exc))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def load_proof_session_source(value):
|
|
339
|
+
source = str(value or '').strip()
|
|
340
|
+
if not source:
|
|
341
|
+
return None
|
|
342
|
+
try:
|
|
343
|
+
if source.startswith('{'):
|
|
344
|
+
session = json.loads(source)
|
|
345
|
+
elif source.startswith(('http://', 'https://')):
|
|
346
|
+
with urlopen(source, timeout=20) as response:
|
|
347
|
+
session = json.loads(response.read(512 * 1024).decode('utf-8'))
|
|
348
|
+
else:
|
|
349
|
+
with open(os.path.abspath(os.path.expanduser(source))) as f:
|
|
350
|
+
session = json.load(f)
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
raise SystemExit('resume_session could not be loaded: ' + str(exc))
|
|
353
|
+
|
|
354
|
+
if not isinstance(session, dict):
|
|
355
|
+
raise SystemExit('resume_session must be a JSON object.')
|
|
356
|
+
if session.get('version') != PROOF_SESSION_VERSION:
|
|
357
|
+
raise SystemExit('resume_session has unsupported version: ' + str(session.get('version') or ''))
|
|
358
|
+
if not session.get('session_id'):
|
|
359
|
+
raise SystemExit('resume_session is missing session_id.')
|
|
360
|
+
if not session.get('fingerprint'):
|
|
361
|
+
raise SystemExit('resume_session is missing fingerprint.')
|
|
362
|
+
return session
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def apply_proof_session_defaults(state, session):
|
|
366
|
+
if not isinstance(session, dict):
|
|
367
|
+
return []
|
|
368
|
+
applied = []
|
|
369
|
+
route = session.get('route') if isinstance(session.get('route'), dict) else {}
|
|
370
|
+
capture = session.get('capture') if isinstance(session.get('capture'), dict) else {}
|
|
371
|
+
target_image = session.get('target_image') if isinstance(session.get('target_image'), dict) else {}
|
|
372
|
+
|
|
373
|
+
defaults = [
|
|
374
|
+
('server_path', route.get('path'), 'proof_session'),
|
|
375
|
+
('wait_for_selector', capture.get('wait_for_selector'), 'proof_session'),
|
|
376
|
+
('proof_plan', capture.get('proof_plan'), 'proof_session'),
|
|
377
|
+
('capture_script', capture.get('capture_script'), 'proof_session'),
|
|
378
|
+
('reference', session.get('reference'), 'proof_session'),
|
|
379
|
+
('verification_mode', session.get('verification_mode'), 'proof_session'),
|
|
380
|
+
('target_image_url', target_image.get('url'), 'proof_session'),
|
|
381
|
+
('target_image_hash', target_image.get('hash'), 'proof_session'),
|
|
382
|
+
]
|
|
383
|
+
for key, value, source in defaults:
|
|
384
|
+
text = str(value or '').strip()
|
|
385
|
+
if text and not str(state.get(key) or '').strip():
|
|
386
|
+
state[key] = text
|
|
387
|
+
applied.append(key)
|
|
388
|
+
if key == 'server_path':
|
|
389
|
+
state['server_path_source'] = source
|
|
390
|
+
|
|
391
|
+
if state.get('viewport_matrix_json') in (None, '') and session.get('viewport_matrix') is not None:
|
|
392
|
+
state['viewport_matrix_json'] = json.dumps(session.get('viewport_matrix'))
|
|
393
|
+
applied.append('viewport_matrix_json')
|
|
394
|
+
if state.get('deterministic_setup_json') in (None, '') and session.get('deterministic_setup') is not None:
|
|
395
|
+
state['deterministic_setup_json'] = json.dumps(session.get('deterministic_setup'))
|
|
396
|
+
applied.append('deterministic_setup_json')
|
|
397
|
+
if state.get('assertions_json') in (None, '') and session.get('assertions') is not None:
|
|
398
|
+
state['assertions_json'] = json.dumps(session.get('assertions'))
|
|
399
|
+
applied.append('assertions_json')
|
|
400
|
+
return applied
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def visual_session_fingerprint_basis(state, route=''):
|
|
404
|
+
route_value = str(route or state.get('server_path') or '').strip()
|
|
405
|
+
basis = {
|
|
406
|
+
'version': PROOF_SESSION_FINGERPRINT_VERSION,
|
|
407
|
+
'repo': str(state.get('repo') or '').strip() or None,
|
|
408
|
+
'route': route_value or None,
|
|
409
|
+
'wait_for_selector': str(state.get('wait_for_selector') or '').strip() or None,
|
|
410
|
+
'reference': str(state.get('requested_reference') or state.get('reference') or '').strip() or None,
|
|
411
|
+
'verification_mode': str(state.get('verification_mode') or '').strip().lower() or None,
|
|
412
|
+
'target_image_url': str(state.get('target_image_url') or '').strip() or None,
|
|
413
|
+
'target_image_hash': str(state.get('target_image_hash') or '').strip() or None,
|
|
414
|
+
'viewport_matrix': state.get('viewport_matrix'),
|
|
415
|
+
'deterministic_setup': state.get('deterministic_setup'),
|
|
416
|
+
'assertions': state.get('parsed_assertions'),
|
|
417
|
+
'capture_script_hash': sha256_text(state.get('capture_script')),
|
|
418
|
+
}
|
|
419
|
+
return {key: value for key, value in basis.items() if value is not None}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def visual_session_fingerprint_from_basis(basis):
|
|
423
|
+
return hashlib.sha256(stable_json(basis).encode('utf-8')).hexdigest()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def visual_session_fingerprint(state, route=''):
|
|
427
|
+
return visual_session_fingerprint_from_basis(visual_session_fingerprint_basis(state, route=route))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def proof_session_mismatches(state, session, route=''):
|
|
431
|
+
if not isinstance(session, dict):
|
|
432
|
+
return []
|
|
433
|
+
expected = session.get('fingerprint_basis') if isinstance(session.get('fingerprint_basis'), dict) else {}
|
|
434
|
+
actual = visual_session_fingerprint_basis(state, route=route)
|
|
435
|
+
keys = sorted(set(expected.keys()) | set(actual.keys()))
|
|
436
|
+
mismatches = []
|
|
437
|
+
for key in keys:
|
|
438
|
+
if stable_json(expected.get(key)) != stable_json(actual.get(key)):
|
|
439
|
+
mismatches.append({
|
|
440
|
+
'key': key,
|
|
441
|
+
'expected': expected.get(key),
|
|
442
|
+
'actual': actual.get(key),
|
|
443
|
+
})
|
|
444
|
+
return mismatches
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def validate_proof_session_resume(state, route=''):
|
|
448
|
+
session = state.get('parent_proof_session')
|
|
449
|
+
if not isinstance(session, dict):
|
|
450
|
+
return {'status': 'not_requested'}
|
|
451
|
+
mismatches = proof_session_mismatches(state, session, route=route)
|
|
452
|
+
if mismatches:
|
|
453
|
+
state['proof_session_resume'] = {
|
|
454
|
+
'status': 'fingerprint_mismatch',
|
|
455
|
+
'parent_session_id': session.get('session_id'),
|
|
456
|
+
'parent_fingerprint': session.get('fingerprint'),
|
|
457
|
+
'mismatches': mismatches,
|
|
458
|
+
}
|
|
459
|
+
raise SystemExit(
|
|
460
|
+
'resume_session fingerprint mismatch: ' +
|
|
461
|
+
', '.join(item['key'] for item in mismatches[:8])
|
|
462
|
+
)
|
|
463
|
+
state['proof_session_resume'] = {
|
|
464
|
+
'status': 'accepted',
|
|
465
|
+
'parent_session_id': session.get('session_id'),
|
|
466
|
+
'parent_fingerprint': session.get('fingerprint'),
|
|
467
|
+
'fingerprint': visual_session_fingerprint(state, route=route),
|
|
468
|
+
}
|
|
469
|
+
return state['proof_session_resume']
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def capture_proof_session_seed(state, route=''):
|
|
473
|
+
parent = state.get('parent_proof_session') if isinstance(state.get('parent_proof_session'), dict) else {}
|
|
474
|
+
basis = visual_session_fingerprint_basis(state, route=route)
|
|
475
|
+
return {
|
|
476
|
+
'version': PROOF_SESSION_VERSION,
|
|
477
|
+
'session_kind': 'capture-seed',
|
|
478
|
+
'run_id': str(state.get('run_id') or '').strip(),
|
|
479
|
+
'parent_session_id': parent.get('session_id') or None,
|
|
480
|
+
'parent_fingerprint': parent.get('fingerprint') or None,
|
|
481
|
+
'fingerprint': visual_session_fingerprint_from_basis(basis),
|
|
482
|
+
'fingerprint_basis': basis,
|
|
483
|
+
'repo': str(state.get('repo') or '').strip(),
|
|
484
|
+
'route': {'path': str(route or state.get('server_path') or '').strip()},
|
|
485
|
+
'reference': str(state.get('requested_reference') or state.get('reference') or '').strip(),
|
|
486
|
+
'verification_mode': str(state.get('verification_mode') or '').strip(),
|
|
487
|
+
'target_image': {
|
|
488
|
+
'url': str(state.get('target_image_url') or '').strip(),
|
|
489
|
+
'hash': str(state.get('target_image_hash') or '').strip(),
|
|
490
|
+
},
|
|
491
|
+
'viewport_matrix': state.get('viewport_matrix'),
|
|
492
|
+
'deterministic_setup': state.get('deterministic_setup'),
|
|
493
|
+
'capture': {
|
|
494
|
+
'proof_plan': str(state.get('proof_plan') or '').strip(),
|
|
495
|
+
'capture_script': str(state.get('capture_script') or '').strip(),
|
|
496
|
+
'wait_for_selector': str(state.get('wait_for_selector') or '').strip(),
|
|
497
|
+
},
|
|
498
|
+
'assertions': state.get('parsed_assertions'),
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def proof_session_output_url(payload):
|
|
503
|
+
item = capture_output_item(payload, 'proof-session.json') or capture_output_item(payload, 'proof-session')
|
|
504
|
+
return (item or {}).get('url', '') if item else ''
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def build_visual_proof_session(state, route='', observed_after_path='', artifacts=None, evidence=None, status=''):
|
|
508
|
+
parent = state.get('parent_proof_session') if isinstance(state.get('parent_proof_session'), dict) else {}
|
|
509
|
+
basis = visual_session_fingerprint_basis(state, route=route)
|
|
510
|
+
session_id = 'rps_' + time.strftime('%Y%m%dT%H%M%SZ', time.gmtime()) + '_' + hashlib.sha1(os.urandom(16)).hexdigest()[:8]
|
|
511
|
+
return {
|
|
512
|
+
'version': PROOF_SESSION_VERSION,
|
|
513
|
+
'session_id': session_id,
|
|
514
|
+
'run_id': str(state.get('run_id') or '').strip(),
|
|
515
|
+
'parent_session_id': parent.get('session_id') or None,
|
|
516
|
+
'parent_fingerprint': parent.get('fingerprint') or None,
|
|
517
|
+
'created_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
518
|
+
'fingerprint': visual_session_fingerprint_from_basis(basis),
|
|
519
|
+
'fingerprint_basis': basis,
|
|
520
|
+
'repo': str(state.get('repo') or '').strip(),
|
|
521
|
+
'branch': str(state.get('branch') or state.get('target_branch') or '').strip(),
|
|
522
|
+
'route': {
|
|
523
|
+
'path': str(route or state.get('server_path') or '').strip(),
|
|
524
|
+
'observed_after_path': str(observed_after_path or '').strip(),
|
|
525
|
+
},
|
|
526
|
+
'reference': str(state.get('requested_reference') or state.get('reference') or '').strip(),
|
|
527
|
+
'verification_mode': str(state.get('verification_mode') or '').strip(),
|
|
528
|
+
'target_image': {
|
|
529
|
+
'url': str(state.get('target_image_url') or '').strip(),
|
|
530
|
+
'hash': str(state.get('target_image_hash') or '').strip(),
|
|
531
|
+
},
|
|
532
|
+
'viewport_matrix': state.get('viewport_matrix'),
|
|
533
|
+
'deterministic_setup': state.get('deterministic_setup'),
|
|
534
|
+
'capture': {
|
|
535
|
+
'proof_plan': str(state.get('proof_plan') or '').strip(),
|
|
536
|
+
'capture_script': str(state.get('capture_script') or '').strip(),
|
|
537
|
+
'wait_for_selector': str(state.get('wait_for_selector') or '').strip(),
|
|
538
|
+
},
|
|
539
|
+
'assertions': state.get('parsed_assertions'),
|
|
540
|
+
'artifacts': artifacts or {},
|
|
541
|
+
'evidence': evidence or {},
|
|
542
|
+
'status': status,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
309
546
|
def redact_for_diagnostics(value):
|
|
310
547
|
if isinstance(value, dict):
|
|
311
548
|
redacted = {}
|
package/runtime/lib/verify.py
CHANGED
|
@@ -13,6 +13,8 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
13
13
|
from util import (
|
|
14
14
|
append_capture_diagnostic,
|
|
15
15
|
apply_auth_context,
|
|
16
|
+
build_visual_proof_session,
|
|
17
|
+
capture_proof_session_seed,
|
|
16
18
|
capture_static_preview,
|
|
17
19
|
enrich_capture_payload,
|
|
18
20
|
has_auth_context,
|
|
@@ -24,6 +26,7 @@ from util import (
|
|
|
24
26
|
run_project_build,
|
|
25
27
|
save_state,
|
|
26
28
|
should_use_static_preview,
|
|
29
|
+
proof_session_output_url,
|
|
27
30
|
summarize_capture_artifacts,
|
|
28
31
|
)
|
|
29
32
|
import subprocess as sp
|
|
@@ -215,7 +218,7 @@ def abort_capture_failure(state, results, expected_path, message, raw_payload):
|
|
|
215
218
|
raise SystemExit(summary)
|
|
216
219
|
|
|
217
220
|
|
|
218
|
-
def build_probe_capture_script(base_script='', verification_mode='proof'):
|
|
221
|
+
def build_probe_capture_script(base_script='', verification_mode='proof', proof_session_seed=None):
|
|
219
222
|
pieces = []
|
|
220
223
|
script = (base_script or '').strip()
|
|
221
224
|
pieces.append('let __riddleProofCaptureScriptError = null;')
|
|
@@ -280,6 +283,12 @@ def build_probe_capture_script(base_script='', verification_mode='proof'):
|
|
|
280
283
|
])
|
|
281
284
|
if auto_screenshot_for_mode(verification_mode) and not capture_script_saves_screenshot(script):
|
|
282
285
|
pieces.append("await saveScreenshot('after-proof');")
|
|
286
|
+
if isinstance(proof_session_seed, dict):
|
|
287
|
+
pieces.append(
|
|
288
|
+
'try { if (typeof saveJson === "function") await saveJson("proof-session", ' +
|
|
289
|
+
json.dumps(proof_session_seed) +
|
|
290
|
+
'); } catch {}'
|
|
291
|
+
)
|
|
283
292
|
pieces.append('if (__riddleProofCaptureScriptError) throw __riddleProofCaptureScriptError;')
|
|
284
293
|
pieces.append('return { pageState, proofEvidence: __riddleProofEvidenceValue };')
|
|
285
294
|
return ' '.join(pieces)
|
|
@@ -559,6 +568,34 @@ def visual_delta_applies(verification_mode):
|
|
|
559
568
|
return screenshot_required_for_mode(verification_mode)
|
|
560
569
|
|
|
561
570
|
|
|
571
|
+
def visual_delta_passes_ship_gate(visual_delta):
|
|
572
|
+
return (
|
|
573
|
+
isinstance(visual_delta, dict)
|
|
574
|
+
and visual_delta.get('status') == 'measured'
|
|
575
|
+
and visual_delta.get('passed') is True
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def visual_delta_blocker_for_mode(verification_mode, visual_delta):
|
|
580
|
+
if not visual_delta_applies(verification_mode):
|
|
581
|
+
return ''
|
|
582
|
+
if visual_delta_passes_ship_gate(visual_delta):
|
|
583
|
+
return ''
|
|
584
|
+
if not isinstance(visual_delta, dict):
|
|
585
|
+
status = 'missing'
|
|
586
|
+
reason = 'No visual_delta object was found in proof evidence.'
|
|
587
|
+
else:
|
|
588
|
+
status = str(visual_delta.get('status') or 'missing')
|
|
589
|
+
reason = str(visual_delta.get('reason') or '').strip()
|
|
590
|
+
if status == 'unmeasured':
|
|
591
|
+
return 'visual_delta.status=unmeasured blocks ready_to_ship for visual/UI proof; capture a measured before/after visual delta or choose needs_richer_proof.'
|
|
592
|
+
if status == 'measured' and isinstance(visual_delta, dict) and visual_delta.get('passed') is False:
|
|
593
|
+
return 'visual_delta.status=measured but visual_delta.passed=false blocks ready_to_ship for visual/UI proof; the measured change did not clear the threshold.'
|
|
594
|
+
if reason:
|
|
595
|
+
return f'visual_delta.status={status} blocks ready_to_ship for visual/UI proof: {reason}'
|
|
596
|
+
return f'visual_delta.status={status} blocks ready_to_ship for visual/UI proof.'
|
|
597
|
+
|
|
598
|
+
|
|
562
599
|
def list_value(value):
|
|
563
600
|
return value if isinstance(value, list) else []
|
|
564
601
|
|
|
@@ -684,11 +721,11 @@ def artifact_contract_for_mode(verification_mode):
|
|
|
684
721
|
'route_semantics': True,
|
|
685
722
|
'screenshot': screenshot_required_for_mode(mode),
|
|
686
723
|
'proof_evidence': proof_evidence_required_for_mode(mode),
|
|
724
|
+
'visual_delta': visual_delta_applies(mode),
|
|
687
725
|
},
|
|
688
726
|
'preferred': {
|
|
689
727
|
'page_state': True,
|
|
690
728
|
'structured_payload': mode in STRUCTURED_FIRST_MODES or proof_evidence_required_for_mode(mode),
|
|
691
|
-
'visual_delta': visual_delta_applies(mode),
|
|
692
729
|
},
|
|
693
730
|
'optional': {
|
|
694
731
|
'console_summary': True,
|
|
@@ -731,7 +768,7 @@ def artifact_signal_availability(state, after_observation, supporting, visual_de
|
|
|
731
768
|
),
|
|
732
769
|
'structured_payload': bool(supporting.get('has_structured_payload')),
|
|
733
770
|
'proof_evidence': bool(supporting.get('proof_evidence_present')),
|
|
734
|
-
'visual_delta':
|
|
771
|
+
'visual_delta': visual_delta_passes_ship_gate(visual_delta),
|
|
735
772
|
'console_summary': bool(supporting.get('console_entries')),
|
|
736
773
|
'json_artifacts': bool(supporting.get('data_outputs')),
|
|
737
774
|
'image_outputs': bool(supporting.get('image_outputs')),
|
|
@@ -1028,6 +1065,36 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1028
1065
|
semantic_context,
|
|
1029
1066
|
[],
|
|
1030
1067
|
)
|
|
1068
|
+
observed_after_path = ((semantic_context.get('route') or {}).get('after_observed_path')) or ''
|
|
1069
|
+
proof_session_artifact_url = proof_session_output_url(after_payload)
|
|
1070
|
+
outputs = []
|
|
1071
|
+
for item in after_payload.get('outputs') or []:
|
|
1072
|
+
if not isinstance(item, dict):
|
|
1073
|
+
continue
|
|
1074
|
+
outputs.append({
|
|
1075
|
+
'name': str(item.get('name') or ''),
|
|
1076
|
+
'url': str(item.get('url') or ''),
|
|
1077
|
+
'type': str(item.get('type') or item.get('kind') or ''),
|
|
1078
|
+
})
|
|
1079
|
+
proof_session = build_visual_proof_session(
|
|
1080
|
+
state,
|
|
1081
|
+
route=expected_path,
|
|
1082
|
+
observed_after_path=observed_after_path,
|
|
1083
|
+
artifacts={
|
|
1084
|
+
'before': state.get('before_cdn') or '',
|
|
1085
|
+
'prod': state.get('prod_cdn') or '',
|
|
1086
|
+
'after': state.get('after_cdn') or '',
|
|
1087
|
+
'session': proof_session_artifact_url,
|
|
1088
|
+
'outputs': outputs,
|
|
1089
|
+
},
|
|
1090
|
+
evidence={
|
|
1091
|
+
'visual_delta': visual_delta,
|
|
1092
|
+
'semantic_context': semantic_context,
|
|
1093
|
+
'artifact_contract': artifact_contract,
|
|
1094
|
+
'artifact_usage': artifact_usage,
|
|
1095
|
+
},
|
|
1096
|
+
status='evidence_captured' if after_observation.get('valid') else 'capture_incomplete',
|
|
1097
|
+
)
|
|
1031
1098
|
return {
|
|
1032
1099
|
'verification_mode': normalized_verification_mode(state.get('verification_mode')),
|
|
1033
1100
|
'reference': state.get('requested_reference') or state.get('reference', 'both'),
|
|
@@ -1050,6 +1117,7 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1050
1117
|
'proof_evidence_sample': compact_value(proof_evidence) if proof_evidence is not None else '',
|
|
1051
1118
|
'success_criteria': (state.get('success_criteria') or '').strip(),
|
|
1052
1119
|
'assertions': state.get('parsed_assertions') or None,
|
|
1120
|
+
'proof_session': proof_session,
|
|
1053
1121
|
}
|
|
1054
1122
|
|
|
1055
1123
|
|
|
@@ -1097,6 +1165,8 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
|
|
|
1097
1165
|
evidence_bundle['artifact_contract'] = artifact_contract
|
|
1098
1166
|
evidence_bundle['artifact_production'] = artifact_production
|
|
1099
1167
|
evidence_bundle['artifact_usage'] = artifact_usage
|
|
1168
|
+
visual_delta_blocker = visual_delta_blocker_for_mode(verification_mode, visual_delta)
|
|
1169
|
+
hard_blockers = [visual_delta_blocker] if visual_delta_blocker else []
|
|
1100
1170
|
|
|
1101
1171
|
return {
|
|
1102
1172
|
'status': 'needs_supervising_agent_assessment',
|
|
@@ -1112,14 +1182,15 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
|
|
|
1112
1182
|
'artifact_contract': artifact_contract,
|
|
1113
1183
|
'artifact_production': artifact_production,
|
|
1114
1184
|
'artifact_usage': artifact_usage,
|
|
1185
|
+
'hard_blockers': hard_blockers,
|
|
1115
1186
|
'instructions': [
|
|
1116
1187
|
'The supervising agent owns proof assessment. Inspect the recon baseline(s), after evidence, and any structured artifacts together.',
|
|
1117
1188
|
'Decide whether the evidence is ready_to_ship or should continue internally through author, implement, or recon.',
|
|
1189
|
+
'Hard blockers cannot be overridden by supervisor judgment; if hard_blockers is non-empty, do not choose ready_to_ship.',
|
|
1118
1190
|
'Do not mark ready_to_ship if the before/prod baseline is blank, shell-only, generic, or not visibly tied to the requested feature.',
|
|
1119
1191
|
'Use semantic_context.route plus headings/buttons/text anchors to ground route and content judgment before treating a screenshot as wrong-route.',
|
|
1120
1192
|
'For visual/UI modes, use screenshots plus after_observation.details.visible_text_sample, headings, buttons, links, canvas_count, and large_visible_elements to explain what the proof actually shows.',
|
|
1121
|
-
'For visual/UI polish, capture success is not proof. If visual_delta.status
|
|
1122
|
-
'If visual_delta.status=unmeasured for visual/UI proof, only choose ready_to_ship when the screenshots and page-state details let you name a clearly legible before/after change; otherwise request richer proof or another implementation pass.',
|
|
1193
|
+
'For visual/UI polish, capture success is not proof. If visual_delta.status is unmeasured, missing, not_applicable, or measured with passed=false, choose needs_implementation or needs_richer_proof instead of ready_to_ship.',
|
|
1123
1194
|
'For data/audio/log/metrics/custom modes, judge the structured evidence bundle and proof_evidence_sample directly; screenshots are optional supporting context.',
|
|
1124
1195
|
'The summary must name the concrete change, the target route/UI, what changed in after evidence, and why the stop condition is satisfied.',
|
|
1125
1196
|
'Only set escalation_target=human when you conclude the workflow has hit a real wall or is not converging.',
|
|
@@ -1162,7 +1233,8 @@ expected_path = (
|
|
|
1162
1233
|
or '/'
|
|
1163
1234
|
)
|
|
1164
1235
|
verification_mode = normalized_verification_mode(s.get('verification_mode'))
|
|
1165
|
-
|
|
1236
|
+
proof_session_seed = capture_proof_session_seed(s, expected_path)
|
|
1237
|
+
probe_capture_script = build_probe_capture_script(capture_script, verification_mode, proof_session_seed)
|
|
1166
1238
|
results = {
|
|
1167
1239
|
'baseline': {
|
|
1168
1240
|
'reference': reference,
|
|
@@ -1371,9 +1443,24 @@ if reference in ('prod', 'both') and prod_url:
|
|
|
1371
1443
|
|
|
1372
1444
|
evidence_bundle = build_evidence_bundle(s, results, after_payload, after_observation, required_baseline_present, expected_path)
|
|
1373
1445
|
s['evidence_bundle'] = evidence_bundle
|
|
1446
|
+
s['proof_session'] = evidence_bundle.get('proof_session') or {}
|
|
1447
|
+
s['proof_session_fingerprint'] = (s.get('proof_session') or {}).get('fingerprint') or ''
|
|
1448
|
+
s['proof_session_artifact_url'] = ((s.get('proof_session') or {}).get('artifacts') or {}).get('session') or ''
|
|
1449
|
+
if s.get('proof_session'):
|
|
1450
|
+
summary_lines.append(
|
|
1451
|
+
'Proof session: ' +
|
|
1452
|
+
str((s.get('proof_session') or {}).get('session_id') or '') +
|
|
1453
|
+
' ' +
|
|
1454
|
+
str((s.get('proof_session') or {}).get('fingerprint') or '')[:12]
|
|
1455
|
+
)
|
|
1456
|
+
if s.get('proof_session_artifact_url'):
|
|
1457
|
+
summary_lines.append('Proof session artifact: ' + s['proof_session_artifact_url'])
|
|
1374
1458
|
visual_delta = ((evidence_bundle.get('after') or {}).get('visual_delta') or {})
|
|
1375
1459
|
if visual_delta.get('status') != 'not_applicable':
|
|
1376
1460
|
summary_lines.append('Visual delta gate: ' + compact_value(visual_delta, limit=700))
|
|
1461
|
+
visual_delta_blocker = visual_delta_blocker_for_mode(s.get('verification_mode'), visual_delta)
|
|
1462
|
+
if visual_delta_blocker:
|
|
1463
|
+
summary_lines.append('Visual delta hard gate: ' + visual_delta_blocker)
|
|
1377
1464
|
|
|
1378
1465
|
proof_evidence_blocker = ''
|
|
1379
1466
|
if proof_evidence_required_for_mode(s.get('verification_mode')):
|