-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpackage.py
More file actions
316 lines (259 loc) · 11.1 KB
/
Copy pathpackage.py
File metadata and controls
316 lines (259 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
"""v0.3 package separating source-grounded records from derived analysis."""
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Literal
from pydantic import Field, model_validator
from paperreading.domain.base import DomainModel
from paperreading.domain.document import DocumentManifest
from paperreading.domain.evidence import EvidenceSpan, VerificationStatus
from paperreading.domain.paper import (
DataDescription,
EmpiricalDesign,
GapType,
PaperMetadata,
Relationship,
Theory,
Variable,
)
class RecordType(str, Enum):
EMPIRICAL = "empirical"
THEORETICAL = "theoretical"
REVIEW = "review"
OTHER = "other"
class LanguageStrength(str, Enum):
DESCRIPTIVE = "descriptive"
ASSOCIATIONAL = "associational"
CAUSAL = "causal"
class LimitationOrigin(str, Enum):
PAPER = "paper"
LEGACY_UNKNOWN = "legacy_unknown"
class AssessmentOrigin(str, Enum):
RESEARCHER = "researcher"
AI_ASSISTED = "ai_assisted"
LEGACY_UNKNOWN = "legacy_unknown"
class AssessmentType(str, Enum):
INTERPRETATION = "interpretation"
LIMITATION = "limitation"
CAVEAT = "caveat"
class AuditStatus(str, Enum):
PASS = "pass"
FAIL = "fail"
UNCLEAR = "unclear"
NOT_APPLICABLE = "not_applicable"
class CausalSupport(str, Enum):
UNSUPPORTED = "unsupported"
LIMITED = "limited"
SUPPORTED_WITH_CAVEATS = "supported_with_caveats"
SUPPORTED = "supported"
class PackageState(str, Enum):
MIGRATED = "migrated"
FINALIZED = "finalized"
VERIFIED = "verified"
AUDITED = "audited"
class GroundedSourceClaim(DomainModel):
claim_id: str = Field(min_length=1)
statement: str = Field(min_length=1)
evidence_ids: list[str] = Field(min_length=1)
class GroundedFinding(DomainModel):
finding_id: str = Field(min_length=1)
statement: str = Field(min_length=1)
relationship: Relationship
language_strength: LanguageStrength = LanguageStrength.ASSOCIATIONAL
coefficient: float | None = None
significance: str | None = None
evidence_ids: list[str] = Field(min_length=1)
class GroundedMechanism(DomainModel):
mechanism_id: str = Field(min_length=1)
statement: str = Field(min_length=1)
variables: list[str] = Field(default_factory=list)
evidence_ids: list[str] = Field(min_length=1)
class GroundedTestResult(DomainModel):
test_id: str = Field(min_length=1)
name: str = Field(min_length=1)
result: str = Field(min_length=1)
passed: bool | None = None
evidence_ids: list[str] = Field(min_length=1)
class GroundedLimitation(DomainModel):
limitation_id: str = Field(min_length=1)
statement: str = Field(min_length=1)
origin: LimitationOrigin
evidence_ids: list[str] = Field(default_factory=list)
@model_validator(mode="after")
def require_evidence_for_paper_origin(self) -> GroundedLimitation:
if self.origin is LimitationOrigin.PAPER and not self.evidence_ids:
raise ValueError("paper-reported limitations require evidence IDs")
return self
class GroundedPaperRecord(DomainModel):
"""Source-derived facts only; analysis and suggestions live outside this model."""
schema_version: Literal["0.3"] = "0.3"
paper_id: str = Field(min_length=1)
record_type: RecordType = RecordType.EMPIRICAL
metadata: PaperMetadata
research_questions: list[str] = Field(min_length=1, max_length=3)
theoretical_framework: Theory
data: DataDescription
variables: list[Variable] = Field(default_factory=list)
empirical_design: EmpiricalDesign | None = None
source_claims: list[GroundedSourceClaim] = Field(min_length=1)
findings: list[GroundedFinding] = Field(default_factory=list)
mechanisms: list[GroundedMechanism] = Field(default_factory=list)
heterogeneity: list[GroundedFinding] = Field(default_factory=list)
robustness: list[GroundedTestResult] = Field(default_factory=list)
limitations: list[GroundedLimitation] = Field(default_factory=list)
field_evidence: dict[str, list[str]] = Field(default_factory=dict)
@model_validator(mode="after")
def require_empirical_content(self) -> GroundedPaperRecord:
if self.record_type is RecordType.EMPIRICAL:
if self.empirical_design is None:
raise ValueError("empirical records require empirical_design")
if not self.findings:
raise ValueError("empirical records require at least one finding")
return self
def referenced_evidence_ids(self) -> set[str]:
result = {item for values in self.field_evidence.values() for item in values}
result.update(
item for claim in self.source_claims for item in claim.evidence_ids
)
result.update(
item for finding in self.findings for item in finding.evidence_ids
)
result.update(
item for finding in self.heterogeneity for item in finding.evidence_ids
)
result.update(
item for mechanism in self.mechanisms for item in mechanism.evidence_ids
)
result.update(item for test in self.robustness for item in test.evidence_ids)
result.update(
item for limitation in self.limitations for item in limitation.evidence_ids
)
return result
class ResearchAssessment(DomainModel):
assessment_id: str = Field(min_length=1)
type: AssessmentType
statement: str = Field(min_length=1)
origin: AssessmentOrigin
basis_evidence_ids: list[str] = Field(default_factory=list)
caveats: list[str] = Field(default_factory=list)
class AnalyzedResearchExtension(DomainModel):
extension_id: str = Field(min_length=1)
title: str = Field(min_length=1)
gap_type: GapType
research_question: str = Field(min_length=1)
supporting_evidence_ids: list[str] = Field(default_factory=list)
identification_strategy: str | None = None
sample: str | None = None
data_sources: list[str] = Field(default_factory=list)
variables: list[str] = Field(default_factory=list)
mechanism_test: str | None = None
robustness: list[str] = Field(default_factory=list)
falsification: str | None = None
expected_contribution: str | None = None
assumptions: list[str] = Field(default_factory=list)
@model_validator(mode="after")
def require_executable_design(self) -> AnalyzedResearchExtension:
if not (
self.identification_strategy
or self.sample
or self.data_sources
or self.variables
or self.mechanism_test
or self.falsification
):
raise ValueError("research extension requires an executable design element")
return self
class ResearchAnalysis(DomainModel):
assessments: list[ResearchAssessment] = Field(default_factory=list)
research_extensions: list[AnalyzedResearchExtension] = Field(default_factory=list)
def referenced_evidence_ids(self) -> set[str]:
result = {
item
for assessment in self.assessments
for item in assessment.basis_evidence_ids
}
result.update(
item
for extension in self.research_extensions
for item in extension.supporting_evidence_ids
)
return result
class AuditCheck(DomainModel):
code: str = Field(min_length=1)
status: AuditStatus
message: str = Field(min_length=1)
evidence_ids: list[str] = Field(default_factory=list)
severity: str = "info"
class MethodAuditReport(DomainModel):
method: str = Field(min_length=1)
causal_support: CausalSupport
checks: list[AuditCheck] = Field(default_factory=list)
auditor_version: str = "0.3.1"
def referenced_evidence_ids(self) -> set[str]:
return {item for check in self.checks for item in check.evidence_ids}
class RunManifest(DomainModel):
run_id: str = Field(min_length=1)
pipeline_version: str = "0.3.1"
source_sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
canonical_text_sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
config_hash: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
provider: str | None = None
provider_version: str | None = None
model: str | None = None
prompt_versions: dict[str, str] = Field(default_factory=dict)
created_at: datetime
@model_validator(mode="after")
def require_timezone(self) -> RunManifest:
if self.created_at.tzinfo is None or self.created_at.utcoffset() is None:
raise ValueError("created_at must include a timezone")
if self.model and not self.provider:
raise ValueError("model requires a provider")
if self.provider_version and not self.provider:
raise ValueError("provider_version requires a provider")
return self
class PaperPackage(DomainModel):
"""Canonical v0.3 research asset with normalized evidence references."""
schema_version: Literal["0.3"] = "0.3"
state: PackageState
document: DocumentManifest
record: GroundedPaperRecord
evidence_index: dict[str, EvidenceSpan] = Field(min_length=1)
analysis: ResearchAnalysis | None = None
audit: MethodAuditReport | None = None
run: RunManifest
migration_notes: list[str] = Field(default_factory=list)
@model_validator(mode="after")
def validate_evidence_graph(self) -> PaperPackage:
if self.document.source_id != self.record.metadata.source_id:
raise ValueError("document and metadata source_id values must match")
for key, evidence in self.evidence_index.items():
if key != evidence.evidence_id:
raise ValueError("evidence_index key must equal evidence_id")
if evidence.source_id != self.document.source_id:
raise ValueError("evidence source_id must match document source_id")
referenced = self.record.referenced_evidence_ids()
if self.analysis:
referenced.update(self.analysis.referenced_evidence_ids())
if self.audit:
referenced.update(self.audit.referenced_evidence_ids())
missing = sorted(referenced - set(self.evidence_index))
if missing:
raise ValueError(f"unresolved evidence IDs: {', '.join(missing)}")
if self.state is PackageState.VERIFIED and any(
evidence.verification is None
or evidence.verification.status is not VerificationStatus.VERIFIED
for evidence in self.evidence_index.values()
):
raise ValueError(
"verified packages require every evidence span to be fully verified"
)
if self.state is PackageState.AUDITED and self.audit is None:
raise ValueError("audited packages require an audit report")
if self.document.sha256 and self.run.source_sha256:
if self.document.sha256 != self.run.source_sha256:
raise ValueError("run source_sha256 must match document sha256")
elif self.document.sha256 and self.state is not PackageState.MIGRATED:
raise ValueError(
"finalized, verified, and audited packages require run source_sha256"
)
return self