@riddledc/riddle-proof 0.8.48 → 0.8.50

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.8.48",
3
+ "version": "0.8.50",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -15,6 +15,8 @@ VISUAL_FIRST_MODES = {
15
15
  'visual', 'render', 'ui', 'layout', 'screenshot',
16
16
  'canvas', 'animation',
17
17
  }
18
+ INTERACTION_MODES = {'interaction', 'interactive', 'user_flow', 'user-flow', 'workflow'}
19
+ REFERENCE_MODES = ('before', 'prod', 'both')
18
20
 
19
21
 
20
22
  def read_json_file(path):
@@ -682,6 +684,7 @@ def build_ship_report(state, marked_ready=None):
682
684
  prod_artifact_url = first_public_artifact_url(state, 'prod', 'image') or state.get('prod_cdn', '')
683
685
  after_artifact_url = first_public_artifact_url(state, 'after', 'image') or state.get('after_cdn', '')
684
686
  artifact_publication = state.get('proof_artifact_publication') or {}
687
+ ship_gate = ship_gate_report_facts(state)
685
688
  return {
686
689
  'pr_url': state.get('pr_url', ''),
687
690
  'pr_branch': branch,
@@ -699,6 +702,7 @@ def build_ship_report(state, marked_ready=None):
699
702
  'proof_artifacts_url': artifact_publication.get('html_url', '') if isinstance(artifact_publication, dict) else '',
700
703
  'proof_artifacts_manifest_url': artifact_publication.get('manifest_url', '') if isinstance(artifact_publication, dict) else '',
701
704
  'proof_artifact_publication': artifact_publication if isinstance(artifact_publication, dict) else {},
705
+ 'ship_gate': ship_gate,
702
706
  }
703
707
 
704
708
 
@@ -709,13 +713,7 @@ def record_ship_report(state, marked_ready=None):
709
713
 
710
714
 
711
715
  def proof_assessment_is_ready(state):
712
- assessment = state.get('proof_assessment') or {}
713
- source = str(assessment.get('source') or state.get('proof_assessment_source') or '').strip().lower()
714
- return (
715
- source in ('supervising_agent', 'supervisor')
716
- and assessment.get('decision') == 'ready_to_ship'
717
- and not visual_delta_ship_blocker(state)
718
- )
716
+ return ship_gate_report_facts(state).get('ok') is True
719
717
 
720
718
 
721
719
  def effective_merge_recommendation(state):
@@ -795,6 +793,175 @@ def state_has_after_evidence(state):
795
793
  )
796
794
 
797
795
 
