@riddledc/riddle-proof 0.5.15 → 0.5.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.5.15",
3
+ "version": "0.5.17",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -566,6 +566,41 @@ def git(cmd, cwd):
566
566
  return sp.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
567
567
 
568
568
 
569
+ def run_project_build(project_dir, build_cmd, timeout=600, clean_cache_dir='.next'):
570
+ """Build once with existing cache, then retry clean if the cached build fails."""
571
+ attempts = []
572
+ for clean_first in (False, True):
573
+ if clean_first and clean_cache_dir:
574
+ cache_path = os.path.join(project_dir, clean_cache_dir)
575
+ if os.path.exists(cache_path):
576
+ sp.run(
577
+ 'rm -rf ' + shell_quote(clean_cache_dir),
578
+ shell=True,
579
+ cwd=project_dir,
580
+ capture_output=True,
581
+ text=True,
582
+ )
583
+ result = sp.run(build_cmd, shell=True, cwd=project_dir, capture_output=True, text=True, timeout=timeout)
584
+ attempts.append({
585
+ 'clean_first': clean_first,
586
+ 'returncode': int(result.returncode),
587
+ 'stderr': (result.stderr or '')[:500],
588
+ })
589
+ if result.returncode == 0:
590
+ return {
591
+ 'result': result,
592
+ 'clean_retry_used': clean_first,
593
+ 'attempts': attempts,
594
+ }
595
+ if not clean_cache_dir or clean_first:
596
+ break
597
+ return {
598
+ 'result': result,
599
+ 'clean_retry_used': False,
600
+ 'attempts': attempts,
601
+ }
602
+
603
+
569
604
  def load_package_json(project_dir):
570
605
  package_json = os.path.join(project_dir, 'package.json')
571
606
  if not os.path.exists(package_json):
@@ -20,6 +20,7 @@ from util import (
20
20
  load_state,
21
21
  prepare_server_preview,
22
22
  record_successful_capture_hint,
23
+ run_project_build,
23
24
  save_state,
24
25
  should_use_static_preview,
25
26
  summarize_capture_artifacts,
@@ -624,6 +625,111 @@ def collect_supporting_artifacts(payload):
624
625
  }
625
626
 
626
627
 
