Skip to content

Commit 9fb8f7b

Browse files
cschanhniemOpenCode
andauthored
feat: Phase E E8 release-summary generator -- strips restricted fields before external distribution, machine-checkable RSM- schema with RESTRICTED_FIELDS enforcement (#1019)
Co-authored-by: OpenCode <opencode@example.com>
1 parent 1396556 commit 9fb8f7b

4 files changed

Lines changed: 602 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
@@ -99,7 +99,7 @@ Make qualified external review easier and safer.
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 |
102-
| E8 | Add release-summary generator that strips restricted fields. | Safer public summaries. | D |
102+
| E8 | Add release-summary generator that strips restricted fields. | Safer public summaries. | D | DONE |
103103
| E9 | Add domain review outcome schema (complete). | Structured expert verdict on a PEP with controlled taxonomy of domains and outcomes; closes ESC→RVQ→DRO review chain. | B/C |
104104
| E10 | Add expert-review example with mock/toy candidates only (complete). | ERP- schema: 14 fields, 16 validation rules, mock candidate ID prefix enforcement (MOCK-/TOY-/EXAMPLE-/DEMO-/TEST-), is_example_data=True and dry_lab_only=True enforced; CI-checkable template cannot accidentally leak real candidates. | B/C |
105105

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""RSM- release-summary schema and generator.
2+
3+
Produces a public-safe summary of what was released by stripping restricted
4+
fields before any external distribution. Every external share of pipeline
5+
outputs must go through this generator — no ad-hoc field omission.
6+
7+
Restricted fields list what must NOT appear in public summaries:
8+
individual candidate IDs, internal batch references, rejection reasons,
9+
raw sequence data, reviewer identities, and internal restriction text.
10+
11+
Makes the stripping step machine-checkable rather than relying on human
12+
memory of what is sensitive.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
from dataclasses import dataclass
18+
19+
RESTRICTED_FIELDS: tuple[str, ...] = (
20+
"erp_id",
21+
"rejection_reason",
22+
"restrictions",
23+
"candidate_ids",
24+
"reviewer_ids",
25+
"raw_sequences",
26+
"batch_ids",
27+
"internal_notes",
28+
"collaborator_names",
29+
)
30+
31+
VALID_RSM_RELEASE_SCOPES: frozenset[str] = frozenset({
32+
"academic_collaboration",
33+
"public_preprint",
34+
"internal_only",
35+
"restricted_partner",
36+
})
37+
38+
VALID_RSM_DECISIONS: frozenset[str] = frozenset({
39+
"authorized",
40+
"rejected",
41+
"pending_review",
42+
})
43+
44+
45+
@dataclass
46+
class ReleaseSummary:
47+
rsm_id: str
48+
srd_id: str
49+
pipeline_version: str
50+
release_scope: str
51+
release_decision: str
52+
candidate_count: int
53+
safety_checks_summary: list[str]
54+
public_notes: str
55+
limitations_summary: str
56+
restricted_fields_stripped: bool
57+
dry_lab_only: bool
58+
created_at: str
59+
60+
61+
def validate_release_summary(rsm: ReleaseSummary) -> None:
62+
if not rsm.rsm_id.startswith("RSM-"):
63+
raise ValueError(f"rsm_id must start with 'RSM-': {rsm.rsm_id!r}")
64+
if not rsm.srd_id.startswith("SRD-"):
65+
raise ValueError(f"srd_id must start with 'SRD-': {rsm.srd_id!r}")
66+
if not rsm.pipeline_version:
67+
raise ValueError("pipeline_version must be non-empty")
68+
if rsm.release_scope not in VALID_RSM_RELEASE_SCOPES:
69+
raise ValueError(
70+
f"release_scope {rsm.release_scope!r} not in VALID_RSM_RELEASE_SCOPES"
71+
)
72+
if rsm.release_decision not in VALID_RSM_DECISIONS:
73+
raise ValueError(
74+
f"release_decision {rsm.release_decision!r} not in VALID_RSM_DECISIONS"
75+
)
76+
if rsm.candidate_count < 0:
77+
raise ValueError("candidate_count must be non-negative")
78+
if not rsm.restricted_fields_stripped:
79+
raise ValueError("restricted_fields_stripped must be True")
80+
if not rsm.dry_lab_only:
81+
raise ValueError("dry_lab_only must be True")
82+
if not rsm.limitations_summary:
83+
raise ValueError("limitations_summary must be non-empty")
84+
if not rsm.created_at:
85+
raise ValueError("created_at must be non-empty")
86+
87+
88+
def build_release_summary(
89+
*,
90+
rsm_id: str,
91+
srd_id: str,
92+
pipeline_version: str,
93+
release_scope: str,
94+
release_decision: str,
95+
candidate_count: int,
96+
safety_checks_summary: list[str],
97+
public_notes: str = "",
98+
limitations_summary: str,
99+
created_at: str,
100+
) -> ReleaseSummary:
101+
"""Build a ReleaseSummary with restricted_fields_stripped and dry_lab_only always True."""
102+
rsm = ReleaseSummary(
103+
rsm_id=rsm_id,
104+
srd_id=srd_id,
105+
pipeline_version=pipeline_version,
106+
release_scope=release_scope,
107+
release_decision=release_decision,
108+
candidate_count=candidate_count,
109+
safety_checks_summary=list(safety_checks_summary),
110+
public_notes=public_notes,
111+
limitations_summary=limitations_summary,
112+
restricted_fields_stripped=True,
113+
dry_lab_only=True,
114+
created_at=created_at,
115+
)
116+
validate_release_summary(rsm)
117+
return rsm
118+
119+
120+
def strip_restricted_fields(source: dict) -> dict:
121+
"""Return a copy of *source* with all RESTRICTED_FIELDS removed."""
122+
return {k: v for k, v in source.items() if k not in RESTRICTED_FIELDS}
123+
124+
125+
def generate_release_summary(
126+
*,
127+
rsm_id: str,
128+
srd_id: str,
129+
pipeline_version: str,
130+
release_scope: str,
131+
release_decision: str,
132+
candidate_count: int,
133+
safety_checks_summary: list[str],
134+
limitations_summary: str,
135+
public_notes: str = "",
136+
created_at: str,
137+
source_fields: dict | None = None,
138+
) -> ReleaseSummary:
139+
"""Generate a public-safe ReleaseSummary, stripping any restricted fields
140+
found in *source_fields* before building.
141+
142+
*source_fields* is optional raw data that will be validated for absence of
143+
restricted fields — raising ValueError if any restricted key is present.
144+
This makes accidental leakage detectable at generation time.
145+
"""
146+
if source_fields is not None:
147+
leaked = [k for k in RESTRICTED_FIELDS if k in source_fields]
148+
if leaked:
149+
raise ValueError(
150+
f"Restricted fields present in source_fields, must be stripped first: {leaked}"
151+
)
152+
return build_release_summary(
153+
rsm_id=rsm_id,
154+
srd_id=srd_id,
155+
pipeline_version=pipeline_version,
156+
release_scope=release_scope,
157+
release_decision=release_decision,
158+
candidate_count=candidate_count,
159+
safety_checks_summary=safety_checks_summary,
160+
public_notes=public_notes,
161+
limitations_summary=limitations_summary,
162+
created_at=created_at,
163+
)
164+
165+
166+
def format_release_summary(rsm: ReleaseSummary) -> str:
167+
lines = [
168+
f"Release Summary — {rsm.rsm_id}",
169+
f"Authorization: {rsm.srd_id} | Pipeline: {rsm.pipeline_version}",
170+
f"Decision: {rsm.release_decision} | Scope: {rsm.release_scope}",
171+
f"Candidates in summary: {rsm.candidate_count}",
172+
f"Restricted fields stripped: {rsm.restricted_fields_stripped}",
173+
]
174+
if rsm.safety_checks_summary:
175+
lines.append(f"Safety checks: {'; '.join(rsm.safety_checks_summary)}")
176+
if rsm.public_notes:
177+
lines.append(f"Notes: {rsm.public_notes}")
178+
lines.append(f"Limitations: {rsm.limitations_summary}")
179+
lines.append(f"Created: {rsm.created_at}")
180+
lines.append(f"dry_lab_only: {rsm.dry_lab_only}")
181+
return "\n".join(lines)

0 commit comments

Comments
 (0)