796
+ def state_has_proof_evidence(state):
797
+ if state.get('proof_evidence_present') is True:
798
+ return True
799
+ proof_evidence = state.get('proof_evidence')
800
+ if proof_evidence is not None:
801
+ if not isinstance(proof_evidence, dict):
802
+ return True
803
+ if len(proof_evidence.keys()) > 0:
804
+ return True
805
+ bundle = state.get('evidence_bundle') or {}
806
+ if not isinstance(bundle, dict):
807
+ bundle = {}
808
+ after = bundle.get('after') or {}
809
+ if not isinstance(after, dict):
810
+ after = {}
811
+ supporting = after.get('supporting_artifacts') or {}
812
+ if not isinstance(supporting, dict):
813
+ supporting = {}
814
+ request = state.get('proof_assessment_request') or {}
815
+ if not isinstance(request, dict):
816
+ request = {}
817
+ structured_evidence = request.get('structured_evidence') or {}
818
+ if not isinstance(structured_evidence, dict):
819
+ structured_evidence = {}
820
+ bundle_proof_evidence = bundle.get('proof_evidence') or {}
821
+ after_proof_evidence = after.get('proof_evidence') or {}
822
+ return bool(
823
+ supporting.get('proof_evidence_present') is True
824
+ or structured_evidence.get('proof_evidence_present') is True
825
+ or (isinstance(bundle_proof_evidence, dict) and len(bundle_proof_evidence.keys()) > 0)
826
+ or (isinstance(after_proof_evidence, dict) and len(after_proof_evidence.keys()) > 0)
827
+ )
828
+
829
+
830
+ def proof_assessment_hard_blockers_for_state(state):
831
+ request = state.get('proof_assessment_request') or {}
832
+ if not isinstance(request, dict):
833
+ request = {}
834
+ blockers = []
835
+
836
+ def add(value):
837
+ if not isinstance(value, str):
838
+ return
839
+ trimmed = value.strip()
840
+ if trimmed and trimmed not in blockers:
841
+ blockers.append(trimmed)
842
+
843
+ for blocker in request.get('hard_blockers') or []:
844
+ add(blocker)
845
+ add(state.get('structured_interaction_capture_failure_summary'))
846
+ add(state.get('structured_interaction_failure_summary'))
847
+ if normalized_verification_mode(state) in INTERACTION_MODES and not state_has_proof_evidence(state):
848
+ add('interaction proof evidence is required before ready_to_ship; proof_evidence_present=false')
849
+ if str(state.get('merge_recommendation') or '').strip() == 'do-not-merge' and blockers:
850
+ add('merge_recommendation=do-not-merge because the proof bundle contains hard blockers.')
851
+ return blockers
852
+
853
+
854
+ def required_baseline_labels_for_state(state):
855
+ reference = str(state.get('requested_reference') or state.get('reference') or 'before').strip()
856
+ labels = []
857
+ if reference in ('before', 'both'):
858
+ labels.append('before')
859
+ if reference in ('prod', 'both'):
860
+ labels.append('prod')
861
+ return labels
862
+
863
+
864
+ def ship_gate_report_facts(state):
865
+ reference = str(state.get('requested_reference') or state.get('reference') or 'before').strip() or 'before'
866
+ prod_url = str(state.get('prod_url') or '').strip()
867
+ before_cdn = str(state.get('before_cdn') or '').strip()
868
+ prod_cdn = str(state.get('prod_cdn') or '').strip()
869
+ after_cdn = str(state.get('after_cdn') or '').strip()
870
+ verify_status = str(state.get('verify_status') or '').strip()
871
+ assessment = state.get('proof_assessment') or {}
872
+ if not isinstance(assessment, dict):
873
+ assessment = {}
874
+ proof_source = str(assessment.get('source') or state.get('proof_assessment_source') or '').strip().lower()
875
+ proof_decision = str(assessment.get('decision') or '').strip()
876
+ visual_delta = visual_delta_for_state(state)
877
+ visual_delta_required = visual_delta_required_for_ship(state)
878
+ visual_delta_blocker = visual_delta_ship_blocker(state)
879
+ hard_blockers = proof_assessment_hard_blockers_for_state(state)
880
+ required_baselines = required_baseline_labels_for_state(state)
881
+ after_evidence_present = state_has_after_evidence(state)
882
+ reasons = []
883
+
884
+ if reference not in REFERENCE_MODES:
885
+ reasons.append('reference must be before, prod, or both; got ' + reference)
886
+ if 'before' in required_baselines and not before_cdn:
887
+ reasons.append('before_cdn is required before ship')
888
+ if 'prod' in required_baselines:
889
+ if not prod_url:
890
+ reasons.append('prod_url is required when reference=' + reference)
891
+ if not prod_cdn:
892
+ reasons.append('prod_cdn is required before ship')
893
+ if not after_evidence_present:
894
+ reasons.append('after_cdn is required before ship')
895
+ if verify_status != 'evidence_captured':
896
+ reasons.append('verify_status must be evidence_captured before ship')
897
+ if proof_source not in ('supervising_agent', 'supervisor'):
898
+ reasons.append('proof_assessment.source must be supervising_agent before ship')
899
+ if proof_decision != 'ready_to_ship':
900
+ reasons.append('proof_assessment.decision must be ready_to_ship before ship')
901
+ if visual_delta_blocker:
902
+ reasons.append(visual_delta_blocker)
903
+ for blocker in hard_blockers:
904
+ reasons.append('proof hard blocker prevents ready_to_ship: ' + blocker)
905
+
906
+ return {
907
+ 'ok': len(reasons) == 0,
908
+ 'reasons': reasons,
909
+ 'required_baselines': required_baselines,
910
+ 'evidence': {
911
+ 'reference': reference,
912
+ 'verification_mode': normalized_verification_mode(state),
913
+ 'prod_url_present': bool(prod_url),
914
+ 'before_present': bool(before_cdn),
915
+ 'prod_present': bool(prod_cdn),
916
+ 'after_present': bool(after_cdn) or after_evidence_present,
917
+ 'after_artifact_url_present': bool(after_cdn),
918
+ 'verify_status': verify_status or None,
919
+ 'proof_assessment_decision': proof_decision or None,
920
+ 'proof_assessment_source': proof_source or None,
921
+ 'visual_delta_required': visual_delta_required,
922
+ 'visual_delta_status': visual_delta.get('status') if isinstance(visual_delta.get('status'), str) else None,
923
+ 'visual_delta_passed': visual_delta.get('passed') if isinstance(visual_delta.get('passed'), bool) else None,
924
+ 'hard_blockers': hard_blockers,
925
+ },
926
+ }
927
+
928
+
929
+ def ship_gate_failure_message(ship_gate):
930
+ reasons = ship_gate.get('reasons') or []
931
+ if not reasons:
932
+ return 'Ship gate is blocked.'
933
+ first = reasons[0]
934
+ if first == 'after_cdn is required before ship':
935
+ return 'No after evidence in state. Run verify first.'
936
+ if first.startswith('visual_delta.'):
937
+ return first + '. Rerun verify with measured before/after visual delta or return a non-shipping proof assessment.'
938
+ return first
939
+
940
+
941
+ def ship_gate_text(state):
942
+ gate = ship_gate_report_facts(state)
943
+ evidence = gate.get('evidence') or {}
944
+ required = gate.get('required_baselines') or []
945
+ hard_blockers = evidence.get('hard_blockers') or []
946
+ visual_status = evidence.get('visual_delta_status') or ('required' if evidence.get('visual_delta_required') else 'not_required')
947
+ lines = [
948
+ 'Status: ' + ('ok' if gate.get('ok') else 'blocked'),
949
+ 'Reference: ' + str(evidence.get('reference') or 'unknown'),
950
+ 'Required baselines: ' + (', '.join(required) if required else 'none'),
951
+ 'Evidence present: before=' + ('yes' if evidence.get('before_present') else 'no')
952
+ + ', prod=' + ('yes' if evidence.get('prod_present') else 'no')
953
+ + ', after=' + ('yes' if evidence.get('after_present') else 'no'),
954
+ 'Verify status: ' + str(evidence.get('verify_status') or 'unknown'),
955
+ 'Proof assessment: source=' + str(evidence.get('proof_assessment_source') or 'unknown')
956
+ + ', decision=' + str(evidence.get('proof_assessment_decision') or 'unknown'),
957
+ 'Visual delta: ' + str(visual_status),
958
+ 'Hard blockers: ' + (', '.join(hard_blockers) if hard_blockers else 'none'),
959
+ ]
960
+ if gate.get('reasons'):
961
+ lines.append('Reasons: ' + '; '.join(str(reason) for reason in gate.get('reasons') or []))
962
+ return '\n'.join(lines)
963
+
964
+
798
965
  def evidence_bundle_text(state):