628
+ def artifact_contract_for_mode(verification_mode):
629
+ mode = normalized_verification_mode(verification_mode)
630
+ return {
631
+ 'verification_mode': mode,
632
+ 'required': {
633
+ 'baseline_context': True,
634
+ 'route_semantics': True,
635
+ 'screenshot': screenshot_required_for_mode(mode),
636
+ 'proof_evidence': proof_evidence_required_for_mode(mode),
637
+ },
638
+ 'preferred': {
639
+ 'page_state': True,
640
+ 'structured_payload': mode in STRUCTURED_FIRST_MODES or proof_evidence_required_for_mode(mode),
641
+ 'visual_delta': visual_delta_applies(mode),
642
+ },
643
+ 'optional': {
644
+ 'console_summary': True,
645
+ 'json_artifacts': True,
646
+ 'image_outputs': True,
647
+ },
648
+ }
649
+
650
+
651
+ def artifact_production_summary(payload, supporting):
652
+ artifact_summary = summarize_capture_artifacts(payload)
653
+ return {
654
+ 'output_names': [str(item.get('name') or '') for item in (artifact_summary.get('outputs') or [])[:20]],
655
+ 'screenshot_names': [str(item.get('name') or '') for item in (artifact_summary.get('screenshots') or [])[:10]],
656
+ 'artifact_json': list(artifact_summary.get('artifact_json') or []),
657
+ 'artifact_error_names': sorted((artifact_summary.get('artifact_errors') or {}).keys()),
658
+ 'image_output_count': len(supporting.get('image_outputs') or []),
659
+ 'data_output_count': len(supporting.get('data_outputs') or []),
660
+ 'other_output_count': len(supporting.get('other_outputs') or []),
661
+ 'console_entries': int(supporting.get('console_entries') or 0),
662
+ 'structured_result_keys': list(supporting.get('structured_result_keys') or []),
663
+ 'proof_evidence_present': bool(supporting.get('proof_evidence_present')),
664
+ 'has_structured_payload': bool(supporting.get('has_structured_payload')),
665
+ }
666
+
667
+
668
+ def artifact_signal_availability(state, after_observation, supporting, visual_delta, required_baseline_present, semantic_context):
669
+ details = (after_observation or {}).get('details') or {}
670
+ route = (semantic_context or {}).get('route') or {}
671
+ return {
672
+ 'baseline_context': bool(required_baseline_present),
673
+ 'route_semantics': bool(route.get('after_observed_path') or details.get('observed_path')),
674
+ 'screenshot': bool(details.get('has_screenshot')),
675
+ 'page_state': bool(
676
+ details.get('visible_text_sample')
677
+ or details.get('headings')
678
+ or details.get('buttons')
679
+ or details.get('links')
680
+ or details.get('semantic_anchor_count')
681
+ ),
682
+ 'structured_payload': bool(supporting.get('has_structured_payload')),
683
+ 'proof_evidence': bool(supporting.get('proof_evidence_present')),
684
+ 'visual_delta': bool((visual_delta or {}).get('status') not in ('', None, 'not_applicable')),
685
+ 'console_summary': bool(supporting.get('console_entries')),
686
+ 'json_artifacts': bool(supporting.get('data_outputs')),
687
+ 'image_outputs': bool(supporting.get('image_outputs')),
688
+ 'assertions': bool(state.get('parsed_assertions')),
689
+ 'success_criteria': bool((state.get('success_criteria') or '').strip()),
690
+ }
691
+
692
+
693
+ def artifact_usage_summary(state, after_observation, supporting, visual_delta, required_baseline_present, semantic_context, evidence_basis):
694
+ contract = artifact_contract_for_mode(state.get('verification_mode'))
695
+ available = artifact_signal_availability(
696
+ state,
697
+ after_observation,
698
+ supporting,
699
+ visual_delta,
700
+ required_baseline_present,
701
+ semantic_context,
702
+ )
703
+ capture_quality = []
704
+ details = (after_observation or {}).get('details') or {}
705
+ if details.get('has_screenshot'):
706
+ capture_quality.append('screenshot')
707
+ if available.get('page_state'):
708
+ capture_quality.append('page_state')
709
+ if available.get('console_summary'):
710
+ capture_quality.append('console_summary')
711
+ if available.get('structured_payload'):
712
+ capture_quality.append('structured_payload')
713
+ if available.get('proof_evidence'):
714
+ capture_quality.append('proof_evidence')
715
+ if available.get('visual_delta'):
716
+ capture_quality.append('visual_delta')
717
+
718
+ required_signals = [key for key, enabled in (contract.get('required') or {}).items() if enabled]
719
+ preferred_signals = [key for key, enabled in (contract.get('preferred') or {}).items() if enabled]
720
+ optional_signals = [key for key, enabled in (contract.get('optional') or {}).items() if enabled]
721
+
722
+ return {
723
+ 'required_signals': required_signals,
724
+ 'preferred_signals': preferred_signals,
725
+ 'optional_signals': optional_signals,
726
+ 'available_signals': [key for key, enabled in available.items() if enabled],
727
+ 'missing_required_signals': [key for key in required_signals if not available.get(key)],
728
+ 'capture_quality_signals': capture_quality,
729
+ 'supervisor_review_signals': list(evidence_basis or []),
730
+ }
731
+
732
+
627
733
  def evaluate_capture_quality(payload, expected_path, verification_mode='proof'):
628
734
  payload = enrich_capture_payload(payload)
629
735
  mode = normalized_verification_mode(verification_mode)
@@ -862,6 +968,17 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
862
968
  else {'status': 'not_applicable', 'passed': None, 'reason': 'Verification mode does not require visual delta gating.'}
863
969
  )
864
970
  semantic_context = build_semantic_context(state, results, after_observation, expected_path)
