simjecture 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conjecture_solver/__init__.py +10 -0
- conjecture_solver/__main__.py +4 -0
- conjecture_solver/action_handlers.py +459 -0
- conjecture_solver/adapters/__init__.py +7 -0
- conjecture_solver/adapters/base.py +96 -0
- conjecture_solver/adapters/fake.py +155 -0
- conjecture_solver/adapters/pic.py +196 -0
- conjecture_solver/adapters/warpx.py +1375 -0
- conjecture_solver/autonomous_research.py +1071 -0
- conjecture_solver/benchmarks/__init__.py +6 -0
- conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
- conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
- conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
- conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
- conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
- conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
- conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
- conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
- conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
- conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
- conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
- conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
- conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
- conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
- conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
- conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
- conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
- conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
- conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
- conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
- conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
- conjecture_solver/campaign.py +406 -0
- conjecture_solver/cli.py +963 -0
- conjecture_solver/confirmation.py +287 -0
- conjecture_solver/control.py +234 -0
- conjecture_solver/deployment.py +874 -0
- conjecture_solver/discovery.py +68 -0
- conjecture_solver/domains/__init__.py +40 -0
- conjecture_solver/domains/base.py +157 -0
- conjecture_solver/domains/kinetic_sufficiency.py +521 -0
- conjecture_solver/ledger.py +238 -0
- conjecture_solver/lifecycle.py +131 -0
- conjecture_solver/literature.py +494 -0
- conjecture_solver/llm.py +228 -0
- conjecture_solver/models.py +279 -0
- conjecture_solver/mvp_agent.py +3212 -0
- conjecture_solver/mvp_claims.py +1279 -0
- conjecture_solver/mvp_control.py +322 -0
- conjecture_solver/mvp_guidance.py +172 -0
- conjecture_solver/mvp_launch.py +1296 -0
- conjecture_solver/mvp_monitor.py +1487 -0
- conjecture_solver/mvp_skills.py +538 -0
- conjecture_solver/orchestration.py +578 -0
- conjecture_solver/outbox.py +526 -0
- conjecture_solver/parameters.py +394 -0
- conjecture_solver/proposals.py +427 -0
- conjecture_solver/research_tools.py +278 -0
- conjecture_solver/schema_export.py +246 -0
- conjecture_solver/search.py +1047 -0
- conjecture_solver/semantics.py +146 -0
- conjecture_solver/tui/__init__.py +7 -0
- conjecture_solver/tui/app.py +202 -0
- conjecture_solver/tui/claim_views.py +213 -0
- conjecture_solver/tui/screens.py +1014 -0
- conjecture_solver/warpx_analysis.py +184 -0
- conjecture_solver/warpx_campaign.py +148 -0
- conjecture_solver/warpx_confirmation.py +617 -0
- simjecture-0.1.0.dist-info/METADATA +400 -0
- simjecture-0.1.0.dist-info/RECORD +75 -0
- simjecture-0.1.0.dist-info/WHEEL +4 -0
- simjecture-0.1.0.dist-info/entry_points.txt +4 -0
- simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
- simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
"""Domain-neutral working-claim ledger for the natural-language MVP.
|
|
2
|
+
|
|
3
|
+
This is not a domain-specific daughter schema. Claims are free-text scientific,
|
|
4
|
+
instrument, diagnostic, or control statements with lineage and evidence links.
|
|
5
|
+
The agent still designs all physics and experiments; the ledger only forces
|
|
6
|
+
identity, parent relation, and disposition so audits do not depend on scanning
|
|
7
|
+
the full transcript.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
from pathlib import Path, PurePosixPath
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
|
|
20
|
+
from pydantic import Field, model_validator
|
|
21
|
+
|
|
22
|
+
from .models import StrictModel
|
|
23
|
+
|
|
24
|
+
CLAIM_ID_PATTERN = re.compile(r"^claim_[a-z0-9_]+$")
|
|
25
|
+
ROOT_CLAIM_ID = "claim_root"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClaimKind(StrEnum):
|
|
29
|
+
SCIENTIFIC = "scientific"
|
|
30
|
+
INSTRUMENT = "instrument"
|
|
31
|
+
DIAGNOSTIC = "diagnostic"
|
|
32
|
+
CONTROL = "control"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClaimRelation(StrEnum):
|
|
36
|
+
ROOT = "root"
|
|
37
|
+
REFINES = "refines"
|
|
38
|
+
ALTERNATE = "alternate"
|
|
39
|
+
DIAGNOSTIC_OF = "diagnostic_of"
|
|
40
|
+
INSTRUMENT_OF = "instrument_of"
|
|
41
|
+
CONTROL_FOR = "control_for"
|
|
42
|
+
SUCCEEDS = "succeeds"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ClaimDisposition(StrEnum):
|
|
46
|
+
OPEN = "open"
|
|
47
|
+
SUPPORTED = "supported"
|
|
48
|
+
WEAKENED = "weakened"
|
|
49
|
+
FALSIFIED = "falsified"
|
|
50
|
+
SUPERSEDED = "superseded"
|
|
51
|
+
UNRESOLVED = "unresolved"
|
|
52
|
+
INSTRUMENT_LIMITED = "instrument_limited"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CommissioningAspect(StrEnum):
|
|
56
|
+
"""Domain-neutral aspects required before a scientific capability run."""
|
|
57
|
+
|
|
58
|
+
INTERFACE = "interface"
|
|
59
|
+
REPRESENTATION = "representation"
|
|
60
|
+
PHYSICS_CONTROLS = "physics_controls"
|
|
61
|
+
BOUNDARIES = "boundaries"
|
|
62
|
+
DIAGNOSTICS = "diagnostics"
|
|
63
|
+
NUMERICAL_REGIME = "numerical_regime"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
REQUIRED_SCIENTIFIC_COMMISSIONING_ASPECTS = frozenset(
|
|
67
|
+
{
|
|
68
|
+
CommissioningAspect.REPRESENTATION,
|
|
69
|
+
CommissioningAspect.PHYSICS_CONTROLS,
|
|
70
|
+
CommissioningAspect.BOUNDARIES,
|
|
71
|
+
CommissioningAspect.DIAGNOSTICS,
|
|
72
|
+
CommissioningAspect.NUMERICAL_REGIME,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validation_scalars_equal(actual: Any, expected: Any) -> bool:
|
|
78
|
+
"""Compare JSON scalars without treating booleans as numeric values."""
|
|
79
|
+
|
|
80
|
+
if isinstance(actual, bool) or isinstance(expected, bool):
|
|
81
|
+
return type(actual) is type(expected) and actual == expected
|
|
82
|
+
if isinstance(actual, (int, float)) and isinstance(expected, (int, float)):
|
|
83
|
+
return (
|
|
84
|
+
math.isfinite(actual)
|
|
85
|
+
and math.isfinite(expected)
|
|
86
|
+
and actual == expected
|
|
87
|
+
)
|
|
88
|
+
return type(actual) is type(expected) and actual == expected
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ClaimEvidenceValidationCheck(StrictModel):
|
|
92
|
+
"""One exact assertion over a JSON evidence summary."""
|
|
93
|
+
|
|
94
|
+
aspect: CommissioningAspect | None = Field(
|
|
95
|
+
default=None,
|
|
96
|
+
description=(
|
|
97
|
+
"Commissioning aspect tested by this assertion. Instrument evidence "
|
|
98
|
+
"used to unlock scientific capability execution must cover every required "
|
|
99
|
+
"aspect in one prospectively registered contract."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
json_path: str = Field(
|
|
103
|
+
pattern=r"^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$",
|
|
104
|
+
description="Dot-separated object keys; array indexing is deliberately unsupported",
|
|
105
|
+
)
|
|
106
|
+
expected_value: str | int | float | bool | None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ClaimEvidenceValidationResult(StrictModel):
|
|
110
|
+
"""Deterministic evaluation of one prospective JSON assertion."""
|
|
111
|
+
|
|
112
|
+
aspect: CommissioningAspect | None = None
|
|
113
|
+
json_path: str
|
|
114
|
+
expected_value: str | int | float | bool | None
|
|
115
|
+
actual_value: str | int | float | bool | None = None
|
|
116
|
+
passed: bool
|
|
117
|
+
error: str | None = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ClaimExecutionBinding(StrictModel):
|
|
121
|
+
"""Prospective command identity for commissioning and later science runs."""
|
|
122
|
+
|
|
123
|
+
capability: str = Field(pattern=r"^[a-z][a-z0-9_.-]*$")
|
|
124
|
+
program_path: str = Field(min_length=1)
|
|
125
|
+
program_sha256: str | None = Field(
|
|
126
|
+
default=None,
|
|
127
|
+
pattern=r"^[0-9a-f]{64}$",
|
|
128
|
+
description=(
|
|
129
|
+
"Runner-attested source identity sealed before the first bound "
|
|
130
|
+
"capability execution"
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
commissioning_argv: tuple[str, ...] = Field(min_length=1)
|
|
134
|
+
allowed_scientific_argv: tuple[tuple[str, ...], ...] = ()
|
|
135
|
+
|
|
136
|
+
@model_validator(mode="after")
|
|
137
|
+
def paths_and_commands_are_consistent(self) -> ClaimExecutionBinding:
|
|
138
|
+
path = PurePosixPath(self.program_path)
|
|
139
|
+
if (
|
|
140
|
+
path.is_absolute()
|
|
141
|
+
or self.program_path in {"", "."}
|
|
142
|
+
or ".." in path.parts
|
|
143
|
+
or "\x00" in self.program_path
|
|
144
|
+
):
|
|
145
|
+
raise ValueError("execution binding program_path must stay in the workspace")
|
|
146
|
+
commands = (self.commissioning_argv, *self.allowed_scientific_argv)
|
|
147
|
+
for argv in commands:
|
|
148
|
+
if not argv:
|
|
149
|
+
raise ValueError("execution binding commands cannot be empty")
|
|
150
|
+
if argv[0] != self.program_path:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
"every bound command must use program_path as argv[0]"
|
|
153
|
+
)
|
|
154
|
+
if any("\x00" in item for item in argv):
|
|
155
|
+
raise ValueError("execution binding arguments cannot contain NUL bytes")
|
|
156
|
+
if len(set(self.allowed_scientific_argv)) != len(
|
|
157
|
+
self.allowed_scientific_argv
|
|
158
|
+
):
|
|
159
|
+
raise ValueError("allowed_scientific_argv cannot contain duplicates")
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def allows_evidence_argv(self, argv: tuple[str, ...], *, instrument: bool) -> bool:
|
|
163
|
+
"""Return whether a prospectively bound command may produce claim evidence."""
|
|
164
|
+
|
|
165
|
+
if instrument:
|
|
166
|
+
return argv == self.commissioning_argv
|
|
167
|
+
return argv in self.allowed_scientific_argv
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ClaimEvidenceContract(StrictModel):
|
|
171
|
+
"""Prospective, domain-neutral rule for interpreting evidence for one claim."""
|
|
172
|
+
|
|
173
|
+
version: int = Field(ge=1)
|
|
174
|
+
observable: str = Field(min_length=8)
|
|
175
|
+
expected_outcomes: str = Field(min_length=8)
|
|
176
|
+
decision_rule: str = Field(min_length=8)
|
|
177
|
+
required_observation: str = Field(min_length=8)
|
|
178
|
+
uncertainty_criterion: str = Field(min_length=8)
|
|
179
|
+
inconclusive_conditions: str = Field(min_length=8)
|
|
180
|
+
validation_checks: tuple[ClaimEvidenceValidationCheck, ...] = ()
|
|
181
|
+
execution_binding: ClaimExecutionBinding | None = None
|
|
182
|
+
additional_execution_bindings: tuple[ClaimExecutionBinding, ...] = ()
|
|
183
|
+
registered_iteration: int = Field(ge=0)
|
|
184
|
+
|
|
185
|
+
@model_validator(mode="after")
|
|
186
|
+
def execution_pipeline_is_unambiguous(self) -> ClaimEvidenceContract:
|
|
187
|
+
if self.additional_execution_bindings and self.execution_binding is None:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"additional_execution_bindings require a primary execution_binding"
|
|
190
|
+
)
|
|
191
|
+
seen_commands: set[tuple[str, tuple[str, ...]]] = set()
|
|
192
|
+
for binding in self.all_execution_bindings():
|
|
193
|
+
for argv in binding.allowed_scientific_argv:
|
|
194
|
+
identity = (binding.capability, argv)
|
|
195
|
+
if identity in seen_commands:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"scientific execution commands cannot be duplicated across "
|
|
198
|
+
"execution bindings"
|
|
199
|
+
)
|
|
200
|
+
seen_commands.add(identity)
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def all_execution_bindings(self) -> tuple[ClaimExecutionBinding, ...]:
|
|
204
|
+
"""Return the primary and any prospectively declared pipeline stages."""
|
|
205
|
+
|
|
206
|
+
if self.execution_binding is None:
|
|
207
|
+
return ()
|
|
208
|
+
return (self.execution_binding, *self.additional_execution_bindings)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ClaimEvidenceProvenance(StrictModel):
|
|
212
|
+
"""Immutable identity and generating-action metadata for a workspace artifact."""
|
|
213
|
+
|
|
214
|
+
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
215
|
+
bytes: int = Field(ge=0)
|
|
216
|
+
tracked: bool
|
|
217
|
+
generated_iteration: int | None = Field(default=None, ge=0)
|
|
218
|
+
action: str | None = None
|
|
219
|
+
action_sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
|
|
220
|
+
command_argv: tuple[str, ...] = ()
|
|
221
|
+
capability: str | None = None
|
|
222
|
+
program_path: str | None = None
|
|
223
|
+
program_sha256: str | None = Field(
|
|
224
|
+
default=None,
|
|
225
|
+
pattern=r"^[0-9a-f]{64}$",
|
|
226
|
+
)
|
|
227
|
+
execution_succeeded: bool | None = None
|
|
228
|
+
execution_returncode: int | None = None
|
|
229
|
+
execution_timed_out: bool | None = None
|
|
230
|
+
execution_workspace_exceeded: bool | None = None
|
|
231
|
+
execution_stage: Literal["workbench", "evidence"] | None = None
|
|
232
|
+
evidence_eligible: bool = True
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ClaimEvidenceLink(StrictModel):
|
|
236
|
+
path: str = Field(min_length=1)
|
|
237
|
+
note: str = Field(min_length=1)
|
|
238
|
+
iteration: int = Field(ge=0)
|
|
239
|
+
contract_version: int | None = Field(default=None, ge=1)
|
|
240
|
+
observation_sufficient: bool | None = None
|
|
241
|
+
observation_note: str | None = None
|
|
242
|
+
validation_passed: bool | None = None
|
|
243
|
+
validation_results: tuple[ClaimEvidenceValidationResult, ...] = ()
|
|
244
|
+
commissioning_claim_id: str | None = Field(
|
|
245
|
+
default=None,
|
|
246
|
+
pattern=r"^claim_[a-z0-9_]+$",
|
|
247
|
+
)
|
|
248
|
+
provenance: ClaimEvidenceProvenance | None = None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class MVPClaim(StrictModel):
|
|
252
|
+
id: str = Field(pattern=r"^claim_[a-z0-9_]+$")
|
|
253
|
+
statement: str = Field(min_length=8)
|
|
254
|
+
kind: ClaimKind
|
|
255
|
+
relation: ClaimRelation
|
|
256
|
+
parent_id: str | None = None
|
|
257
|
+
status: ClaimDisposition = ClaimDisposition.OPEN
|
|
258
|
+
rationale: str = Field(min_length=8)
|
|
259
|
+
evidence_contracts: tuple[ClaimEvidenceContract, ...] = ()
|
|
260
|
+
evidence: tuple[ClaimEvidenceLink, ...] = ()
|
|
261
|
+
closed_reason: str | None = None
|
|
262
|
+
created_iteration: int = Field(ge=0)
|
|
263
|
+
updated_iteration: int = Field(ge=0)
|
|
264
|
+
|
|
265
|
+
@model_validator(mode="after")
|
|
266
|
+
def relation_and_parent_are_consistent(self) -> MVPClaim:
|
|
267
|
+
if self.relation == ClaimRelation.ROOT:
|
|
268
|
+
if self.id != ROOT_CLAIM_ID:
|
|
269
|
+
raise ValueError("only claim_root may use relation=root")
|
|
270
|
+
if self.parent_id is not None:
|
|
271
|
+
raise ValueError("root claim cannot have a parent_id")
|
|
272
|
+
else:
|
|
273
|
+
if self.parent_id is None:
|
|
274
|
+
raise ValueError("non-root claims require parent_id")
|
|
275
|
+
if not CLAIM_ID_PATTERN.match(self.parent_id):
|
|
276
|
+
raise ValueError("parent_id must match claim_[a-z0-9_]+")
|
|
277
|
+
if self.status != ClaimDisposition.OPEN and (
|
|
278
|
+
self.closed_reason is None or not self.closed_reason.strip()
|
|
279
|
+
):
|
|
280
|
+
raise ValueError("closed claims require closed_reason")
|
|
281
|
+
if self.status == ClaimDisposition.OPEN and self.closed_reason is not None:
|
|
282
|
+
raise ValueError("open claims cannot carry closed_reason")
|
|
283
|
+
return self
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class MVPClaimLedger(StrictModel):
|
|
287
|
+
schema_version: Literal[
|
|
288
|
+
"0.1.0",
|
|
289
|
+
"0.2.0",
|
|
290
|
+
"0.3.0",
|
|
291
|
+
"0.4.0",
|
|
292
|
+
"0.5.0",
|
|
293
|
+
"0.6.0",
|
|
294
|
+
"0.7.0",
|
|
295
|
+
"0.8.0",
|
|
296
|
+
] = (
|
|
297
|
+
"0.8.0"
|
|
298
|
+
)
|
|
299
|
+
root_hypothesis: str = Field(min_length=1)
|
|
300
|
+
claims: tuple[MVPClaim, ...] = ()
|
|
301
|
+
|
|
302
|
+
def by_id(self) -> dict[str, MVPClaim]:
|
|
303
|
+
return {claim.id: claim for claim in self.claims}
|
|
304
|
+
|
|
305
|
+
def open_claims(self) -> tuple[MVPClaim, ...]:
|
|
306
|
+
return tuple(
|
|
307
|
+
claim for claim in self.claims if claim.status == ClaimDisposition.OPEN
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def compact_summary(self, *, max_claims: int = 24) -> dict[str, Any]:
|
|
311
|
+
"""Short ledger view safe to inject into tool results / context."""
|
|
312
|
+
ordered = sorted(self.claims, key=lambda claim: claim.updated_iteration)
|
|
313
|
+
tail = ordered[-max_claims:]
|
|
314
|
+
return {
|
|
315
|
+
"schema_version": self.schema_version,
|
|
316
|
+
"root_hypothesis": self.root_hypothesis,
|
|
317
|
+
"claim_count": len(self.claims),
|
|
318
|
+
"open_count": len(self.open_claims()),
|
|
319
|
+
"claims": [
|
|
320
|
+
{
|
|
321
|
+
"id": claim.id,
|
|
322
|
+
"kind": claim.kind.value,
|
|
323
|
+
"relation": claim.relation.value,
|
|
324
|
+
"parent_id": claim.parent_id,
|
|
325
|
+
"status": claim.status.value,
|
|
326
|
+
"statement": claim.statement,
|
|
327
|
+
"evidence_contract_count": len(claim.evidence_contracts),
|
|
328
|
+
"evidence_count": len(claim.evidence),
|
|
329
|
+
}
|
|
330
|
+
for claim in tail
|
|
331
|
+
],
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class MVPClaimLedgerStore:
|
|
336
|
+
"""Durable claim ledger beside the MVP report and transcript."""
|
|
337
|
+
|
|
338
|
+
def __init__(self, path: str | Path, *, root_hypothesis: str) -> None:
|
|
339
|
+
self.path = Path(path).resolve()
|
|
340
|
+
self.root_hypothesis = root_hypothesis.strip()
|
|
341
|
+
if not self.root_hypothesis:
|
|
342
|
+
raise ValueError("root hypothesis cannot be empty")
|
|
343
|
+
self._ledger = self._load_or_create()
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def ledger(self) -> MVPClaimLedger:
|
|
347
|
+
return self._ledger
|
|
348
|
+
|
|
349
|
+
def _load_or_create(self) -> MVPClaimLedger:
|
|
350
|
+
if self.path.exists():
|
|
351
|
+
ledger = MVPClaimLedger.model_validate_json(self.path.read_text())
|
|
352
|
+
if ledger.root_hypothesis != self.root_hypothesis:
|
|
353
|
+
raise ValueError("claim ledger belongs to a different root hypothesis")
|
|
354
|
+
return ledger
|
|
355
|
+
root = MVPClaim(
|
|
356
|
+
id=ROOT_CLAIM_ID,
|
|
357
|
+
statement=self.root_hypothesis,
|
|
358
|
+
kind=ClaimKind.SCIENTIFIC,
|
|
359
|
+
relation=ClaimRelation.ROOT,
|
|
360
|
+
parent_id=None,
|
|
361
|
+
status=ClaimDisposition.OPEN,
|
|
362
|
+
rationale="Immutable root hypothesis supplied by the campaign operator.",
|
|
363
|
+
evidence_contracts=(),
|
|
364
|
+
evidence=(),
|
|
365
|
+
closed_reason=None,
|
|
366
|
+
created_iteration=0,
|
|
367
|
+
updated_iteration=0,
|
|
368
|
+
)
|
|
369
|
+
ledger = MVPClaimLedger(root_hypothesis=self.root_hypothesis, claims=(root,))
|
|
370
|
+
self._persist(ledger)
|
|
371
|
+
return ledger
|
|
372
|
+
|
|
373
|
+
def _persist(self, ledger: MVPClaimLedger) -> None:
|
|
374
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
375
|
+
temporary = self.path.with_suffix(self.path.suffix + ".tmp")
|
|
376
|
+
temporary.write_text(ledger.model_dump_json(indent=2) + "\n")
|
|
377
|
+
os.replace(temporary, self.path)
|
|
378
|
+
|
|
379
|
+
def _replace_claim(self, claim: MVPClaim) -> MVPClaimLedger:
|
|
380
|
+
claims = [existing for existing in self._ledger.claims if existing.id != claim.id]
|
|
381
|
+
claims.append(claim)
|
|
382
|
+
claims.sort(key=lambda item: (item.created_iteration, item.id))
|
|
383
|
+
self._ledger = MVPClaimLedger(
|
|
384
|
+
root_hypothesis=self.root_hypothesis,
|
|
385
|
+
claims=tuple(claims),
|
|
386
|
+
)
|
|
387
|
+
self._persist(self._ledger)
|
|
388
|
+
return self._ledger
|
|
389
|
+
|
|
390
|
+
def register(
|
|
391
|
+
self,
|
|
392
|
+
*,
|
|
393
|
+
claim_id: str,
|
|
394
|
+
statement: str,
|
|
395
|
+
kind: ClaimKind,
|
|
396
|
+
relation: ClaimRelation,
|
|
397
|
+
parent_id: str,
|
|
398
|
+
rationale: str,
|
|
399
|
+
iteration: int,
|
|
400
|
+
) -> dict[str, Any]:
|
|
401
|
+
claim_id = claim_id.strip().casefold()
|
|
402
|
+
parent_id = parent_id.strip().casefold()
|
|
403
|
+
if relation == ClaimRelation.ROOT:
|
|
404
|
+
raise ValueError("cannot re-register the root claim")
|
|
405
|
+
if claim_id == ROOT_CLAIM_ID:
|
|
406
|
+
raise ValueError("claim_root is reserved")
|
|
407
|
+
if not CLAIM_ID_PATTERN.match(claim_id):
|
|
408
|
+
raise ValueError("claim_id must match claim_[a-z0-9_]+")
|
|
409
|
+
existing = self._ledger.by_id()
|
|
410
|
+
if claim_id in existing:
|
|
411
|
+
raise ValueError(f"claim_id already exists: {claim_id}")
|
|
412
|
+
if parent_id not in existing:
|
|
413
|
+
raise ValueError(f"unknown parent_id: {parent_id}")
|
|
414
|
+
parent = existing[parent_id]
|
|
415
|
+
relation_kinds = {
|
|
416
|
+
ClaimRelation.INSTRUMENT_OF: ClaimKind.INSTRUMENT,
|
|
417
|
+
ClaimRelation.DIAGNOSTIC_OF: ClaimKind.DIAGNOSTIC,
|
|
418
|
+
ClaimRelation.CONTROL_FOR: ClaimKind.CONTROL,
|
|
419
|
+
}
|
|
420
|
+
expected_kind = relation_kinds.get(relation)
|
|
421
|
+
if expected_kind is not None:
|
|
422
|
+
if kind != expected_kind or parent.kind != ClaimKind.SCIENTIFIC:
|
|
423
|
+
if relation == ClaimRelation.CONTROL_FOR:
|
|
424
|
+
raise ValueError(
|
|
425
|
+
f"relation={relation.value} requires a "
|
|
426
|
+
f"{expected_kind.value} claim whose parent is scientific "
|
|
427
|
+
f"(got kind={kind.value}, parent_kind={parent.kind.value}); "
|
|
428
|
+
"for non-scientific exact-reuse smokes use control_for "
|
|
429
|
+
"with parent_id of a scientific claim such as claim_root, "
|
|
430
|
+
"not an instrument claim"
|
|
431
|
+
)
|
|
432
|
+
raise ValueError(
|
|
433
|
+
f"relation={relation.value} requires a {expected_kind.value} "
|
|
434
|
+
"claim whose parent is scientific"
|
|
435
|
+
)
|
|
436
|
+
elif relation in {ClaimRelation.REFINES, ClaimRelation.ALTERNATE}:
|
|
437
|
+
if kind != ClaimKind.SCIENTIFIC or parent.kind != ClaimKind.SCIENTIFIC:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"relation={relation.value} requires scientific child and parent"
|
|
440
|
+
)
|
|
441
|
+
elif relation == ClaimRelation.SUCCEEDS:
|
|
442
|
+
if kind != parent.kind:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
"relation=succeeds requires child and predecessor to have the "
|
|
445
|
+
"same claim kind"
|
|
446
|
+
)
|
|
447
|
+
if parent.status == ClaimDisposition.OPEN:
|
|
448
|
+
raise ValueError(
|
|
449
|
+
"relation=succeeds requires a closed predecessor claim"
|
|
450
|
+
)
|
|
451
|
+
claim = MVPClaim(
|
|
452
|
+
id=claim_id,
|
|
453
|
+
statement=statement,
|
|
454
|
+
kind=kind,
|
|
455
|
+
relation=relation,
|
|
456
|
+
parent_id=parent_id,
|
|
457
|
+
status=ClaimDisposition.OPEN,
|
|
458
|
+
rationale=rationale,
|
|
459
|
+
evidence_contracts=(),
|
|
460
|
+
evidence=(),
|
|
461
|
+
closed_reason=None,
|
|
462
|
+
created_iteration=iteration,
|
|
463
|
+
updated_iteration=iteration,
|
|
464
|
+
)
|
|
465
|
+
ledger = self._replace_claim(claim)
|
|
466
|
+
return {
|
|
467
|
+
"registered": claim.model_dump(mode="json"),
|
|
468
|
+
"claim_ledger": ledger.compact_summary(),
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
def register_evidence_contract(
|
|
472
|
+
self,
|
|
473
|
+
*,
|
|
474
|
+
claim_id: str,
|
|
475
|
+
observable: str,
|
|
476
|
+
expected_outcomes: str,
|
|
477
|
+
decision_rule: str,
|
|
478
|
+
required_observation: str,
|
|
479
|
+
uncertainty_criterion: str,
|
|
480
|
+
inconclusive_conditions: str,
|
|
481
|
+
validation_checks: tuple[ClaimEvidenceValidationCheck, ...] = (),
|
|
482
|
+
execution_binding: ClaimExecutionBinding | None = None,
|
|
483
|
+
additional_execution_bindings: tuple[ClaimExecutionBinding, ...] = (),
|
|
484
|
+
iteration: int,
|
|
485
|
+
) -> dict[str, Any]:
|
|
486
|
+
claim_id = claim_id.strip().casefold()
|
|
487
|
+
existing = self._ledger.by_id()
|
|
488
|
+
if claim_id not in existing:
|
|
489
|
+
raise ValueError(f"unknown claim_id: {claim_id}")
|
|
490
|
+
claim = existing[claim_id]
|
|
491
|
+
if claim.status != ClaimDisposition.OPEN:
|
|
492
|
+
raise ValueError(f"claim is already closed: {claim_id}")
|
|
493
|
+
declared_aspects = {
|
|
494
|
+
check.aspect for check in validation_checks if check.aspect is not None
|
|
495
|
+
}
|
|
496
|
+
is_complete_instrument_contract = (
|
|
497
|
+
claim.kind == ClaimKind.INSTRUMENT
|
|
498
|
+
and claim.relation == ClaimRelation.INSTRUMENT_OF
|
|
499
|
+
and REQUIRED_SCIENTIFIC_COMMISSIONING_ASPECTS.issubset(
|
|
500
|
+
declared_aspects
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
if is_complete_instrument_contract and (
|
|
504
|
+
execution_binding is None
|
|
505
|
+
or not execution_binding.allowed_scientific_argv
|
|
506
|
+
):
|
|
507
|
+
raise ValueError(
|
|
508
|
+
"complete instrument commissioning requires a prospective "
|
|
509
|
+
"execution_binding with at least one exact allowed_scientific_argv "
|
|
510
|
+
"command"
|
|
511
|
+
)
|
|
512
|
+
if claim.kind == ClaimKind.INSTRUMENT and additional_execution_bindings:
|
|
513
|
+
raise ValueError(
|
|
514
|
+
"instrument claims bind one program; register a separate instrument_of "
|
|
515
|
+
"claim for each additional evidence-pipeline program"
|
|
516
|
+
)
|
|
517
|
+
contract = ClaimEvidenceContract(
|
|
518
|
+
version=len(claim.evidence_contracts) + 1,
|
|
519
|
+
observable=observable,
|
|
520
|
+
expected_outcomes=expected_outcomes,
|
|
521
|
+
decision_rule=decision_rule,
|
|
522
|
+
required_observation=required_observation,
|
|
523
|
+
uncertainty_criterion=uncertainty_criterion,
|
|
524
|
+
inconclusive_conditions=inconclusive_conditions,
|
|
525
|
+
validation_checks=validation_checks,
|
|
526
|
+
execution_binding=execution_binding,
|
|
527
|
+
additional_execution_bindings=additional_execution_bindings,
|
|
528
|
+
registered_iteration=iteration,
|
|
529
|
+
)
|
|
530
|
+
updated = MVPClaim(
|
|
531
|
+
id=claim.id,
|
|
532
|
+
statement=claim.statement,
|
|
533
|
+
kind=claim.kind,
|
|
534
|
+
relation=claim.relation,
|
|
535
|
+
parent_id=claim.parent_id,
|
|
536
|
+
status=claim.status,
|
|
537
|
+
rationale=claim.rationale,
|
|
538
|
+
evidence_contracts=claim.evidence_contracts + (contract,),
|
|
539
|
+
evidence=claim.evidence,
|
|
540
|
+
closed_reason=claim.closed_reason,
|
|
541
|
+
created_iteration=claim.created_iteration,
|
|
542
|
+
updated_iteration=iteration,
|
|
543
|
+
)
|
|
544
|
+
ledger = self._replace_claim(updated)
|
|
545
|
+
result: dict[str, Any] = {
|
|
546
|
+
"registered_evidence_contract": contract.model_dump(mode="json"),
|
|
547
|
+
"claim_ledger": ledger.compact_summary(),
|
|
548
|
+
}
|
|
549
|
+
if claim.evidence:
|
|
550
|
+
result["adaptive_contract_warning"] = (
|
|
551
|
+
"This contract was registered after evidence was already linked. "
|
|
552
|
+
"Only evidence generated under this contract can justify a new "
|
|
553
|
+
"supported or falsified disposition."
|
|
554
|
+
)
|
|
555
|
+
return result
|
|
556
|
+
|
|
557
|
+
@staticmethod
|
|
558
|
+
def _evaluate_validation_checks(
|
|
559
|
+
checks: tuple[ClaimEvidenceValidationCheck, ...],
|
|
560
|
+
*,
|
|
561
|
+
evidence_document: Any | None,
|
|
562
|
+
evidence_document_error: str | None,
|
|
563
|
+
) -> tuple[bool | None, tuple[ClaimEvidenceValidationResult, ...]]:
|
|
564
|
+
if not checks:
|
|
565
|
+
return None, ()
|
|
566
|
+
results: list[ClaimEvidenceValidationResult] = []
|
|
567
|
+
for check in checks:
|
|
568
|
+
actual: Any = evidence_document
|
|
569
|
+
error = evidence_document_error
|
|
570
|
+
if error is None:
|
|
571
|
+
for key in check.json_path.split("."):
|
|
572
|
+
if not isinstance(actual, dict) or key not in actual:
|
|
573
|
+
error = f"JSON path is missing: {check.json_path}"
|
|
574
|
+
actual = None
|
|
575
|
+
break
|
|
576
|
+
actual = actual[key]
|
|
577
|
+
if error is None and not isinstance(
|
|
578
|
+
actual,
|
|
579
|
+
(str, int, float, bool, type(None)),
|
|
580
|
+
):
|
|
581
|
+
error = f"JSON path is not a scalar: {check.json_path}"
|
|
582
|
+
actual = None
|
|
583
|
+
passed = error is None and _validation_scalars_equal(
|
|
584
|
+
actual, check.expected_value
|
|
585
|
+
)
|
|
586
|
+
results.append(
|
|
587
|
+
ClaimEvidenceValidationResult(
|
|
588
|
+
aspect=check.aspect,
|
|
589
|
+
json_path=check.json_path,
|
|
590
|
+
expected_value=check.expected_value,
|
|
591
|
+
actual_value=actual,
|
|
592
|
+
passed=passed,
|
|
593
|
+
error=error,
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
frozen = tuple(results)
|
|
597
|
+
return all(result.passed for result in frozen), frozen
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def _qualifying_contract_evidence(claim: MVPClaim) -> tuple[ClaimEvidenceLink, ...]:
|
|
601
|
+
contracts = {contract.version: contract for contract in claim.evidence_contracts}
|
|
602
|
+
qualifying: list[ClaimEvidenceLink] = []
|
|
603
|
+
for evidence in claim.evidence:
|
|
604
|
+
contract = contracts.get(evidence.contract_version)
|
|
605
|
+
provenance = evidence.provenance
|
|
606
|
+
if (
|
|
607
|
+
contract is None
|
|
608
|
+
or evidence.observation_sufficient is not True
|
|
609
|
+
or provenance is None
|
|
610
|
+
or not provenance.tracked
|
|
611
|
+
or provenance.generated_iteration is None
|
|
612
|
+
or provenance.generated_iteration < contract.registered_iteration
|
|
613
|
+
):
|
|
614
|
+
continue
|
|
615
|
+
if (
|
|
616
|
+
claim.kind == ClaimKind.INSTRUMENT
|
|
617
|
+
and provenance.capability is not None
|
|
618
|
+
and provenance.execution_succeeded is not True
|
|
619
|
+
):
|
|
620
|
+
continue
|
|
621
|
+
if contract.validation_checks and evidence.validation_passed is not True:
|
|
622
|
+
continue
|
|
623
|
+
qualifying.append(evidence)
|
|
624
|
+
return tuple(qualifying)
|
|
625
|
+
|
|
626
|
+
def _validate_commissioning_claim(
|
|
627
|
+
self,
|
|
628
|
+
*,
|
|
629
|
+
scientific_claim: MVPClaim,
|
|
630
|
+
commissioning_claim_id: str | None,
|
|
631
|
+
scientific_provenance: ClaimEvidenceProvenance,
|
|
632
|
+
) -> str:
|
|
633
|
+
if commissioning_claim_id is None:
|
|
634
|
+
raise ValueError(
|
|
635
|
+
f"capability evidence for scientific claim {scientific_claim.id} "
|
|
636
|
+
"requires commissioning_claim_id"
|
|
637
|
+
)
|
|
638
|
+
commissioning_claim_id = commissioning_claim_id.strip().casefold()
|
|
639
|
+
claims = self._ledger.by_id()
|
|
640
|
+
if commissioning_claim_id not in claims:
|
|
641
|
+
raise ValueError(f"unknown commissioning_claim_id: {commissioning_claim_id}")
|
|
642
|
+
commissioning = claims[commissioning_claim_id]
|
|
643
|
+
if (
|
|
644
|
+
commissioning.kind != ClaimKind.INSTRUMENT
|
|
645
|
+
or commissioning.relation != ClaimRelation.INSTRUMENT_OF
|
|
646
|
+
or commissioning.parent_id != scientific_claim.id
|
|
647
|
+
):
|
|
648
|
+
raise ValueError(
|
|
649
|
+
"commissioning claim must be kind=instrument, relation=instrument_of, "
|
|
650
|
+
f"and parent_id={scientific_claim.id}"
|
|
651
|
+
)
|
|
652
|
+
if commissioning.status != ClaimDisposition.SUPPORTED:
|
|
653
|
+
raise ValueError(
|
|
654
|
+
f"commissioning claim {commissioning_claim_id} is not supported"
|
|
655
|
+
)
|
|
656
|
+
if not commissioning.evidence_contracts or not any(
|
|
657
|
+
contract.validation_checks for contract in commissioning.evidence_contracts
|
|
658
|
+
):
|
|
659
|
+
raise ValueError(
|
|
660
|
+
f"commissioning claim {commissioning_claim_id} requires prospective "
|
|
661
|
+
"machine-checkable validation_checks"
|
|
662
|
+
)
|
|
663
|
+
if scientific_provenance.program_sha256 is None:
|
|
664
|
+
raise ValueError(
|
|
665
|
+
"scientific capability evidence requires a stable workspace program "
|
|
666
|
+
"whose source hash can be matched to commissioning"
|
|
667
|
+
)
|
|
668
|
+
generated_iteration = scientific_provenance.generated_iteration
|
|
669
|
+
if generated_iteration is None or commissioning.updated_iteration >= generated_iteration:
|
|
670
|
+
raise ValueError(
|
|
671
|
+
f"commissioning claim {commissioning_claim_id} must be supported before "
|
|
672
|
+
"the scientific artifact is generated"
|
|
673
|
+
)
|
|
674
|
+
contracts = {
|
|
675
|
+
contract.version: contract for contract in commissioning.evidence_contracts
|
|
676
|
+
}
|
|
677
|
+
qualifying: list[ClaimEvidenceLink] = []
|
|
678
|
+
observed_aspects: set[CommissioningAspect] = set()
|
|
679
|
+
complete_same_capability = False
|
|
680
|
+
complete_same_source = False
|
|
681
|
+
complete_binding_present = False
|
|
682
|
+
for evidence in self._qualifying_contract_evidence(commissioning):
|
|
683
|
+
contract = contracts.get(evidence.contract_version)
|
|
684
|
+
if (
|
|
685
|
+
contract is None
|
|
686
|
+
or not contract.validation_checks
|
|
687
|
+
or evidence.validation_passed is not True
|
|
688
|
+
):
|
|
689
|
+
continue
|
|
690
|
+
if scientific_provenance.capability is not None and (
|
|
691
|
+
evidence.provenance is None
|
|
692
|
+
or evidence.provenance.capability != scientific_provenance.capability
|
|
693
|
+
):
|
|
694
|
+
continue
|
|
695
|
+
contract_aspects = {
|
|
696
|
+
check.aspect
|
|
697
|
+
for check in contract.validation_checks
|
|
698
|
+
if check.aspect is not None
|
|
699
|
+
}
|
|
700
|
+
observed_aspects.update(contract_aspects)
|
|
701
|
+
if contract_aspects >= REQUIRED_SCIENTIFIC_COMMISSIONING_ASPECTS:
|
|
702
|
+
complete_same_capability = True
|
|
703
|
+
if (
|
|
704
|
+
evidence.provenance is not None
|
|
705
|
+
and evidence.provenance.program_sha256
|
|
706
|
+
== scientific_provenance.program_sha256
|
|
707
|
+
):
|
|
708
|
+
complete_same_source = True
|
|
709
|
+
binding = contract.execution_binding
|
|
710
|
+
if (
|
|
711
|
+
binding is None
|
|
712
|
+
or not binding.allowed_scientific_argv
|
|
713
|
+
):
|
|
714
|
+
continue
|
|
715
|
+
complete_binding_present = True
|
|
716
|
+
if (
|
|
717
|
+
scientific_provenance.capability != binding.capability
|
|
718
|
+
or scientific_provenance.program_path
|
|
719
|
+
!= binding.program_path
|
|
720
|
+
or (
|
|
721
|
+
binding.program_sha256 is not None
|
|
722
|
+
and scientific_provenance.program_sha256
|
|
723
|
+
!= binding.program_sha256
|
|
724
|
+
)
|
|
725
|
+
or scientific_provenance.command_argv
|
|
726
|
+
not in binding.allowed_scientific_argv
|
|
727
|
+
):
|
|
728
|
+
continue
|
|
729
|
+
qualifying.append(evidence)
|
|
730
|
+
if not qualifying:
|
|
731
|
+
missing_aspects = sorted(
|
|
732
|
+
aspect.value
|
|
733
|
+
for aspect in REQUIRED_SCIENTIFIC_COMMISSIONING_ASPECTS - observed_aspects
|
|
734
|
+
)
|
|
735
|
+
if missing_aspects:
|
|
736
|
+
raise ValueError(
|
|
737
|
+
f"commissioning claim {commissioning_claim_id} is missing required "
|
|
738
|
+
f"machine-checked aspects {missing_aspects}; one qualifying evidence "
|
|
739
|
+
"contract must cover representation, physics_controls, boundaries, "
|
|
740
|
+
"diagnostics, and numerical_regime"
|
|
741
|
+
)
|
|
742
|
+
if complete_same_capability:
|
|
743
|
+
if not complete_same_source:
|
|
744
|
+
raise ValueError(
|
|
745
|
+
f"commissioning claim {commissioning_claim_id} has complete "
|
|
746
|
+
"machine-checked evidence for the capability but not for the "
|
|
747
|
+
"same program source; parameterize and reuse the commissioned "
|
|
748
|
+
"program or recommission the changed source"
|
|
749
|
+
)
|
|
750
|
+
if not complete_binding_present:
|
|
751
|
+
raise ValueError(
|
|
752
|
+
f"commissioning claim {commissioning_claim_id} requires a "
|
|
753
|
+
"prospective execution_binding with at least one exact "
|
|
754
|
+
"allowed_scientific_argv command"
|
|
755
|
+
)
|
|
756
|
+
raise ValueError(
|
|
757
|
+
f"scientific command {list(scientific_provenance.command_argv)!r} "
|
|
758
|
+
f"is outside commissioning claim {commissioning_claim_id}'s "
|
|
759
|
+
"prospectively allowed scientific argv"
|
|
760
|
+
)
|
|
761
|
+
raise ValueError(
|
|
762
|
+
f"commissioning claim {commissioning_claim_id} has no qualifying "
|
|
763
|
+
"complete machine-checked evidence from the scientific capability"
|
|
764
|
+
)
|
|
765
|
+
return commissioning_claim_id
|
|
766
|
+
|
|
767
|
+
@staticmethod
|
|
768
|
+
def _matching_execution_binding(
|
|
769
|
+
contract: ClaimEvidenceContract,
|
|
770
|
+
*,
|
|
771
|
+
capability: str | None,
|
|
772
|
+
program_path: str | None,
|
|
773
|
+
program_sha256: str | None,
|
|
774
|
+
argv: tuple[str, ...],
|
|
775
|
+
instrument: bool,
|
|
776
|
+
) -> tuple[int, ClaimExecutionBinding] | None:
|
|
777
|
+
"""Resolve one exact command within a prospectively declared pipeline."""
|
|
778
|
+
|
|
779
|
+
for index, binding in enumerate(contract.all_execution_bindings()):
|
|
780
|
+
if (
|
|
781
|
+
capability == binding.capability
|
|
782
|
+
and program_path == binding.program_path
|
|
783
|
+
and binding.allows_evidence_argv(argv, instrument=instrument)
|
|
784
|
+
and (
|
|
785
|
+
binding.program_sha256 is None
|
|
786
|
+
or binding.program_sha256 == program_sha256
|
|
787
|
+
)
|
|
788
|
+
):
|
|
789
|
+
return index, binding
|
|
790
|
+
return None
|
|
791
|
+
|
|
792
|
+
def _seal_scientific_execution_binding(
|
|
793
|
+
self,
|
|
794
|
+
*,
|
|
795
|
+
claim: MVPClaim,
|
|
796
|
+
binding_index: int,
|
|
797
|
+
program_sha256: str,
|
|
798
|
+
iteration: int,
|
|
799
|
+
) -> MVPClaim:
|
|
800
|
+
"""Persist the first runner-attested hash for one scientific pipeline stage."""
|
|
801
|
+
|
|
802
|
+
contract = claim.evidence_contracts[-1]
|
|
803
|
+
bindings = list(contract.all_execution_bindings())
|
|
804
|
+
binding = bindings[binding_index]
|
|
805
|
+
if binding.program_sha256 is not None:
|
|
806
|
+
return claim
|
|
807
|
+
bindings[binding_index] = binding.model_copy(
|
|
808
|
+
update={"program_sha256": program_sha256}
|
|
809
|
+
)
|
|
810
|
+
updated_contract = contract.model_copy(
|
|
811
|
+
update={
|
|
812
|
+
"execution_binding": bindings[0],
|
|
813
|
+
"additional_execution_bindings": tuple(bindings[1:]),
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
updated_claim = claim.model_copy(
|
|
817
|
+
update={
|
|
818
|
+
"evidence_contracts": (
|
|
819
|
+
*claim.evidence_contracts[:-1],
|
|
820
|
+
updated_contract,
|
|
821
|
+
),
|
|
822
|
+
"updated_iteration": iteration,
|
|
823
|
+
}
|
|
824
|
+
)
|
|
825
|
+
self._replace_claim(updated_claim)
|
|
826
|
+
return updated_claim
|
|
827
|
+
|
|
828
|
+
def validate_capability_execution(
|
|
829
|
+
self,
|
|
830
|
+
*,
|
|
831
|
+
claim_id: str,
|
|
832
|
+
capability: str,
|
|
833
|
+
argv: tuple[str, ...],
|
|
834
|
+
program_sha256: str | None,
|
|
835
|
+
iteration: int,
|
|
836
|
+
) -> str | None:
|
|
837
|
+
"""Validate stage ordering before a capability can create side effects."""
|
|
838
|
+
claim_id = claim_id.strip().casefold()
|
|
839
|
+
claims = self._ledger.by_id()
|
|
840
|
+
if claim_id not in claims:
|
|
841
|
+
raise ValueError(f"unknown active_claim_id: {claim_id}")
|
|
842
|
+
claim = claims[claim_id]
|
|
843
|
+
if claim.status != ClaimDisposition.OPEN:
|
|
844
|
+
raise ValueError(
|
|
845
|
+
f"active_claim_id {claim_id} is not open ({claim.status.value})"
|
|
846
|
+
)
|
|
847
|
+
if not claim.evidence_contracts:
|
|
848
|
+
raise ValueError(
|
|
849
|
+
f"cannot execute capability {capability!r} for claim {claim_id}: "
|
|
850
|
+
"register a prospective evidence contract on the active claim before "
|
|
851
|
+
"capability execution"
|
|
852
|
+
)
|
|
853
|
+
if claim.evidence_contracts[-1].registered_iteration >= iteration:
|
|
854
|
+
raise ValueError(
|
|
855
|
+
f"cannot execute capability {capability!r} for claim {claim_id}: "
|
|
856
|
+
"the active evidence contract must be registered in an earlier turn"
|
|
857
|
+
)
|
|
858
|
+
active_contract = claim.evidence_contracts[-1]
|
|
859
|
+
active_binding = active_contract.execution_binding
|
|
860
|
+
if (
|
|
861
|
+
claim.kind == ClaimKind.INSTRUMENT
|
|
862
|
+
and active_binding is not None
|
|
863
|
+
and (
|
|
864
|
+
active_binding.capability != capability
|
|
865
|
+
or active_binding.commissioning_argv != argv
|
|
866
|
+
)
|
|
867
|
+
):
|
|
868
|
+
raise ValueError(
|
|
869
|
+
f"capability execution for bound instrument claim {claim_id} "
|
|
870
|
+
"must exactly match execution_binding.capability and "
|
|
871
|
+
"commissioning_argv; use a separate prospective interface claim "
|
|
872
|
+
"for scouting probes"
|
|
873
|
+
)
|
|
874
|
+
if claim.kind == ClaimKind.INSTRUMENT and active_binding is not None:
|
|
875
|
+
if program_sha256 is None:
|
|
876
|
+
raise ValueError(
|
|
877
|
+
f"capability execution for bound instrument claim {claim_id} "
|
|
878
|
+
"requires a stable workspace program source"
|
|
879
|
+
)
|
|
880
|
+
if active_binding.program_sha256 is None:
|
|
881
|
+
active_binding = active_binding.model_copy(
|
|
882
|
+
update={"program_sha256": program_sha256}
|
|
883
|
+
)
|
|
884
|
+
active_contract = claim.evidence_contracts[-1].model_copy(
|
|
885
|
+
update={"execution_binding": active_binding}
|
|
886
|
+
)
|
|
887
|
+
claim = claim.model_copy(
|
|
888
|
+
update={
|
|
889
|
+
"evidence_contracts": (
|
|
890
|
+
*claim.evidence_contracts[:-1],
|
|
891
|
+
active_contract,
|
|
892
|
+
),
|
|
893
|
+
"updated_iteration": iteration,
|
|
894
|
+
}
|
|
895
|
+
)
|
|
896
|
+
self._replace_claim(claim)
|
|
897
|
+
claims = self._ledger.by_id()
|
|
898
|
+
elif active_binding.program_sha256 != program_sha256:
|
|
899
|
+
raise ValueError(
|
|
900
|
+
f"capability execution for bound instrument claim {claim_id} "
|
|
901
|
+
"does not match its sealed program source; register a new "
|
|
902
|
+
"prospective contract and recommission the changed source"
|
|
903
|
+
)
|
|
904
|
+
if (
|
|
905
|
+
claim.kind == ClaimKind.INSTRUMENT
|
|
906
|
+
and claim.relation == ClaimRelation.INSTRUMENT_OF
|
|
907
|
+
):
|
|
908
|
+
prior_interface_claims: list[str] = []
|
|
909
|
+
for sibling in claims.values():
|
|
910
|
+
if (
|
|
911
|
+
sibling.id == claim.id
|
|
912
|
+
or sibling.kind != ClaimKind.INSTRUMENT
|
|
913
|
+
or sibling.relation != ClaimRelation.INSTRUMENT_OF
|
|
914
|
+
or sibling.parent_id != claim.parent_id
|
|
915
|
+
or sibling.status != ClaimDisposition.SUPPORTED
|
|
916
|
+
):
|
|
917
|
+
continue
|
|
918
|
+
contracts = {
|
|
919
|
+
contract.version: contract
|
|
920
|
+
for contract in sibling.evidence_contracts
|
|
921
|
+
}
|
|
922
|
+
for evidence in self._qualifying_contract_evidence(sibling):
|
|
923
|
+
contract = contracts.get(evidence.contract_version)
|
|
924
|
+
provenance = evidence.provenance
|
|
925
|
+
if contract is None or provenance is None:
|
|
926
|
+
continue
|
|
927
|
+
aspects = {
|
|
928
|
+
check.aspect
|
|
929
|
+
for check in contract.validation_checks
|
|
930
|
+
if check.aspect is not None
|
|
931
|
+
}
|
|
932
|
+
if (
|
|
933
|
+
provenance.capability == capability
|
|
934
|
+
and aspects
|
|
935
|
+
and aspects <= {CommissioningAspect.INTERFACE}
|
|
936
|
+
):
|
|
937
|
+
prior_interface_claims.append(sibling.id)
|
|
938
|
+
break
|
|
939
|
+
active_aspects = {
|
|
940
|
+
check.aspect
|
|
941
|
+
for check in claim.evidence_contracts[-1].validation_checks
|
|
942
|
+
if check.aspect is not None
|
|
943
|
+
}
|
|
944
|
+
if (
|
|
945
|
+
prior_interface_claims
|
|
946
|
+
and not active_aspects >= REQUIRED_SCIENTIFIC_COMMISSIONING_ASPECTS
|
|
947
|
+
):
|
|
948
|
+
raise ValueError(
|
|
949
|
+
f"cannot execute another incomplete instrument_of stage for "
|
|
950
|
+
f"capability {capability!r} and parent {claim.parent_id}: supported "
|
|
951
|
+
f"interface discovery already exists in {sorted(prior_interface_claims)}; "
|
|
952
|
+
"the active contract must now cover representation, "
|
|
953
|
+
"physics_controls, boundaries, diagnostics, and "
|
|
954
|
+
"numerical_regime together"
|
|
955
|
+
)
|
|
956
|
+
if claim.kind != ClaimKind.SCIENTIFIC:
|
|
957
|
+
return None
|
|
958
|
+
if program_sha256 is None:
|
|
959
|
+
raise ValueError(
|
|
960
|
+
f"cannot execute capability {capability!r} for scientific claim "
|
|
961
|
+
f"{claim_id}: use a stable workspace program that can be matched to "
|
|
962
|
+
"commissioning"
|
|
963
|
+
)
|
|
964
|
+
matched_binding = self._matching_execution_binding(
|
|
965
|
+
active_contract,
|
|
966
|
+
capability=capability,
|
|
967
|
+
program_path=argv[0],
|
|
968
|
+
program_sha256=program_sha256,
|
|
969
|
+
argv=argv,
|
|
970
|
+
instrument=False,
|
|
971
|
+
)
|
|
972
|
+
if active_contract.all_execution_bindings() and matched_binding is None:
|
|
973
|
+
raise ValueError(
|
|
974
|
+
f"capability execution for scientific claim {claim_id} does not "
|
|
975
|
+
"match an allowed_scientific_argv command in its active prospective "
|
|
976
|
+
"execution_binding pipeline"
|
|
977
|
+
)
|
|
978
|
+
if matched_binding is not None:
|
|
979
|
+
binding_index, binding = matched_binding
|
|
980
|
+
if binding.program_sha256 is None:
|
|
981
|
+
claim = self._seal_scientific_execution_binding(
|
|
982
|
+
claim=claim,
|
|
983
|
+
binding_index=binding_index,
|
|
984
|
+
program_sha256=program_sha256,
|
|
985
|
+
iteration=iteration,
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
synthetic_provenance = ClaimEvidenceProvenance(
|
|
989
|
+
sha256="0" * 64,
|
|
990
|
+
bytes=0,
|
|
991
|
+
tracked=True,
|
|
992
|
+
generated_iteration=iteration,
|
|
993
|
+
action="capability_execution_preflight",
|
|
994
|
+
command_argv=argv,
|
|
995
|
+
capability=capability,
|
|
996
|
+
program_path=argv[0],
|
|
997
|
+
program_sha256=program_sha256,
|
|
998
|
+
)
|
|
999
|
+
candidates = sorted(
|
|
1000
|
+
child.id
|
|
1001
|
+
for child in claims.values()
|
|
1002
|
+
if child.kind == ClaimKind.INSTRUMENT
|
|
1003
|
+
and child.relation == ClaimRelation.INSTRUMENT_OF
|
|
1004
|
+
and child.parent_id == claim_id
|
|
1005
|
+
and child.status == ClaimDisposition.SUPPORTED
|
|
1006
|
+
)
|
|
1007
|
+
for candidate in candidates:
|
|
1008
|
+
try:
|
|
1009
|
+
return self._validate_commissioning_claim(
|
|
1010
|
+
scientific_claim=claim,
|
|
1011
|
+
commissioning_claim_id=candidate,
|
|
1012
|
+
scientific_provenance=synthetic_provenance,
|
|
1013
|
+
)
|
|
1014
|
+
except ValueError:
|
|
1015
|
+
continue
|
|
1016
|
+
raise ValueError(
|
|
1017
|
+
f"cannot execute capability {capability!r} for scientific claim {claim_id}: "
|
|
1018
|
+
"require a supported machine-checked instrument_of claim from the same "
|
|
1019
|
+
"capability, closed before this execution, with one contract covering "
|
|
1020
|
+
"representation, physics_controls, boundaries, diagnostics, and "
|
|
1021
|
+
"numerical_regime for the "
|
|
1022
|
+
"same program source and a prospective execution_binding that allows "
|
|
1023
|
+
"this exact scientific argv; bind scouting and commissioning runs to "
|
|
1024
|
+
"an instrument claim"
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
def link_evidence(
|
|
1028
|
+
self,
|
|
1029
|
+
*,
|
|
1030
|
+
claim_id: str,
|
|
1031
|
+
path: str,
|
|
1032
|
+
note: str,
|
|
1033
|
+
observation_sufficient: bool,
|
|
1034
|
+
observation_note: str,
|
|
1035
|
+
provenance: ClaimEvidenceProvenance,
|
|
1036
|
+
commissioning_claim_id: str | None = None,
|
|
1037
|
+
evidence_document: Any | None = None,
|
|
1038
|
+
evidence_document_error: str | None = None,
|
|
1039
|
+
iteration: int,
|
|
1040
|
+
) -> dict[str, Any]:
|
|
1041
|
+
claim_id = claim_id.strip().casefold()
|
|
1042
|
+
existing = self._ledger.by_id()
|
|
1043
|
+
if claim_id not in existing:
|
|
1044
|
+
raise ValueError(f"unknown claim_id: {claim_id}")
|
|
1045
|
+
claim = existing[claim_id]
|
|
1046
|
+
if claim.status != ClaimDisposition.OPEN:
|
|
1047
|
+
raise ValueError(f"claim is already closed: {claim_id}")
|
|
1048
|
+
contract = claim.evidence_contracts[-1] if claim.evidence_contracts else None
|
|
1049
|
+
contract_version = contract.version if contract is not None else None
|
|
1050
|
+
validation_passed, validation_results = self._evaluate_validation_checks(
|
|
1051
|
+
contract.validation_checks if contract is not None else (),
|
|
1052
|
+
evidence_document=evidence_document,
|
|
1053
|
+
evidence_document_error=evidence_document_error,
|
|
1054
|
+
)
|
|
1055
|
+
if observation_sufficient:
|
|
1056
|
+
if contract is None:
|
|
1057
|
+
raise ValueError(
|
|
1058
|
+
f"cannot mark evidence sufficient for {claim_id} without a "
|
|
1059
|
+
"prospective evidence contract"
|
|
1060
|
+
)
|
|
1061
|
+
if not provenance.evidence_eligible:
|
|
1062
|
+
raise ValueError(
|
|
1063
|
+
"cannot mark a workbench artifact sufficient; freeze and promote "
|
|
1064
|
+
"the program through prospective commissioning, then generate a "
|
|
1065
|
+
"fresh evidence-stage artifact"
|
|
1066
|
+
)
|
|
1067
|
+
if (
|
|
1068
|
+
not provenance.tracked
|
|
1069
|
+
or provenance.generated_iteration is None
|
|
1070
|
+
or provenance.generated_iteration < contract.registered_iteration
|
|
1071
|
+
):
|
|
1072
|
+
raise ValueError(
|
|
1073
|
+
"cannot mark evidence sufficient unless it is provenance-tracked "
|
|
1074
|
+
"and generated after the active contract"
|
|
1075
|
+
)
|
|
1076
|
+
if contract.validation_checks and validation_passed is not True:
|
|
1077
|
+
failures = [
|
|
1078
|
+
result.json_path
|
|
1079
|
+
for result in validation_results
|
|
1080
|
+
if not result.passed
|
|
1081
|
+
]
|
|
1082
|
+
raise ValueError(
|
|
1083
|
+
"cannot mark evidence sufficient: machine-checkable validation "
|
|
1084
|
+
f"failed for {failures}"
|
|
1085
|
+
)
|
|
1086
|
+
bindings = contract.all_execution_bindings()
|
|
1087
|
+
if bindings and self._matching_execution_binding(
|
|
1088
|
+
contract,
|
|
1089
|
+
capability=provenance.capability,
|
|
1090
|
+
program_path=provenance.program_path,
|
|
1091
|
+
program_sha256=provenance.program_sha256,
|
|
1092
|
+
argv=provenance.command_argv,
|
|
1093
|
+
instrument=claim.kind == ClaimKind.INSTRUMENT,
|
|
1094
|
+
) is None:
|
|
1095
|
+
raise ValueError(
|
|
1096
|
+
"cannot mark evidence sufficient: artifact provenance does not "
|
|
1097
|
+
"match a prospectively authorized evidence command in the "
|
|
1098
|
+
"execution_binding pipeline"
|
|
1099
|
+
)
|
|
1100
|
+
if (
|
|
1101
|
+
claim.kind == ClaimKind.INSTRUMENT
|
|
1102
|
+
and claim.relation == ClaimRelation.INSTRUMENT_OF
|
|
1103
|
+
and provenance.capability is not None
|
|
1104
|
+
and provenance.execution_succeeded is not True
|
|
1105
|
+
):
|
|
1106
|
+
raise ValueError(
|
|
1107
|
+
"cannot mark commissioning evidence sufficient unless the "
|
|
1108
|
+
"runner witnessed a successful capability execution (return code "
|
|
1109
|
+
"zero, no timeout, and no workspace-limit termination)"
|
|
1110
|
+
)
|
|
1111
|
+
if claim.kind == ClaimKind.SCIENTIFIC and provenance.capability is not None:
|
|
1112
|
+
commissioning_claim_id = self._validate_commissioning_claim(
|
|
1113
|
+
scientific_claim=claim,
|
|
1114
|
+
commissioning_claim_id=commissioning_claim_id,
|
|
1115
|
+
scientific_provenance=provenance,
|
|
1116
|
+
)
|
|
1117
|
+
link = ClaimEvidenceLink(
|
|
1118
|
+
path=path,
|
|
1119
|
+
note=note,
|
|
1120
|
+
iteration=iteration,
|
|
1121
|
+
contract_version=contract_version,
|
|
1122
|
+
observation_sufficient=observation_sufficient,
|
|
1123
|
+
observation_note=observation_note,
|
|
1124
|
+
validation_passed=validation_passed,
|
|
1125
|
+
validation_results=validation_results,
|
|
1126
|
+
commissioning_claim_id=commissioning_claim_id,
|
|
1127
|
+
provenance=provenance,
|
|
1128
|
+
)
|
|
1129
|
+
updated = MVPClaim(
|
|
1130
|
+
id=claim.id,
|
|
1131
|
+
statement=claim.statement,
|
|
1132
|
+
kind=claim.kind,
|
|
1133
|
+
relation=claim.relation,
|
|
1134
|
+
parent_id=claim.parent_id,
|
|
1135
|
+
status=claim.status,
|
|
1136
|
+
rationale=claim.rationale,
|
|
1137
|
+
evidence_contracts=claim.evidence_contracts,
|
|
1138
|
+
evidence=claim.evidence + (link,),
|
|
1139
|
+
closed_reason=claim.closed_reason,
|
|
1140
|
+
created_iteration=claim.created_iteration,
|
|
1141
|
+
updated_iteration=iteration,
|
|
1142
|
+
)
|
|
1143
|
+
ledger = self._replace_claim(updated)
|
|
1144
|
+
return {
|
|
1145
|
+
"updated": updated.model_dump(mode="json"),
|
|
1146
|
+
"artifact_exists": True,
|
|
1147
|
+
"artifact_provenance": provenance.model_dump(mode="json"),
|
|
1148
|
+
"claim_ledger": ledger.compact_summary(),
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
def close(
|
|
1152
|
+
self,
|
|
1153
|
+
*,
|
|
1154
|
+
claim_id: str,
|
|
1155
|
+
status: ClaimDisposition,
|
|
1156
|
+
reason: str,
|
|
1157
|
+
iteration: int,
|
|
1158
|
+
) -> dict[str, Any]:
|
|
1159
|
+
claim_id = claim_id.strip().casefold()
|
|
1160
|
+
if status == ClaimDisposition.OPEN:
|
|
1161
|
+
raise ValueError("close_claim requires a non-open disposition")
|
|
1162
|
+
existing = self._ledger.by_id()
|
|
1163
|
+
if claim_id not in existing:
|
|
1164
|
+
raise ValueError(f"unknown claim_id: {claim_id}")
|
|
1165
|
+
claim = existing[claim_id]
|
|
1166
|
+
if claim.status != ClaimDisposition.OPEN:
|
|
1167
|
+
raise ValueError(f"claim is already closed: {claim_id}")
|
|
1168
|
+
if status in {ClaimDisposition.SUPPORTED, ClaimDisposition.FALSIFIED}:
|
|
1169
|
+
if not claim.evidence_contracts:
|
|
1170
|
+
raise ValueError(
|
|
1171
|
+
f"cannot close {claim_id} as {status.value} without a registered "
|
|
1172
|
+
"evidence contract"
|
|
1173
|
+
)
|
|
1174
|
+
active_contract = claim.evidence_contracts[-1]
|
|
1175
|
+
if (
|
|
1176
|
+
status == ClaimDisposition.SUPPORTED
|
|
1177
|
+
and claim.kind == ClaimKind.INSTRUMENT
|
|
1178
|
+
and claim.relation == ClaimRelation.INSTRUMENT_OF
|
|
1179
|
+
and active_contract.execution_binding is not None
|
|
1180
|
+
and active_contract.execution_binding.allowed_scientific_argv
|
|
1181
|
+
):
|
|
1182
|
+
active_aspects = {
|
|
1183
|
+
check.aspect
|
|
1184
|
+
for check in active_contract.validation_checks
|
|
1185
|
+
if check.aspect is not None
|
|
1186
|
+
}
|
|
1187
|
+
missing_aspects = sorted(
|
|
1188
|
+
aspect.value
|
|
1189
|
+
for aspect in (
|
|
1190
|
+
REQUIRED_SCIENTIFIC_COMMISSIONING_ASPECTS
|
|
1191
|
+
- active_aspects
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
1194
|
+
if missing_aspects:
|
|
1195
|
+
raise ValueError(
|
|
1196
|
+
f"cannot close capability-bound instrument claim {claim_id} "
|
|
1197
|
+
"as supported: its active contract is intended to authorize "
|
|
1198
|
+
"scientific execution but is missing required machine-checked "
|
|
1199
|
+
f"aspects {missing_aspects}; one contract must cover "
|
|
1200
|
+
"representation, physics_controls, boundaries, diagnostics, "
|
|
1201
|
+
"and numerical_regime"
|
|
1202
|
+
)
|
|
1203
|
+
active_contract_version = claim.evidence_contracts[-1].version
|
|
1204
|
+
prior_contract_evidence = [
|
|
1205
|
+
evidence
|
|
1206
|
+
for evidence in claim.evidence
|
|
1207
|
+
if evidence.contract_version is not None
|
|
1208
|
+
and evidence.contract_version < active_contract_version
|
|
1209
|
+
]
|
|
1210
|
+
if prior_contract_evidence:
|
|
1211
|
+
raise ValueError(
|
|
1212
|
+
f"cannot close {claim_id} as {status.value} under an adaptive "
|
|
1213
|
+
"contract registered after evidence was already linked; close "
|
|
1214
|
+
"non-decisively or register a successor/refinement claim with a "
|
|
1215
|
+
"prospective pipeline and collect fresh observations"
|
|
1216
|
+
)
|
|
1217
|
+
qualifying_evidence = list(self._qualifying_contract_evidence(claim))
|
|
1218
|
+
if claim.kind == ClaimKind.SCIENTIFIC:
|
|
1219
|
+
commissioned: list[ClaimEvidenceLink] = []
|
|
1220
|
+
for evidence in qualifying_evidence:
|
|
1221
|
+
provenance = evidence.provenance
|
|
1222
|
+
assert provenance is not None
|
|
1223
|
+
if provenance.capability is None:
|
|
1224
|
+
commissioned.append(evidence)
|
|
1225
|
+
continue
|
|
1226
|
+
try:
|
|
1227
|
+
self._validate_commissioning_claim(
|
|
1228
|
+
scientific_claim=claim,
|
|
1229
|
+
commissioning_claim_id=evidence.commissioning_claim_id,
|
|
1230
|
+
scientific_provenance=provenance,
|
|
1231
|
+
)
|
|
1232
|
+
except ValueError:
|
|
1233
|
+
continue
|
|
1234
|
+
commissioned.append(evidence)
|
|
1235
|
+
qualifying_evidence = commissioned
|
|
1236
|
+
if not qualifying_evidence:
|
|
1237
|
+
raise ValueError(
|
|
1238
|
+
f"cannot close {claim_id} as {status.value}: require linked, "
|
|
1239
|
+
"provenance-tracked evidence generated under an evidence contract "
|
|
1240
|
+
"and marked observation_sufficient=true; capability-generated "
|
|
1241
|
+
"scientific evidence also requires prior machine-checked commissioning"
|
|
1242
|
+
)
|
|
1243
|
+
updated = MVPClaim(
|
|
1244
|
+
id=claim.id,
|
|
1245
|
+
statement=claim.statement,
|
|
1246
|
+
kind=claim.kind,
|
|
1247
|
+
relation=claim.relation,
|
|
1248
|
+
parent_id=claim.parent_id,
|
|
1249
|
+
status=status,
|
|
1250
|
+
rationale=claim.rationale,
|
|
1251
|
+
evidence_contracts=claim.evidence_contracts,
|
|
1252
|
+
evidence=claim.evidence,
|
|
1253
|
+
closed_reason=reason,
|
|
1254
|
+
created_iteration=claim.created_iteration,
|
|
1255
|
+
updated_iteration=iteration,
|
|
1256
|
+
)
|
|
1257
|
+
ledger = self._replace_claim(updated)
|
|
1258
|
+
result: dict[str, Any] = {
|
|
1259
|
+
"closed": updated.model_dump(mode="json"),
|
|
1260
|
+
"claim_ledger": ledger.compact_summary(),
|
|
1261
|
+
}
|
|
1262
|
+
# Non-evidentiary dispositions remain available so a bounded campaign can
|
|
1263
|
+
# finish honestly even when no decisive artifact was produced.
|
|
1264
|
+
if not claim.evidence and claim_id != ROOT_CLAIM_ID:
|
|
1265
|
+
result["evidence_warning"] = (
|
|
1266
|
+
f"closed {claim_id} with status={status.value} and no linked evidence; "
|
|
1267
|
+
"prefer link_claim_evidence before close_claim when workspace "
|
|
1268
|
+
"artifacts exist"
|
|
1269
|
+
)
|
|
1270
|
+
return result
|
|
1271
|
+
|
|
1272
|
+
def list_claims(self) -> dict[str, Any]:
|
|
1273
|
+
return {
|
|
1274
|
+
"claim_ledger": self._ledger.compact_summary(max_claims=200),
|
|
1275
|
+
"claims": [claim.model_dump(mode="json") for claim in self._ledger.claims],
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
def snapshot(self) -> dict[str, Any]:
|
|
1279
|
+
return json.loads(self._ledger.model_dump_json())
|