799
966
  bundle = state.get('evidence_bundle') or {}
800
967
  if not isinstance(bundle, dict):
@@ -1033,29 +1200,10 @@ def post_assessment_comment_if_needed(state, repo_dir, pr_num):
1033
1200
 
1034
1201
  s = load_state()
1035
1202
 
1036
- before_cdn = s.get('before_cdn', '')
1037
- prod_cdn = s.get('prod_cdn', '')
1038
- after_cdn = s.get('after_cdn', '')
1039
- reference = s.get('requested_reference') or s.get('reference', 'before')
1040
- prod_url = (s.get('prod_url') or '').strip()
1041
1203
  proof_assessment = s.get('proof_assessment') or {}
1042
- proof_source = str(proof_assessment.get('source') or s.get('proof_assessment_source') or '').strip().lower()
1043
- if not state_has_after_evidence(s):
1044
- raise SystemExit('No after evidence in state. Run verify first.')
1045
- if s.get('verify_status') != 'evidence_captured':
1046
- raise SystemExit('verify_status must be evidence_captured before ship.')
1047
- if reference in ('before', 'both') and not before_cdn:
1048
- raise SystemExit('before_cdn is required before ship. Run recon/verify again and preserve the approved baseline.')
1049
- if reference in ('prod', 'both'):
1050
- if not prod_url:
1051
- raise SystemExit('prod_url is required when reference=' + reference + ' before ship.')
1052
- if not prod_cdn:
1053
- raise SystemExit('prod_cdn is required before ship. Run recon/verify again and preserve the approved prod baseline.')
1054
- visual_delta_blocker = visual_delta_ship_blocker(s)
1055
- if visual_delta_blocker:
1056
- raise SystemExit(visual_delta_blocker + '. Rerun verify with measured before/after visual delta or return a non-shipping proof assessment.')
1057
- if proof_source not in ('supervising_agent', 'supervisor') or proof_assessment.get('decision') != 'ready_to_ship':
1058
- raise SystemExit('Supervising-agent proof_assessment.decision=ready_to_ship is required before ship.')
1204
+ ship_gate = ship_gate_report_facts(s)
1205
+ if not ship_gate.get('ok'):
1206
+ raise SystemExit(ship_gate_failure_message(ship_gate))
1059
1207
 
