@riddledc/riddle-proof 0.5.31 → 0.5.33

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.
@@ -14,7 +14,7 @@ import {
14
14
  validateShipGate,
15
15
  workflowFile,
16
16
  writeState
17
- } from "./chunk-R4BPJNAM.js";
17
+ } from "./chunk-A5AWVY5A.js";
18
18
 
19
19
  // src/proof-run-engine.ts
20
20
  import { execFileSync } from "child_process";
@@ -770,6 +770,9 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
770
770
  if (reasons.some((reason) => reason.includes("proof_assessment"))) {
771
771
  return "resume with riddle_proof_review using decision=ready_to_ship only after the screenshots and semantic evidence visibly prove the request; otherwise choose needs_implementation or needs_richer_proof";
772
772
  }
773
+ if (reasons.some((reason) => reason.includes("visual_delta"))) {
774
+ return "rerun verify with a measured before/after visual delta, or choose needs_richer_proof instead of ready_to_ship for this visual proof";
775
+ }
773
776
  if (reasons.some((reason) => reason.includes("after_cdn") || reason.includes("verify_status"))) {
774
777
  return "rerun verify with stronger proof framing so after evidence is captured before shipping";
775
778
  }
@@ -862,7 +865,7 @@ async function executeWorkflow(params, pluginConfig, resolvedConfig) {
862
865
  }
863
866
  state = readState(config.statePath);
864
867
  const continuedStage = params.continue_from_checkpoint ? checkpointContinueStage(state) : null;
