Skip to content

Commit e41252e

Browse files
cschanhniemOpenCode
andauthored
feat: Phase U U1 family evidence trail view schema -- per-family aggregation of all evidence schema references (FET-); REQUIRED_TRAIL_SCHEMA_TYPES (9: BTI/BEG/SAT/PCC/BSP/SEG/ECC/RSR/PQT); VALID_TRAIL_COMPLETENESS_GRADES (complete/partial/empty); build() deduplicates event types, auto-computes grade; dry_lab_only=True; 57 tests; fix regression test to use sys.executable (#990) (#990)
Co-authored-by: OpenCode <opencode@example.com>
1 parent ff119ac commit e41252e

3 files changed

Lines changed: 486 additions & 2 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""FET- family evidence trail view schema.
2+
3+
Per-family aggregation of all evidence schema references into a timeline view.
4+
Shows which schemas are present, missing, and when produced.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from dataclasses import dataclass, field
10+
11+
REQUIRED_TRAIL_SCHEMA_TYPES: frozenset[str] = frozenset({
12+
"BTI", "BEG", "SAT", "PCC",
13+
"BSP", "SEG", "ECC", "RSR", "PQT",
14+
})
15+
16+
VALID_TRAIL_COMPLETENESS_GRADES: frozenset[str] = frozenset({
17+
"complete", "partial", "empty",
18+
})
19+
20+
21+
@dataclass
22+
class FamilySchemaEvent:
23+
schema_type: str
24+
artifact_id: str
25+
created_at: str
26+
27+
28+
@dataclass
29+
class FamilyEvidenceTrailView:
30+
fet_id: str
31+
candidate_family_id: str
32+
batch_id: str
33+
pipeline_version: str
34+
schema_events: list[FamilySchemaEvent]
35+
present_schema_types: list[str]
36+
missing_schema_types: list[str]
37+
n_schemas_present: int
38+
n_schemas_required: int
39+
trail_completeness_grade: str
40+
dry_lab_only: bool
41+
limitations: list[str]
42+
created_at: str
43+
44+
45+
def _compute_grade(n_present: int, n_required: int) -> str:
46+
if n_present == 0:
47+
return "empty"
48+
if n_present == n_required:
49+
return "complete"
50+
return "partial"
51+
52+
53+
def validate_family_evidence_trail_view(view: FamilyEvidenceTrailView) -> None:
54+
if not view.fet_id.startswith("FET-"):
55+
raise ValueError(f"fet_id must start with 'FET-': {view.fet_id!r}")
56+
if view.candidate_family_id.startswith("TOY-"):
57+
raise ValueError(
58+
f"candidate_family_id must not start with 'TOY-': {view.candidate_family_id!r}"
59+
)
60+
if not view.candidate_family_id:
61+
raise ValueError("candidate_family_id must be non-empty")
62+
if not view.batch_id:
63+
raise ValueError("batch_id must be non-empty")
64+
if not view.pipeline_version:
65+
raise ValueError("pipeline_version must be non-empty")
66+
for event in view.schema_events:
67+
if event.schema_type not in REQUIRED_TRAIL_SCHEMA_TYPES:
68+
raise ValueError(
69+
f"schema_type {event.schema_type!r} not in REQUIRED_TRAIL_SCHEMA_TYPES"
70+
)
71+
if not event.artifact_id:
72+
raise ValueError(
73+
f"artifact_id must be non-empty for schema_type {event.schema_type!r}"
74+
)
75+
if view.trail_completeness_grade not in VALID_TRAIL_COMPLETENESS_GRADES:
76+
raise ValueError(
77+
f"trail_completeness_grade {view.trail_completeness_grade!r} not in "
78+
f"VALID_TRAIL_COMPLETENESS_GRADES"
79+
)
80+
if not view.dry_lab_only:
81+
raise ValueError("dry_lab_only must be True")
82+
if not view.limitations:
83+
raise ValueError("limitations must be non-empty")
84+
if not view.created_at:
85+
raise ValueError("created_at must be non-empty")
86+
n_req = len(REQUIRED_TRAIL_SCHEMA_TYPES)
87+
if view.n_schemas_required != n_req:
88+
raise ValueError(
89+
f"n_schemas_required must be {n_req}, got {view.n_schemas_required}"
90+
)
91+
if view.n_schemas_present != len(view.present_schema_types):
92+
raise ValueError("n_schemas_present must equal len(present_schema_types)")
93+
expected_missing = sorted(
94+
REQUIRED_TRAIL_SCHEMA_TYPES - set(view.present_schema_types)
95+
)
96+
if view.missing_schema_types != expected_missing:
97+
raise ValueError(
98+
f"missing_schema_types mismatch: expected {expected_missing}, "
99+
f"got {view.missing_schema_types}"
100+
)
101+
102+
103+
def build_family_evidence_trail_view(
104+
*,
105+
fet_id: str,
106+
candidate_family_id: str,
107+
batch_id: str,
108+
pipeline_version: str,
109+
schema_event_dicts: list[dict],
110+
limitations: list[str],
111+
created_at: str,
112+
) -> FamilyEvidenceTrailView:
113+
events = [
114+
FamilySchemaEvent(
115+
schema_type=d["schema_type"],
116+
artifact_id=d["artifact_id"],
117+
created_at=d["created_at"],
118+
)
119+
for d in schema_event_dicts
120+
]
121+
present_types = sorted({e.schema_type for e in events})
122+
missing_types = sorted(REQUIRED_TRAIL_SCHEMA_TYPES - set(present_types))
123+
n_required = len(REQUIRED_TRAIL_SCHEMA_TYPES)
124+
n_present = len(present_types)
125+
grade = _compute_grade(n_present, n_required)
126+
view = FamilyEvidenceTrailView(
127+
fet_id=fet_id,
128+
candidate_family_id=candidate_family_id,
129+
batch_id=batch_id,
130+
pipeline_version=pipeline_version,
131+
schema_events=events,
132+
present_schema_types=present_types,
133+
missing_schema_types=missing_types,
134+
n_schemas_present=n_present,
135+
n_schemas_required=n_required,
136+
trail_completeness_grade=grade,
137+
dry_lab_only=True,
138+
limitations=limitations,
139+
created_at=created_at,
140+
)
141+
validate_family_evidence_trail_view(view)
142+
return view
143+
144+
145+
def format_family_evidence_trail_view(view: FamilyEvidenceTrailView) -> str:
146+
lines = [
147+
f"Family Evidence Trail View — {view.fet_id}",
148+
f"Family: {view.candidate_family_id} | Batch: {view.batch_id} | Pipeline: {view.pipeline_version}",
149+
f"Grade: {view.trail_completeness_grade} | Present: {view.n_schemas_present}/{view.n_schemas_required}",
150+
]
151+
if view.present_schema_types:
152+
lines.append(f"Present: {', '.join(view.present_schema_types)}")
153+
if view.missing_schema_types:
154+
lines.append(f"Missing: {', '.join(view.missing_schema_types)}")
155+
lines.append(f"Created: {view.created_at}")
156+
if view.schema_events:
157+
lines.append("Events:")
158+
for e in view.schema_events:
159+
lines.append(f" {e.schema_type}: {e.artifact_id} @ {e.created_at}")
160+
lines.append(f"Limitations: {'; '.join(view.limitations)}")
161+
lines.append(f"dry_lab_only: {view.dry_lab_only}")
162+
return "\n".join(lines)

0 commit comments

Comments
 (0)