1060
1208
  s['merge_recommendation'] = effective_merge_recommendation(s)
1061
1209
  s['proof_decision'] = proof_assessment.get('decision')
@@ -1193,6 +1341,7 @@ if s.get('success_criteria'):
1193
1341
  body += '**Success criteria:** ' + s['success_criteria'] + '\n\n'
1194
1342
  body += '**Verification mode:** ' + s.get('verification_mode', 'proof') + '\n\n'
1195
1343
  body += '**Merge recommendation:** ' + effective_merge_recommendation(s) + '\n\n'
1344
+ body += '**Ship gate:** ' + ('ok' if ship_gate_report_facts(s).get('ok') else 'blocked') + '\n\n'
1196
1345
 
1197
1346
  public_artifacts = public_proof_artifacts(s)
1198
1347
  if publication.get('ok') and not publication.get('skipped'):
@@ -1251,6 +1400,9 @@ if bundle_text:
1251
1400
  assessment_text = proof_assessment_text(s)
1252
1401
  if assessment_text:
1253
1402
  body += '### Supervising proof assessment\n```\n' + assessment_text + '\n```\n\n'
1403
+ gate_text = ship_gate_text(s)
1404
+ if gate_text:
1405
+ body += '### Ship gate\n```\n' + gate_text + '\n```\n\n'
1254
1406
  body += '### Proof summary\n```\n' + (s.get('proof_summary') or 'No summary') + '\n```\n\n'
1255
1407
  body += '### Assertion status\n' + s.get('assertion_status', 'unknown') + '\n\n'
1256
1408
  notes = s.get('evidence_notes') or []
@@ -18,6 +18,13 @@ def run(args, cwd, env=None):
18
18
  return result
19
19
 
20
20
 
21
+ def run_failure(args, cwd, env=None):
22
+ result = subprocess.run(args, cwd=cwd, env=env, capture_output=True, text=True, timeout=120)
23
+ if result.returncode == 0:
24
+ raise AssertionError(f"{' '.join(args)} unexpectedly succeeded\nstdout:\n{result.stdout}")
25
+ return result
26
+
27
+
21
28
  def write_fake_gh(path):