971
+ artifact_contract = artifact_contract_for_mode(state.get('verification_mode'))
972
+ artifact_production = artifact_production_summary(after_payload, supporting)
973
+ artifact_usage = artifact_usage_summary(
974
+ state,
975
+ after_observation,
976
+ supporting,
977
+ visual_delta,
978
+ required_baseline_present,
979
+ semantic_context,
980
+ [],
981
+ )
865
982
  return {
866
983
  'verification_mode': normalized_verification_mode(state.get('verification_mode')),
867
984
  'reference': state.get('requested_reference') or state.get('reference', 'both'),
@@ -869,6 +986,9 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
869
986
  'required_baseline_present': required_baseline_present,
870
987
  'baseline': results.get('baseline') or {},
871
988
  'semantic_context': semantic_context,
989
+ 'artifact_contract': artifact_contract,
990
+ 'artifact_production': artifact_production,
991
+ 'artifact_usage': artifact_usage,
872
992
  'after': {
873
993
  'screenshot_url': state.get('after_cdn') or '',
874
994
  'observation': after_observation,
@@ -909,6 +1029,26 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
909
1029
  if has_success_criteria:
910
1030
  evidence_basis.append('success-criteria')
911
1031
 
1032
+ artifact_contract = (evidence_bundle or {}).get('artifact_contract') if isinstance(evidence_bundle, dict) else None
1033
+ if not isinstance(artifact_contract, dict):
1034
+ artifact_contract = artifact_contract_for_mode(verification_mode)
1035
+ artifact_production = (evidence_bundle or {}).get('artifact_production') if isinstance(evidence_bundle, dict) else None
1036
+ if not isinstance(artifact_production, dict):
1037
+ artifact_production = artifact_production_summary(payload, supporting)
1038
+ artifact_usage = artifact_usage_summary(
1039
+ state,
1040
+ after_observation,
1041
+ supporting,
1042
+ visual_delta,
1043
+ required_baseline_present,
1044
+ semantic_context,
1045
+ evidence_basis,
1046
+ )
1047
+ if isinstance(evidence_bundle, dict):
1048
+ evidence_bundle['artifact_contract'] = artifact_contract
1049
+ evidence_bundle['artifact_production'] = artifact_production
1050
+ evidence_bundle['artifact_usage'] = artifact_usage
1051
+
912
1052
  return {
913
1053
  'status': 'needs_supervising_agent_assessment',
914
1054
  'verification_mode': verification_mode,
@@ -920,6 +1060,9 @@ def build_supervisor_assessment_request(state, payload, after_observation, requi
920
1060
  'semantic_context': semantic_context,
921
1061
  'evidence_bundle': evidence_bundle or {},
922
1062
  'evidence_basis': evidence_basis,
1063
+ 'artifact_contract': artifact_contract,
1064
+ 'artifact_production': artifact_production,
1065
+ 'artifact_usage': artifact_usage,
923
1066
  'instructions': [
924
1067
  'The supervising agent owns proof assessment. Inspect the recon baseline(s), after evidence, and any structured artifacts together.',
925
1068
  'Decide whether the evidence is ready_to_ship or should continue internally through author, implement, or recon.',
@@ -1028,14 +1171,17 @@ if existing_prod:
1028
1171
 
1029
1172
  # AFTER (always from after worktree)
1030
1173
  record_verify_phase('build', 'running', 'Building after worktree for verify capture.')
1031
- print('Cleaning after .next cache...')
1032
- sp.run('rm -rf .next', shell=True, cwd=after_dir, capture_output=True)
1033
-
1034
1174
  print('Building after worktree...')
1035
- br = sp.run(build_cmd, shell=True, cwd=after_dir, capture_output=True, text=True, timeout=600)
1175
+ build_attempt = run_project_build(after_dir, build_cmd, timeout=600, clean_cache_dir='.next')
1176
+ br = build_attempt.get('result')
1177
+ if build_attempt.get('clean_retry_used'):
1178
+ print('Verify build recovered after cleaning .next cache.')
1036
1179
  if br.returncode != 0:
1037
1180
  record_verify_phase('build', 'failed', 'After build failed: ' + br.stderr[:300])
1038
1181
  raise SystemExit('After build failed: ' + br.stderr[:500])
1182
+ if build_attempt.get('attempts'):
1183
+ s['verify_build_attempts'] = build_attempt['attempts']
1184
+ s['verify_build_clean_retry_used'] = bool(build_attempt.get('clean_retry_used'))
1039
1185
  record_verify_phase('build', 'completed', 'After worktree build completed.')
1040
1186
 
1041
1187
  after_payload = {}
@@ -508,6 +508,38 @@ def base_state(tempdir: Path, *, reference='before', prod_url=''):
508
508
  }
509
509
 
510
510
 
511
+ def run_project_build_retries_after_clean_failure():
512
+ tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-build-retry-'))
513
+ cache_dir = tempdir / '.next'
514
+ cache_dir.mkdir(parents=True, exist_ok=True)
515
+ util = load_module('util_build_retry', UTIL_PATH)
516
+ original_run = util.sp.run
517
+ calls = []
518
+
519
+ def fake_run(cmd, *args, **kwargs):
520
+ calls.append(cmd)
521
+ if cmd == 'npm run build':
522
+ build_attempts = calls.count('npm run build')
523
+ if build_attempts == 1:
524
+ return sp.CompletedProcess(cmd, 1, '', 'stale cache')
525
+ return sp.CompletedProcess(cmd, 0, 'ok', '')
526
+ if cmd == 'rm -rf .next':
527
+ shutil.rmtree(cache_dir, ignore_errors=True)
528
+ return sp.CompletedProcess(cmd, 0, '', '')
529
+ raise AssertionError(f'unexpected command: {cmd}')
530
+
531
+ try:
532
+ util.sp.run = fake_run
533
+ result = util.run_project_build(str(tempdir), 'npm run build', timeout=30, clean_cache_dir='.next')
534
+ assert result['clean_retry_used'] is True
535
+ assert result['result'].returncode == 0
536
+ assert calls == ['npm run build', 'rm -rf .next', 'npm run build']
537
+ assert not cache_dir.exists()
538
+ finally:
539
+ util.sp.run = original_run
540
+ shutil.rmtree(tempdir)
541
+
542
+
511
543
  def run_recon_then_author_request():
512
544
  tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-supervisor-request-'))
513
545
  state_path = tempdir / 'state.json'
@@ -758,6 +790,20 @@ def run_verify_requests_supervisor_assessment():
758
790
  assert semantic_context['after']['headings'] == ['Pricing'], semantic_context
759
791
  assert 'semantic-context' in after_verify['proof_assessment_request']['evidence_basis']
760
792
  assert after_verify['proof_assessment_request']['evidence_bundle']['semantic_context']['after']['buttons'] == ['Buy Now']
793
+ artifact_contract = after_verify['proof_assessment_request']['artifact_contract']
794
+ assert artifact_contract['required']['baseline_context'] is True
795
+ assert artifact_contract['required']['screenshot'] is True
796
+ artifact_production = after_verify['proof_assessment_request']['artifact_production']
797
+ assert artifact_production['image_output_count'] >= 1
798
+ assert artifact_production['proof_evidence_present'] is False
799
+ artifact_usage = after_verify['proof_assessment_request']['artifact_usage']
800
+ assert artifact_usage['missing_required_signals'] == []
801
+ assert 'after-capture' in artifact_usage['supervisor_review_signals']
802
+ assert 'baseline_context' in artifact_usage['required_signals']
803
+ assert 'route_semantics' in artifact_usage['available_signals']
804
+ assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_contract']['required']['screenshot'] is True
805
+ assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_production']['image_output_count'] >= 1
806
+ assert after_verify['proof_assessment_request']['evidence_bundle']['artifact_usage']['missing_required_signals'] == []
761
807
  assert 'capture success is not proof' in '\n'.join(after_verify['proof_assessment_request']['instructions'])
762
808
  assert after_verify['verify_decision_request']['continue_with_stage'] is None
763
809
  assert after_verify['verify_results']['baseline']['before']['source'] == 'recon'
@@ -1257,6 +1303,7 @@ if __name__ == '__main__':
1257
1303
  'capture_artifact_enrichment': run_capture_artifact_enrichment(),
1258
1304
  'capture_diagnostics_redaction': run_capture_diagnostics_redact_sensitive_values(),
1259
1305
  'apply_auth_context': run_apply_auth_context_passes_supported_auth_payloads(),
1306
+ 'run_project_build_retries_after_clean_failure': run_project_build_retries_after_clean_failure(),
1260
1307
  'verify_quality_ignores_proof_telemetry_console_text': run_verify_quality_ignores_proof_telemetry_console_text(),
1261
1308
  'recon_then_author_request': run_recon_then_author_request(),
1262
1309
  'recon_route_literal_preference': run_recon_prefers_route_literals_over_import_paths(),