@riddledc/riddle-proof 0.8.48 → 0.8.49
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
package/runtime/lib/ship.py
CHANGED
|
@@ -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
|
-
|
|
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 evidence 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 evidence 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
|
-
|
|
1043
|
-
if not
|
|
1044
|
-
raise SystemExit(
|
|
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
|
-
|
|
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": "
|
|
123
|
-
"reference": "
|
|
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
|
-
|
|
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()
|