Skip to content

Commit 500a5d7

Browse files
author
OpenCode
committed
feat: Phase E E4 safety-release decision schema -- machine-checkable SRD- record auditing external release authorization with required safety checks gate
1 parent 523f79e commit 500a5d7

4 files changed

Lines changed: 503 additions & 2 deletions

File tree

‎docs/research/NEXT_100_PR_MAP.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Make qualified external review easier and safer.
9595
| E1 | Add external review packet schema (complete). — evidence/external_review_packet.py: ERP- schema with 14 fields, validation rules, dry_lab_only enforcement; tests/evidence/test_external_review_packet.py + test_external_review_packet_schema.py. | Packets become machine-checkable. | C/D |
9696
| E2 | Add example external review packet using toy data (complete). — evidence/example_external_review_packet.py: toy-data example ERP with MOCK-/TOY- prefix enforcement and is_example_data=True; tests/evidence/test_example_external_review_packet.py. | Partners know what to expect. | C/D |
9797
| E3 | Add reviewer questionnaire schema (complete). | Makes external review feedback machine-readable: Likert clarity ratings for activity/safety/novelty claims, synthesis recommendation, structured comments. | B/C |
98-
| E4 | Add safety-release decision schema. | Release review becomes auditable. | D |
98+
| E4 | Add safety-release decision schema. | Release review becomes auditable. | D | DONE |
9999
| E5 | Add non-protocol pilot pre-registration schema (complete). — evidence/pilot_preregistration.py: non-protocol pilot pre-registration schema (PPR-) freezing selection logic before batch release; tests/evidence/test_pilot_preregistration.py + test_pilot_preregistration_schema.py. | Freezes selection logic. | C/D |
100100
| E6 | Add packet generator CLI (complete). — scripts/generate_review_packet.py: generates skeleton external review packet JSON; make generate-review-packet target; validates against schemas/external_review_packet.schema.json; dry_lab_only_attestation=True enforced. | Reduces manual packaging errors. | C/D |
101101
| E7 | Add packet validator CLI (complete). — src/openamp_foundry/cli/commands/validate_packet.py: load_packet_from_json() reads ERP- JSON from disk; validate_packet_file() returns {valid, violations, packet_id, error}; _run_validate_packet() prints PASS/FAIL with violations; 45 tests in tests/cli/test_validate_packet.py. | Review readiness becomes testable. | C/D |
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""SRD- safety-release decision schema.
2+
3+
Machine-checkable record of whether a candidate package was authorized for
4+
external release, which safety checks were passed, what restrictions apply,
5+
and why it was rejected if not authorized.
6+
7+
Makes release review auditable: every external release of pipeline outputs
8+
must reference an SRD- record. No release is implicit or unrecorded.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from dataclasses import dataclass
14+
15+
VALID_SRD_DECISIONS: frozenset[str] = frozenset({
16+
"authorized",
17+
"rejected",
18+
"pending_review",
19+
})
20+
21+
VALID_RELEASE_SCOPES: frozenset[str] = frozenset({
22+
"academic_collaboration",
23+
"public_preprint",
24+
"internal_only",
25+
"restricted_partner",
26+
})
27+
28+
VALID_SAFETY_CHECK_IDS: frozenset[str] = frozenset({
29+
"dual_use_screened",
30+
"toxicity_flags_reviewed",
31+
"hemolysis_flags_reviewed",
32+
"novelty_claims_bounded",
33+
"dry_lab_only_label_present",
34+
"no_sequence_privacy_violation",
35+
"candidate_count_within_limit",
36+
})
37+
38+
REQUIRED_SAFETY_CHECKS: tuple[str, ...] = (
39+
"dual_use_screened",
40+
"dry_lab_only_label_present",
41+
"novelty_claims_bounded",
42+
)
43+
44+
45+
@dataclass
46+
class SafetyReleaseDecision:
47+
srd_id: str
48+
pipeline_version: str
49+
erp_id: str
50+
release_decision: str
51+
release_scope: str
52+
safety_checks_passed: list[str]
53+
restrictions: list[str]
54+
rejection_reason: str
55+
all_required_checks_passed: bool
56+
dry_lab_only: bool
57+
limitations: list[str]
58+
created_at: str
59+
60+
61+
def validate_safety_release_decision(srd: SafetyReleaseDecision) -> None:
62+
if not srd.srd_id.startswith("SRD-"):
63+
raise ValueError(f"srd_id must start with 'SRD-': {srd.srd_id!r}")
64+
if not srd.pipeline_version:
65+
raise ValueError("pipeline_version must be non-empty")
66+
if not srd.erp_id.startswith("ERP-"):
67+
raise ValueError(f"erp_id must start with 'ERP-': {srd.erp_id!r}")
68+
if srd.release_decision not in VALID_SRD_DECISIONS:
69+
raise ValueError(
70+
f"release_decision {srd.release_decision!r} not in VALID_SRD_DECISIONS"
71+
)
72+
if srd.release_scope not in VALID_RELEASE_SCOPES:
73+
raise ValueError(
74+
f"release_scope {srd.release_scope!r} not in VALID_RELEASE_SCOPES"
75+
)
76+
for check_id in srd.safety_checks_passed:
77+
if check_id not in VALID_SAFETY_CHECK_IDS:
78+
raise ValueError(
79+
f"safety check {check_id!r} not in VALID_SAFETY_CHECK_IDS"
80+
)
81+
required_passed = all(
82+
req in srd.safety_checks_passed for req in REQUIRED_SAFETY_CHECKS
83+
)
84+
if srd.all_required_checks_passed != required_passed:
85+
raise ValueError(
86+
"all_required_checks_passed mismatch with safety_checks_passed"
87+
)
88+
if srd.release_decision == "authorized" and not srd.all_required_checks_passed:
89+
raise ValueError(
90+
"release_decision='authorized' requires all_required_checks_passed=True"
91+
)
92+
if srd.release_decision == "rejected" and not srd.rejection_reason:
93+
raise ValueError(
94+
"rejection_reason must be non-empty when release_decision='rejected'"
95+
)
96+
if not srd.dry_lab_only:
97+
raise ValueError("dry_lab_only must be True")
98+
if not srd.limitations:
99+
raise ValueError("limitations must be non-empty")
100+
if not srd.created_at:
101+
raise ValueError("created_at must be non-empty")
102+
103+
104+
def build_safety_release_decision(
105+
*,
106+
srd_id: str,
107+
pipeline_version: str,
108+
erp_id: str,
109+
release_decision: str,
110+
release_scope: str,
111+
safety_checks_passed: list[str],
112+
restrictions: list[str],
113+
rejection_reason: str = "",
114+
limitations: list[str],
115+
created_at: str,
116+
) -> SafetyReleaseDecision:
117+
"""Build a SafetyReleaseDecision.
118+
119+
safety_checks_passed: list of VALID_SAFETY_CHECK_IDS that were verified.
120+
all_required_checks_passed is auto-computed from REQUIRED_SAFETY_CHECKS.
121+
"""
122+
required_passed = all(
123+
req in safety_checks_passed for req in REQUIRED_SAFETY_CHECKS
124+
)
125+
srd = SafetyReleaseDecision(
126+
srd_id=srd_id,
127+
pipeline_version=pipeline_version,
128+
erp_id=erp_id,
129+
release_decision=release_decision,
130+
release_scope=release_scope,
131+
safety_checks_passed=list(safety_checks_passed),
132+
restrictions=list(restrictions),
133+
rejection_reason=rejection_reason,
134+
all_required_checks_passed=required_passed,
135+
dry_lab_only=True,
136+
limitations=limitations,
137+
created_at=created_at,
138+
)
139+
validate_safety_release_decision(srd)
140+
return srd
141+
142+
143+
def format_safety_release_decision(srd: SafetyReleaseDecision) -> str:
144+
lines = [
145+
f"Safety-Release Decision — {srd.srd_id}",
146+
f"Pipeline: {srd.pipeline_version} | ERP: {srd.erp_id}",
147+
f"Decision: {srd.release_decision} | Scope: {srd.release_scope}",
148+
f"All required checks passed: {srd.all_required_checks_passed}",
149+
]
150+
lines.append(f"Safety checks passed ({len(srd.safety_checks_passed)}):")
151+
for check in srd.safety_checks_passed:
152+
lines.append(f" + {check}")
153+
missing = [r for r in REQUIRED_SAFETY_CHECKS if r not in srd.safety_checks_passed]
154+
if missing:
155+
lines.append("Required checks NOT passed:")
156+
for m in missing:
157+
lines.append(f" ! {m}")
158+
if srd.restrictions:
159+
lines.append(f"Restrictions: {'; '.join(srd.restrictions)}")
160+
if srd.rejection_reason:
161+
lines.append(f"Rejection reason: {srd.rejection_reason}")
162+
lines.append(f"Created: {srd.created_at}")
163+
lines.append(f"Limitations: {'; '.join(srd.limitations)}")
164+
lines.append(f"dry_lab_only: {srd.dry_lab_only}")
165+
return "\n".join(lines)

0 commit comments

Comments
 (0)