865
- if (params.continue_from_checkpoint && !continuedStage) {
868
+ if (params.continue_from_checkpoint && !params.advance_stage && !continuedStage) {
866
869
  const recommended = recommendedAdvanceStage(state);
867
870
  return checkpoint(
868
871
  state?.active_checkpoint_stage || recommended || "recon",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.5.31",
3
+ "version": "0.5.33",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -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 source in ('supervising_agent', 'supervisor') and assessment.get('decision') == 'ready_to_ship'
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
 
@@ -559,6 +559,34 @@ def visual_delta_applies(verification_mode):
559
559
  return screenshot_required_for_mode(verification_mode)
560
560
 
561
561
 
562
+ def visual_delta_passes_ship_gate(visual_delta):
563
+ return (
564
+ isinstance(visual_delta, dict)
565
+ and visual_delta.get('status') == 'measured'
566
+ and visual_delta.get('passed') is True
567
+ )
568
+
569
+
570
+ def visual_delta_blocker_for_mode(verification_mode, visual_delta):
571
+ if not visual_delta_applies(verification_mode):
572
+ return ''
573
+ if visual_delta_passes_ship_gate(visual_delta):
574
+ return ''
575
+ if not isinstance(visual_delta, dict):
576
+ status = 'missing'
577
+ reason = 'No visual_delta object was found in proof evidence.'
578
+ else:
579
+ status = str(visual_delta.get('status') or 'missing')
580
+ reason = str(visual_delta.get('reason') or '').strip()
581
+ if status == 'unmeasured':
582
+ 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.'
583
+ if status == 'measured' and isinstance(visual_delta, dict) and visual_delta.get('passed') is False:
584
+ 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.'
585
+ if reason:
586
+ return f'visual_delta.status={status} blocks ready_to_ship for visual/UI proof: {reason}'
587
+ return f'visual_delta.status={status} blocks ready_to_ship for visual/UI proof.'
588
+
589
+
562
590
  def list_value(value):
563
591
  return value if isinstance(value, list) else []
564
592
 
@@ -684,11 +712,11 @@ def artifact_contract_for_mode(verification_mode):
684
712
  'route_semantics': True,
685
713
  'screenshot': screenshot_required_for_mode(mode),
686
714
  'proof_evidence': proof_evidence_required_for_mode(mode),
715
+ 'visual_delta': visual_delta_applies(mode),
687
716
  },
688
717
  'preferred': {
689
718
  'page_state': True,
690
719
  'structured_payload': mode in STRUCTURED_FIRST_MODES or proof_evidence_required_for_mode(mode),
691
- 'visual_delta': visual_delta_applies(mode),
692
720
  },
693
721
  'optional': {
694
722
  'console_summary': True,
@@ -731,7 +759,7 @@ def artifact_signal_availability(state, after_observation, supporting, visual_de
731
759
  ),
732
760
  'structured_payload': bool(supporting.get('has_structured_payload')),
733
761
  'proof_evidence': bool(supporting.get('proof_evidence_present')),
734
- 'visual_delta': bool((visual_delta or {}).get('status') not in ('', None, 'not_applicable')),
762
+ 'visual_delta': visual_delta_passes_ship_gate(visual_delta),
735
763
  'console_summary': bool(supporting.get('console_entries')),
736
764
  'json_artifacts': bool(supporting.get('data_outputs')),
737
765
  'image_outputs': bool(supporting.get('image_outputs')),
@@ -1097,6 +1125,8 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
1097
1125
  evidence_bundle['artifact_contract'] = artifact_contract
1098
1126
  evidence_bundle['artifact_production'] = artifact_production
1099
1127
  evidence_bundle['artifact_usage'] = artifact_usage
1128
+ visual_delta_blocker = visual_delta_blocker_for_mode(verification_mode, visual_delta)
1129
+ hard_blockers = [visual_delta_blocker] if visual_delta_blocker else []
1100
1130
 
1101
1131
  return {
1102
1132
  'status': 'needs_supervising_agent_assessment',
@@ -1112,14 +1142,15 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
1112
1142
  'artifact_contract': artifact_contract,
1113
1143
  'artifact_production': artifact_production,
1114
1144
  'artifact_usage': artifact_usage,
1145
+ 'hard_blockers': hard_blockers,
1115
1146
  'instructions': [
1116
1147
  'The supervising agent owns proof assessment. Inspect the recon baseline(s), after evidence, and any structured artifacts together.',
1117
1148
  'Decide whether the evidence is ready_to_ship or should continue internally through author, implement, or recon.',
1149
+ 'Hard blockers cannot be overridden by supervisor judgment; if hard_blockers is non-empty, do not choose ready_to_ship.',
1118
1150
  '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
1151
  'Use semantic_context.route plus headings/buttons/text anchors to ground route and content judgment before treating a screenshot as wrong-route.',
1120
1152
  '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=measured and visual_delta.passed=false, choose needs_implementation or needs_richer_proof instead of ready_to_ship.',
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.',
1153
+ '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
1154
  'For data/audio/log/metrics/custom modes, judge the structured evidence bundle and proof_evidence_sample directly; screenshots are optional supporting context.',
1124
1155
  'The summary must name the concrete change, the target route/UI, what changed in after evidence, and why the stop condition is satisfied.',
1125
1156
  'Only set escalation_target=human when you conclude the workflow has hit a real wall or is not converging.',
@@ -1374,6 +1405,9 @@ s['evidence_bundle'] = evidence_bundle
1374
1405
  visual_delta = ((evidence_bundle.get('after') or {}).get('visual_delta') or {})
1375
1406
  if visual_delta.get('status') != 'not_applicable':
1376
1407
  summary_lines.append('Visual delta gate: ' + compact_value(visual_delta, limit=700))
1408
+ visual_delta_blocker = visual_delta_blocker_for_mode(s.get('verification_mode'), visual_delta)
1409
+ if visual_delta_blocker:
1410
+ summary_lines.append('Visual delta hard gate: ' + visual_delta_blocker)
1377
1411
 
1378
1412
  proof_evidence_blocker = ''
1379
1413
  if proof_evidence_required_for_mode(s.get('verification_mode')):
@@ -1200,17 +1200,19 @@ def run_verify_requests_supervisor_assessment():
1200
1200
  artifact_contract = after_verify['proof_assessment_request']['artifact_contract']
1201
1201
  assert artifact_contract['required']['baseline_context'] is True
1202
1202
  assert artifact_contract['required']['screenshot'] is True
1203
+ assert artifact_contract['required']['visual_delta'] is True
1203
1204
  artifact_production = after_verify['proof_assessment_request']['artifact_production']
1204
1205
  assert artifact_production['image_output_count'] >= 1
1205
1206
  assert artifact_production['proof_evidence_present'] is False
1206
1207
  artifact_usage = after_verify['proof_assessment_request']['artifact_usage']
1207
- assert artifact_usage['missing_required_signals'] == []
1208
+ assert artifact_usage['missing_required_signals'] == ['visual_delta']
1208
1209
  assert 'after-capture' in artifact_usage['supervisor_review_signals']
1209
1210
  assert 'baseline_context' in artifact_usage['required_signals']
1210
1211
  assert 'route_semantics' in artifact_usage['available_signals']
1212
+ assert 'visual_delta.status=unmeasured' in after_verify['proof_assessment_request']['hard_blockers'][0]
1211
1213
  assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_contract']['required']['screenshot'] is True
1212
1214
  assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_production']['image_output_count'] >= 1
1213
- assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_usage']['missing_required_signals'] == []
1215
+ assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_usage']['missing_required_signals'] == ['visual_delta']
1214
1216
  assert 'capture success is not proof' in '\n'.join(after_verify['proof_assessment_request']['instructions'])
1215
1217
  assert after_verify['verify_decision_request']['continue_with_stage'] is None
1216
1218
  assert after_verify['verify_results']['baseline']['before']['source'] == 'recon'
@@ -1573,6 +1575,62 @@ def run_ship_missing_supervisor_gate():
1573
1575
  shutil.rmtree(tempdir, ignore_errors=True)
1574
1576
 
1575
1577
 
1578
+ def run_ship_blocks_unmeasured_visual_delta():
1579
+ tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-ship-visual-delta-'))
1580
+ state_path = tempdir / 'state.json'
1581
+ try:
1582
+ state = base_state(tempdir, reference='before')
1583
+ state.update({
1584
+ 'recon_status': 'ready_for_proof_plan',
1585
+ 'author_status': 'ready',
1586
+ 'proof_plan_status': 'ready',
1587
+ 'implementation_status': 'changes_detected',
1588
+ 'verification_mode': 'visual',
1589
+ 'verify_status': 'evidence_captured',
1590
+ 'before_cdn': 'https://cdn.example.com/before.png',
1591
+ 'after_cdn': 'https://cdn.example.com/after.png',
1592
+ 'proof_assessment': {
1593
+ 'decision': 'ready_to_ship',
1594
+ 'summary': 'The screenshots look good.',
1595
+ 'source': 'supervising_agent',
1596
+ },
1597
+ 'proof_assessment_source': 'supervising_agent',
1598
+ 'evidence_bundle': {
1599
+ 'verification_mode': 'visual',
1600
+ 'artifact_contract': {
1601
+ 'required': {
1602
+ 'baseline_context': True,
1603
+ 'route_semantics': True,
1604
+ 'screenshot': True,
1605
+ 'visual_delta': True,
1606
+ },
1607
+ },
1608
+ 'after': {
1609
+ 'screenshot_url': 'https://cdn.example.com/after.png',
1610
+ 'observation': {'valid': True, 'reason': 'ok'},
1611
+ 'visual_delta': {
1612
+ 'status': 'unmeasured',
1613
+ 'passed': None,
1614
+ 'reason': 'No measured before/after visual delta was found in proof evidence.',
1615
+ },
1616
+ },
1617
+ },
1618
+ })
1619
+ write_state(state_path, state)
1620
+ os.environ['RIDDLE_PROOF_STATE_FILE'] = str(state_path)
1621
+
1622
+ try:
1623
+ load_module('ship_blocks_unmeasured_visual_delta', SHIP_PATH)
1624
+ except SystemExit as exc:
1625
+ message = str(exc)
1626
+ assert 'visual_delta.status=unmeasured' in message, message
1627
+ assert 'blocks ready_to_ship' in message, message
1628
+ return {'ok': True, 'error': message}
1629
+ raise AssertionError('ship should have failed when visual delta was unmeasured')
1630
+ finally:
1631
+ shutil.rmtree(tempdir, ignore_errors=True)
1632
+
1633
+
1576
1634
  def run_ship_accepts_structured_after_evidence():
1577
1635
  tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-ship-structured-'))
1578
1636
  state_path = tempdir / 'state.json'
@@ -1829,6 +1887,7 @@ if __name__ == '__main__':
1829
1887
  'verify_capture_retry': run_verify_capture_retry(),
1830
1888
  'missing_baseline_guard': run_verify_missing_baseline(),
1831
1889
  'ship_supervisor_gate': run_ship_missing_supervisor_gate(),
1890
+ 'ship_blocks_unmeasured_visual_delta': run_ship_blocks_unmeasured_visual_delta(),
1832
1891
  'ship_structured_after_evidence': run_ship_accepts_structured_after_evidence(),
1833
1892
  'ship_discord_thread_target': run_ship_discord_thread_target(),
1834
1893
  'ship_filters_tool_noise_when_staging': run_ship_filters_tool_noise_when_staging(),