22
29
  path.write_text(
23
30
  """#!/usr/bin/env python3
@@ -90,12 +97,15 @@ def main():
90
97
  (repo / "README.md").write_text("changed\n", encoding="utf-8")
91
98
 
92
99
  # Tiny valid PNG header/body is enough for GitHub Markdown image embedding.
100
+ png_bytes = bytes.fromhex(
101
+ "89504e470d0a1a0a0000000d4948445200000001000000010802000000907753de"
102
+ "0000000c49444154789c63606060000000040001f61738550000000049454e44ae426082"
103
+ )
104
+ before_screenshot = artifacts / "before-proof.png"
105
+ before_screenshot.write_bytes(png_bytes)
93
106
  screenshot = artifacts / "after-proof.png"
94
107
  screenshot.write_bytes(
95
- bytes.fromhex(
96
- "89504e470d0a1a0a0000000d4948445200000001000000010802000000907753de"
97
- "0000000c49444154789c63606060000000040001f61738550000000049454e44ae426082"
98
- )
108
+ png_bytes
99
109
  )
100
110
  proof_json = artifacts / "proof.json"
101
111
  proof_json.write_text(
@@ -119,9 +129,10 @@ def main():
119
129
  "commit_message": "Test proof artifact publication",
120
130
  "success_criteria": "The PR proof comment embeds a GitHub-hosted image.",
121
131
  "verification_mode": "proof",
122
- "requested_reference": "none",
123
- "reference": "none",
132
+ "requested_reference": "before",
133
+ "reference": "before",
124
134
  "verify_status": "evidence_captured",
135
+ "before_cdn": before_screenshot.as_uri(),
125
136
  "after_cdn": screenshot.as_uri(),
126
137
  "assertion_status": "passed",
127
138
  "proof_summary": "All assertions passed.",
@@ -170,9 +181,29 @@ def main():
170
181
  publication = updated.get("proof_artifact_publication") or {}
171
182
  assert publication.get("ok") is True, "proof artifact publication should be recorded"
172
183
  assert publication.get("artifacts"), "published artifact list should be recorded"
173
- assert updated.get("ship_report", {}).get("after_artifact_url", "").startswith(
184
+ ship_report = updated.get("ship_report", {})
185
+ assert ship_report.get("after_artifact_url", "").startswith(
174
186
  "https://github.com/user-attachments/assets/"
175
187
  ), "ship report should expose a GitHub-hosted attachment URL for the after artifact"
188
+ ship_gate = ship_report.get("ship_gate") or {}
189
+ assert ship_gate.get("ok") is True, "public ship report should expose a passing ship gate"
190
+ assert ship_gate.get("required_baselines") == ["before"], (
191
+ "public ship report should expose required baseline obligations"
192
+ )
193
+ gate_evidence = ship_gate.get("evidence") or {}
194
+ assert gate_evidence.get("reference") == "before", "ship gate should expose the reference mode"
195
+ assert gate_evidence.get("before_present") is True, "ship gate should expose baseline presence"
196
+ assert gate_evidence.get("after_present") is True, "ship gate should expose after evidence presence"
197
+ assert gate_evidence.get("verify_status") == "evidence_captured", (
198
+ "ship gate should expose verify status"
199
+ )
200
+ assert gate_evidence.get("proof_assessment_source") == "supervising_agent", (
201
+ "ship gate should expose trusted proof source"
202
+ )
203
+ assert gate_evidence.get("proof_assessment_decision") == "ready_to_ship", (
204
+ "ship gate should expose proof decision"
205
+ )
206
+ assert gate_evidence.get("hard_blockers") == [], "ship gate should expose hard blockers"
176
207
 
177
208
  comment = comment_body_path.read_text(encoding="utf-8")
178
209
  assert "file://" not in comment, "PR proof comment must not expose local file URLs"
@@ -186,11 +217,33 @@ def main():
186
217
  "PR proof comment should link the structured proof JSON"
187
218
  )
188
219
  assert "Proof artifacts:" in comment, "PR proof comment should link the artifact bundle"
220
+ assert "### Ship gate" in comment, "PR proof comment should include the public ship gate"
221
+ assert "Status: ok" in comment, "PR proof comment should expose passing ship gate status"
222
+ assert "Required baselines: before" in comment, (
223
+ "PR proof comment should expose required baseline obligations"
224
+ )
189
225
 
190
226
  artifact_branch = publication.get("branch")
191
227
  refs = run(["git", f"--git-dir={origin}", "show-ref", f"refs/heads/{artifact_branch}"], cwd=root)
192
228
  assert artifact_branch in refs.stdout, "artifact branch should be pushed to origin"
193
229
 
230
+ invalid_reference_state = {**state, "requested_reference": "none", "reference": "none"}
231
+ state_path.write_text(json.dumps(invalid_reference_state, indent=2), encoding="utf-8")
232
+ invalid_reference = run_failure(["python3", str(SHIP)], cwd=repo, env=env)
233
+ assert "reference must be before, prod, or both; got none" in invalid_reference.stderr, (
234
+ "ship.py should reject unsupported public report reference modes"
235
+ )
236
+
237
+ hard_blocker_state = {
238
+ **state,
239
+ "proof_assessment_request": {"hard_blockers": ["structured proof assertion failed"]},
240
+ }
241
+ state_path.write_text(json.dumps(hard_blocker_state, indent=2), encoding="utf-8")
242
+ hard_blocker = run_failure(["python3", str(SHIP)], cwd=repo, env=env)
243
+ assert "proof hard blocker prevents ready_to_ship: structured proof assertion failed" in hard_blocker.stderr, (
244
+ "ship.py should reject hard blockers before publishing a pass report"
245
+ )
246
+
194
247
 
195
248
  if __name__ == "__main__":
196
249
  main()