Skip to content

Commit 8a97a41

Browse files
cschanhniemOpenCode
andauthored
feat: Phase T T2 cross-batch audit summary (CBA2-) -- aggregates BTI records; VALID_BATCH_GRADES (4), LOW_COMPLETENESS_GRADES (minority/none), VALID_AUDIT_VERDICTS (5); n_batches_flagged auto-counted; flagged_batch_ids sorted; audit_coverage_rate; build() auto-computes; dry_lab_only=True; 55+ tests (#978) (#979)
Co-authored-by: OpenCode <opencode@example.com>
1 parent 07f1694 commit 8a97a41

3 files changed

Lines changed: 940 additions & 1 deletion

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
from __future__ import annotations
2+
from dataclasses import dataclass
3+
from typing import List
4+
5+
6+
VALID_BATCH_GRADES = frozenset({"full", "majority", "minority", "none"})
7+
8+
LOW_COMPLETENESS_GRADES = frozenset({"minority", "none"})
9+
10+
VALID_AUDIT_VERDICTS = frozenset({
11+
"all_complete",
12+
"majority_complete",
13+
"minority_complete",
14+
"none_complete",
15+
"no_batches",
16+
})
17+
18+
COMPLETENESS_AUDIT_THRESHOLD = 0.5
19+
20+
21+
@dataclass
22+
class BatchSummaryEntry:
23+
batch_id: str
24+
bti_id: str
25+
n_families_total: int
26+
n_families_complete: int
27+
n_families_partial: int
28+
n_families_empty: int
29+
batch_completeness_grade: str
30+
flagged_low_completeness: bool
31+
32+
33+
@dataclass
34+
class CrossBatchAuditSummary:
35+
cba2_id: str
36+
pipeline_version: str
37+
n_batches_total: int
38+
n_batches_flagged: int
39+
total_families_across_batches: int
40+
total_families_complete: int
41+
total_families_partial: int
42+
total_families_empty: int
43+
batch_entries: list[BatchSummaryEntry]
44+
overall_audit_verdict: str
45+
flagged_batch_ids: list[str]
46+
audit_coverage_rate: float
47+
dry_lab_only: bool
48+
limitations: list[str]
49+
created_at: str
50+
51+
52+
def _compute_overall_audit_verdict(
53+
n_batches_total: int,
54+
n_full: int,
55+
) -> str:
56+
if n_batches_total == 0:
57+
return "no_batches"
58+
if n_full == n_batches_total:
59+
return "all_complete"
60+
if n_full >= n_batches_total / 2:
61+
return "majority_complete"
62+
if n_full > 0:
63+
return "minority_complete"
64+
return "none_complete"
65+
66+
67+
def validate_cross_batch_audit_summary(
68+
summary: CrossBatchAuditSummary,
69+
) -> list[str]:
70+
violations = []
71+
72+
if not summary.cba2_id.startswith("CBA2-"):
73+
violations.append("cba2_id must start with 'CBA2-'")
74+
75+
if not summary.dry_lab_only:
76+
violations.append("dry_lab_only must be True")
77+
78+
if summary.n_batches_total != len(summary.batch_entries):
79+
violations.append(
80+
f"n_batches_total ({summary.n_batches_total}) must equal len(batch_entries) ({len(summary.batch_entries)})"
81+
)
82+
83+
expected_flagged = sum(
84+
1 for e in summary.batch_entries if e.flagged_low_completeness
85+
)
86+
if summary.n_batches_flagged != expected_flagged:
87+
violations.append(
88+
f"n_batches_flagged ({summary.n_batches_flagged}) must equal count of flagged entries ({expected_flagged})"
89+
)
90+
91+
expected_total = sum(e.n_families_total for e in summary.batch_entries)
92+
if summary.total_families_across_batches != expected_total:
93+
violations.append(
94+
f"total_families_across_batches ({summary.total_families_across_batches}) must equal sum of n_families_total ({expected_total})"
95+
)
96+
97+
expected_complete = sum(e.n_families_complete for e in summary.batch_entries)
98+
if summary.total_families_complete != expected_complete:
99+
violations.append(
100+
f"total_families_complete ({summary.total_families_complete}) must equal sum of n_families_complete ({expected_complete})"
101+
)
102+
103+
expected_partial = sum(e.n_families_partial for e in summary.batch_entries)
104+
if summary.total_families_partial != expected_partial:
105+
violations.append(
106+
f"total_families_partial ({summary.total_families_partial}) must equal sum of n_families_partial ({expected_partial})"
107+
)
108+
109+
expected_empty = sum(e.n_families_empty for e in summary.batch_entries)
110+
if summary.total_families_empty != expected_empty:
111+
violations.append(
112+
f"total_families_empty ({summary.total_families_empty}) must equal sum of n_families_empty ({expected_empty})"
113+
)
114+
115+
for entry in summary.batch_entries:
116+
expected_flag = entry.batch_completeness_grade in LOW_COMPLETENESS_GRADES
117+
if entry.flagged_low_completeness != expected_flag:
118+
violations.append(
119+
f"flagged_low_completeness ({entry.flagged_low_completeness}) for batch {entry.batch_id} must equal {expected_flag} for grade '{entry.batch_completeness_grade}'"
120+
)
121+
122+
expected_flagged_ids = sorted(
123+
e.batch_id for e in summary.batch_entries if e.flagged_low_completeness
124+
)
125+
if summary.flagged_batch_ids != expected_flagged_ids:
126+
violations.append(
127+
f"flagged_batch_ids ({summary.flagged_batch_ids}) must equal {expected_flagged_ids}"
128+
)
129+
130+
total_families = summary.total_families_across_batches
131+
expected_coverage = (
132+
summary.total_families_complete / total_families if total_families > 0 else 0.0
133+
)
134+
if abs(summary.audit_coverage_rate - expected_coverage) > 0.001:
135+
violations.append(
136+
f"audit_coverage_rate ({summary.audit_coverage_rate}) must approximately equal {expected_coverage}"
137+
)
138+
139+
n_full = sum(
140+
1 for e in summary.batch_entries if e.batch_completeness_grade == "full"
141+
)
142+
expected_verdict = _compute_overall_audit_verdict(
143+
summary.n_batches_total, n_full
144+
)
145+
if summary.overall_audit_verdict != expected_verdict:
146+
violations.append(
147+
f"overall_audit_verdict '{summary.overall_audit_verdict}' must be '{expected_verdict}' "
148+
f"given n_batches_total={summary.n_batches_total} and n_full={n_full}"
149+
)
150+
151+
if not summary.limitations:
152+
violations.append("limitations must be non-empty")
153+
154+
return violations
155+
156+
157+
def build_cross_batch_audit_summary(
158+
cba2_id: str,
159+
pipeline_version: str,
160+
batch_entry_dicts: list[dict],
161+
limitations: list[str],
162+
created_at: str,
163+
) -> CrossBatchAuditSummary:
164+
batch_entries: list[BatchSummaryEntry] = []
165+
for d in batch_entry_dicts:
166+
grade = d["batch_completeness_grade"]
167+
flagged = grade in LOW_COMPLETENESS_GRADES
168+
batch_entries.append(
169+
BatchSummaryEntry(
170+
batch_id=d["batch_id"],
171+
bti_id=d["bti_id"],
172+
n_families_total=d["n_families_total"],
173+
n_families_complete=d["n_families_complete"],
174+
n_families_partial=d["n_families_partial"],
175+
n_families_empty=d["n_families_empty"],
176+
batch_completeness_grade=grade,
177+
flagged_low_completeness=flagged,
178+
)
179+
)
180+
181+
n_batches_total = len(batch_entries)
182+
n_batches_flagged = sum(1 for e in batch_entries if e.flagged_low_completeness)
183+
total_families_across_batches = sum(e.n_families_total for e in batch_entries)
184+
total_families_complete = sum(e.n_families_complete for e in batch_entries)
185+
total_families_partial = sum(e.n_families_partial for e in batch_entries)
186+
total_families_empty = sum(e.n_families_empty for e in batch_entries)
187+
audit_coverage_rate = (
188+
total_families_complete / total_families_across_batches
189+
if total_families_across_batches > 0
190+
else 0.0
191+
)
192+
flagged_batch_ids = sorted(
193+
e.batch_id for e in batch_entries if e.flagged_low_completeness
194+
)
195+
n_full = sum(
196+
1 for e in batch_entries if e.batch_completeness_grade == "full"
197+
)
198+
overall_audit_verdict = _compute_overall_audit_verdict(
199+
n_batches_total, n_full
200+
)
201+
202+
summary = CrossBatchAuditSummary(
203+
cba2_id=cba2_id,
204+
pipeline_version=pipeline_version,
205+
n_batches_total=n_batches_total,
206+
n_batches_flagged=n_batches_flagged,
207+
total_families_across_batches=total_families_across_batches,
208+
total_families_complete=total_families_complete,
209+
total_families_partial=total_families_partial,
210+
total_families_empty=total_families_empty,
211+
batch_entries=batch_entries,
212+
overall_audit_verdict=overall_audit_verdict,
213+
flagged_batch_ids=flagged_batch_ids,
214+
audit_coverage_rate=audit_coverage_rate,
215+
dry_lab_only=True,
216+
limitations=limitations,
217+
created_at=created_at,
218+
)
219+
220+
violations = validate_cross_batch_audit_summary(summary)
221+
if violations:
222+
raise ValueError(f"Invalid CrossBatchAuditSummary: {violations}")
223+
return summary
224+
225+
226+
def format_cross_batch_audit_summary(summary: CrossBatchAuditSummary) -> str:
227+
flagged_str = ", ".join(summary.flagged_batch_ids) if summary.flagged_batch_ids else "(none)"
228+
limitations_str = "; ".join(summary.limitations)
229+
lines = [
230+
f"Cross-Batch Audit Summary — {summary.cba2_id}",
231+
f"Pipeline: {summary.pipeline_version} | Created: {summary.created_at}",
232+
f"Batches: {summary.n_batches_total} total, {summary.n_batches_flagged} flagged",
233+
f"Audit Verdict: {summary.overall_audit_verdict} | "
234+
f"Coverage Rate: {summary.audit_coverage_rate:.3f}",
235+
f"Families — Complete: {summary.total_families_complete} | "
236+
f"Partial: {summary.total_families_partial} | "
237+
f"Empty: {summary.total_families_empty} | "
238+
f"Total: {summary.total_families_across_batches}",
239+
f"Flagged Batches: [{flagged_str}]",
240+
f"Limitations: {limitations_str}",
241+
"dry_lab_only: True",
242+
"",
243+
"Batch Entries:",
244+
]
245+
for entry in summary.batch_entries:
246+
flag_marker = " [FLAGGED]" if entry.flagged_low_completeness else ""
247+
lines.append(
248+
f" {entry.batch_id} ({entry.bti_id}): "
249+
f"grade={entry.batch_completeness_grade}, "
250+
f"complete={entry.n_families_complete}, "
251+
f"partial={entry.n_families_partial}, "
252+
f"empty={entry.n_families_empty}, "
253+
f"total={entry.n_families_total}{flag_marker}"
254+
)
255+
return "\n".join(lines)

0 commit comments

Comments
 (0)