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.
Files changed (75) hide show
  1. conjecture_solver/__init__.py +10 -0
  2. conjecture_solver/__main__.py +4 -0
  3. conjecture_solver/action_handlers.py +459 -0
  4. conjecture_solver/adapters/__init__.py +7 -0
  5. conjecture_solver/adapters/base.py +96 -0
  6. conjecture_solver/adapters/fake.py +155 -0
  7. conjecture_solver/adapters/pic.py +196 -0
  8. conjecture_solver/adapters/warpx.py +1375 -0
  9. conjecture_solver/autonomous_research.py +1071 -0
  10. conjecture_solver/benchmarks/__init__.py +6 -0
  11. conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
  12. conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
  13. conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
  14. conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
  15. conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
  16. conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
  17. conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
  18. conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
  19. conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
  20. conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
  21. conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
  22. conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
  23. conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
  24. conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
  25. conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
  26. conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
  27. conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
  28. conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
  29. conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
  30. conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
  31. conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
  32. conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
  33. conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
  34. conjecture_solver/campaign.py +406 -0
  35. conjecture_solver/cli.py +963 -0
  36. conjecture_solver/confirmation.py +287 -0
  37. conjecture_solver/control.py +234 -0
  38. conjecture_solver/deployment.py +874 -0
  39. conjecture_solver/discovery.py +68 -0
  40. conjecture_solver/domains/__init__.py +40 -0
  41. conjecture_solver/domains/base.py +157 -0
  42. conjecture_solver/domains/kinetic_sufficiency.py +521 -0
  43. conjecture_solver/ledger.py +238 -0
  44. conjecture_solver/lifecycle.py +131 -0
  45. conjecture_solver/literature.py +494 -0
  46. conjecture_solver/llm.py +228 -0
  47. conjecture_solver/models.py +279 -0
  48. conjecture_solver/mvp_agent.py +3212 -0
  49. conjecture_solver/mvp_claims.py +1279 -0
  50. conjecture_solver/mvp_control.py +322 -0
  51. conjecture_solver/mvp_guidance.py +172 -0
  52. conjecture_solver/mvp_launch.py +1296 -0
  53. conjecture_solver/mvp_monitor.py +1487 -0
  54. conjecture_solver/mvp_skills.py +538 -0
  55. conjecture_solver/orchestration.py +578 -0
  56. conjecture_solver/outbox.py +526 -0
  57. conjecture_solver/parameters.py +394 -0
  58. conjecture_solver/proposals.py +427 -0
  59. conjecture_solver/research_tools.py +278 -0
  60. conjecture_solver/schema_export.py +246 -0
  61. conjecture_solver/search.py +1047 -0
  62. conjecture_solver/semantics.py +146 -0
  63. conjecture_solver/tui/__init__.py +7 -0
  64. conjecture_solver/tui/app.py +202 -0
  65. conjecture_solver/tui/claim_views.py +213 -0
  66. conjecture_solver/tui/screens.py +1014 -0
  67. conjecture_solver/warpx_analysis.py +184 -0
  68. conjecture_solver/warpx_campaign.py +148 -0
  69. conjecture_solver/warpx_confirmation.py +617 -0
  70. simjecture-0.1.0.dist-info/METADATA +400 -0
  71. simjecture-0.1.0.dist-info/RECORD +75 -0
  72. simjecture-0.1.0.dist-info/WHEEL +4 -0
  73. simjecture-0.1.0.dist-info/entry_points.txt +4 -0
  74. simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
  75. simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
@@ -0,0 +1,322 @@
1
+ """Operator control channel and run clock for MVP campaigns.
2
+
3
+ These files live under operator_input/ and are not scientific evidence. The
4
+ runner consults them only at action boundaries. They never use SIGSTOP.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from datetime import UTC, datetime
12
+ from enum import StrEnum
13
+ from pathlib import Path
14
+ from typing import Any, Literal
15
+
16
+ from pydantic import Field
17
+
18
+ from .models import StrictModel
19
+
20
+ CONTROL_RELATIVE = Path("operator_input") / "control.json"
21
+ CLOCK_RELATIVE = Path("operator_input") / "run_clock.json"
22
+ PAUSE_STATE_RELATIVE = Path("operator_input") / "pause_state.json"
23
+
24
+
25
+ class ControlCommand(StrEnum):
26
+ NONE = "none"
27
+ PAUSE = "pause"
28
+ CANCEL = "cancel"
29
+
30
+
31
+ class CampaignPaused(Exception):
32
+ """Raised when the runner honors an action-boundary pause.
33
+
34
+ No terminal report is written. Repeating the original contract resumes.
35
+ """
36
+
37
+ def __init__(self, run_directory: str | Path, *, iterations: int) -> None:
38
+ self.run_directory = Path(run_directory)
39
+ self.iterations = iterations
40
+ super().__init__(
41
+ f"campaign paused at an action boundary after {iterations} iteration(s)"
42
+ )
43
+
44
+
45
+ class OperatorControl(StrictModel):
46
+ schema_version: Literal["0.1.0"] = "0.1.0"
47
+ command: ControlCommand = ControlCommand.NONE
48
+ requested_at: datetime | None = None
49
+ source: str = "operator"
50
+
51
+
52
+ class RunClock(StrictModel):
53
+ schema_version: Literal["0.1.0"] = "0.1.0"
54
+ started_at: datetime
55
+ session_started_at: datetime
56
+ last_tick_at: datetime
57
+ accumulated_active_seconds: float = Field(default=0.0, ge=0)
58
+ state: Literal["running", "paused", "finished"] = "running"
59
+
60
+
61
+ class PauseState(StrictModel):
62
+ schema_version: Literal["0.1.0"] = "0.1.0"
63
+ paused_at: datetime
64
+ iterations: int = Field(ge=0)
65
+ reason: str = "operator requested pause at the next action boundary"
66
+
67
+
68
+ def utc_now() -> datetime:
69
+ return datetime.now(UTC)
70
+
71
+
72
+ def read_control(run_directory: str | Path) -> OperatorControl:
73
+ path = Path(run_directory) / CONTROL_RELATIVE
74
+ payload = _read_json(path)
75
+ if payload is None:
76
+ return OperatorControl()
77
+ try:
78
+ return OperatorControl.model_validate(payload)
79
+ except Exception:
80
+ return OperatorControl()
81
+
82
+
83
+ def write_control(
84
+ run_directory: str | Path,
85
+ command: ControlCommand,
86
+ *,
87
+ source: str = "operator",
88
+ now: datetime | None = None,
89
+ ) -> OperatorControl:
90
+ control = OperatorControl(
91
+ command=command,
92
+ requested_at=now or utc_now(),
93
+ source=source,
94
+ )
95
+ _write_json(Path(run_directory) / CONTROL_RELATIVE, control.model_dump(mode="json"))
96
+ return control
97
+
98
+
99
+ def clear_control(run_directory: str | Path, *, source: str = "runner") -> None:
100
+ write_control(run_directory, ControlCommand.NONE, source=source)
101
+
102
+
103
+ def poll_control(run_directory: str | Path) -> ControlCommand:
104
+ return read_control(run_directory).command
105
+
106
+
107
+ def read_clock(run_directory: str | Path) -> RunClock | None:
108
+ payload = _read_json(Path(run_directory) / CLOCK_RELATIVE)
109
+ if payload is None:
110
+ return None
111
+ try:
112
+ return RunClock.model_validate(payload)
113
+ except Exception:
114
+ return None
115
+
116
+
117
+ def read_pause_state(run_directory: str | Path) -> PauseState | None:
118
+ payload = _read_json(Path(run_directory) / PAUSE_STATE_RELATIVE)
119
+ if payload is None:
120
+ return None
121
+ try:
122
+ return PauseState.model_validate(payload)
123
+ except Exception:
124
+ return None
125
+
126
+
127
+ def begin_or_resume_clock(
128
+ run_directory: str | Path,
129
+ *,
130
+ now: datetime | None = None,
131
+ ) -> RunClock:
132
+ """Start or continue the operator clock and clear a prior pause request."""
133
+
134
+ moment = now or utc_now()
135
+ existing = read_clock(run_directory)
136
+ if existing is None:
137
+ clock = RunClock(
138
+ started_at=moment,
139
+ session_started_at=moment,
140
+ last_tick_at=moment,
141
+ accumulated_active_seconds=0.0,
142
+ state="running",
143
+ )
144
+ else:
145
+ orphan = 0.0
146
+ if existing.state == "running":
147
+ orphan = max(
148
+ 0.0,
149
+ (existing.last_tick_at - existing.session_started_at).total_seconds(),
150
+ )
151
+ clock = RunClock(
152
+ started_at=existing.started_at,
153
+ session_started_at=moment,
154
+ last_tick_at=moment,
155
+ accumulated_active_seconds=existing.accumulated_active_seconds + orphan,
156
+ state="running",
157
+ )
158
+ _write_clock(run_directory, clock)
159
+ clear_control(run_directory, source="runner")
160
+ pause_path = Path(run_directory) / PAUSE_STATE_RELATIVE
161
+ if pause_path.exists():
162
+ pause_path.unlink()
163
+ return clock
164
+
165
+
166
+ def tick_clock(run_directory: str | Path, *, now: datetime | None = None) -> RunClock:
167
+ moment = now or utc_now()
168
+ clock = read_clock(run_directory)
169
+ if clock is None:
170
+ return begin_or_resume_clock(run_directory, now=moment)
171
+ if clock.state != "running":
172
+ return clock
173
+ updated = clock.model_copy(update={"last_tick_at": moment})
174
+ _write_clock(run_directory, updated)
175
+ return updated
176
+
177
+
178
+ def pause_at_boundary(
179
+ run_directory: str | Path,
180
+ *,
181
+ iterations: int,
182
+ now: datetime | None = None,
183
+ ) -> PauseState:
184
+ moment = now or utc_now()
185
+ clock = read_clock(run_directory)
186
+ if clock is not None and clock.state == "running":
187
+ session = max(0.0, (moment - clock.session_started_at).total_seconds())
188
+ paused_clock = RunClock(
189
+ started_at=clock.started_at,
190
+ session_started_at=clock.session_started_at,
191
+ last_tick_at=moment,
192
+ accumulated_active_seconds=clock.accumulated_active_seconds + session,
193
+ state="paused",
194
+ )
195
+ _write_clock(run_directory, paused_clock)
196
+ state = PauseState(paused_at=moment, iterations=iterations)
197
+ _write_json(Path(run_directory) / PAUSE_STATE_RELATIVE, state.model_dump(mode="json"))
198
+ clear_control(run_directory, source="runner")
199
+ return state
200
+
201
+
202
+ def finalize_clock(run_directory: str | Path, *, now: datetime | None = None) -> RunClock | None:
203
+ moment = now or utc_now()
204
+ clock = read_clock(run_directory)
205
+ if clock is None:
206
+ return None
207
+ extra = 0.0
208
+ if clock.state == "running":
209
+ extra = max(0.0, (moment - clock.session_started_at).total_seconds())
210
+ finished = RunClock(
211
+ started_at=clock.started_at,
212
+ session_started_at=clock.session_started_at,
213
+ last_tick_at=moment,
214
+ accumulated_active_seconds=clock.accumulated_active_seconds + extra,
215
+ state="finished",
216
+ )
217
+ _write_clock(run_directory, finished)
218
+ return finished
219
+
220
+
221
+ def elapsed_from_clock(clock: RunClock, *, now: datetime | None = None) -> float:
222
+ moment = now or utc_now()
223
+ if clock.state == "running":
224
+ session = max(0.0, (moment - clock.session_started_at).total_seconds())
225
+ return clock.accumulated_active_seconds + session
226
+ return clock.accumulated_active_seconds
227
+
228
+
229
+ def parse_usage_payload(usage: Any) -> dict[str, int]:
230
+ """Normalize provider usage objects without treating them as scientific status."""
231
+
232
+ empty = {
233
+ "prompt_tokens": 0,
234
+ "completion_tokens": 0,
235
+ "total_tokens": 0,
236
+ "cached_tokens": 0,
237
+ "reasoning_tokens": 0,
238
+ }
239
+ if not isinstance(usage, dict):
240
+ return empty
241
+ prompt = _as_int(usage.get("prompt_tokens"))
242
+ if prompt is None:
243
+ prompt = _as_int(usage.get("input_tokens")) or 0
244
+ completion = _as_int(usage.get("completion_tokens"))
245
+ if completion is None:
246
+ completion = _as_int(usage.get("output_tokens")) or 0
247
+ total = _as_int(usage.get("total_tokens"))
248
+ if total is None:
249
+ total = prompt + completion
250
+ cached = 0
251
+ details = usage.get("prompt_tokens_details")
252
+ if isinstance(details, dict):
253
+ cached = _as_int(details.get("cached_tokens")) or 0
254
+ cached = (
255
+ _as_int(usage.get("prompt_cache_hit_tokens"))
256
+ or _as_int(usage.get("cached_tokens"))
257
+ or cached
258
+ )
259
+ reasoning = 0
260
+ out_details = usage.get("completion_tokens_details")
261
+ if isinstance(out_details, dict):
262
+ reasoning = _as_int(out_details.get("reasoning_tokens")) or 0
263
+ return {
264
+ "prompt_tokens": max(0, prompt),
265
+ "completion_tokens": max(0, completion),
266
+ "total_tokens": max(0, total),
267
+ "cached_tokens": max(0, cached),
268
+ "reasoning_tokens": max(0, reasoning),
269
+ }
270
+
271
+
272
+ def format_token_counts(
273
+ *,
274
+ prompt_tokens: int,
275
+ completion_tokens: int,
276
+ total_tokens: int,
277
+ turns: int,
278
+ missing_turns: int = 0,
279
+ cached_tokens: int = 0,
280
+ ) -> str:
281
+ parts = [
282
+ f"tokens: {_comma(prompt_tokens)} in / {_comma(completion_tokens)} out / "
283
+ f"{_comma(total_tokens)} total"
284
+ ]
285
+ extras: list[str] = [f"{turns} turn" + ("s" if turns != 1 else "")]
286
+ if missing_turns:
287
+ extras.append(f"{missing_turns} without usage")
288
+ if cached_tokens:
289
+ extras.append(f"{_comma(cached_tokens)} cached")
290
+ parts.append("(" + ", ".join(extras) + ")")
291
+ return " ".join(parts)
292
+
293
+
294
+ def _comma(value: int) -> str:
295
+ return f"{value:,}"
296
+
297
+
298
+ def _as_int(value: Any) -> int | None:
299
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
300
+ return None
301
+ return int(value)
302
+
303
+
304
+ def _read_json(path: Path) -> dict[str, Any] | None:
305
+ if not path.is_file():
306
+ return None
307
+ try:
308
+ payload = json.loads(path.read_text())
309
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError):
310
+ return None
311
+ return payload if isinstance(payload, dict) else None
312
+
313
+
314
+ def _write_clock(run_directory: str | Path, clock: RunClock) -> None:
315
+ _write_json(Path(run_directory) / CLOCK_RELATIVE, clock.model_dump(mode="json"))
316
+
317
+
318
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
319
+ path.parent.mkdir(parents=True, exist_ok=True)
320
+ temporary = path.with_name(f".{path.name}.tmp")
321
+ temporary.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
322
+ os.replace(temporary, path)
@@ -0,0 +1,172 @@
1
+ """Validated starting points for guided MVP commissioning campaigns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from dataclasses import dataclass
8
+ from pathlib import Path, PurePosixPath
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import Field, model_validator
12
+
13
+ from .models import StrictModel
14
+
15
+
16
+ def _workspace_relative_path(value: str) -> str:
17
+ path = PurePosixPath(value)
18
+ if (
19
+ not value
20
+ or path.is_absolute()
21
+ or "\\" in value
22
+ or any(part in {"", ".", ".."} for part in path.parts)
23
+ ):
24
+ raise ValueError(f"guided commissioning path must be workspace-relative: {value!r}")
25
+ if path.as_posix() != value:
26
+ raise ValueError(f"guided commissioning path must be canonical: {value!r}")
27
+ return path.as_posix()
28
+
29
+
30
+ class MVPGuidedCommissioningSpec(StrictModel):
31
+ """Operator-authored manifest for one known-runnable starting point.
32
+
33
+ The package deliberately says only what was run and where its compact validation
34
+ record lives. It does not qualify the supplied output as campaign evidence.
35
+ """
36
+
37
+ schema_version: Literal["0.1.0"] = "0.1.0"
38
+ name: str = Field(min_length=1, pattern=r"^[a-z0-9][a-z0-9._-]*$")
39
+ description: str = Field(min_length=1)
40
+ capability: str = Field(min_length=1)
41
+ program_path: str = Field(min_length=1)
42
+ validated_argv: tuple[str, ...] = Field(min_length=1)
43
+ validation_summary_path: str = Field(min_length=1)
44
+ operator_validation: str = Field(min_length=1)
45
+ limitations: tuple[str, ...] = ()
46
+ files: tuple[str, ...] = Field(min_length=2)
47
+
48
+ @model_validator(mode="after")
49
+ def paths_and_command_are_self_contained(self) -> MVPGuidedCommissioningSpec:
50
+ normalized_files = tuple(_workspace_relative_path(path) for path in self.files)
51
+ if len(set(normalized_files)) != len(normalized_files):
52
+ raise ValueError("guided commissioning files must be unique")
53
+ program_path = _workspace_relative_path(self.program_path)
54
+ summary_path = _workspace_relative_path(self.validation_summary_path)
55
+ if program_path not in normalized_files:
56
+ raise ValueError("program_path must be listed in files")
57
+ if summary_path not in normalized_files:
58
+ raise ValueError("validation_summary_path must be listed in files")
59
+ if self.validated_argv[0] != program_path:
60
+ raise ValueError("validated_argv[0] must equal program_path")
61
+ if any(not argument for argument in self.validated_argv):
62
+ raise ValueError("validated_argv arguments cannot be empty")
63
+ return self
64
+
65
+
66
+ class MVPGuidedCommissioningFile(StrictModel):
67
+ path: str = Field(min_length=1)
68
+ bytes: int = Field(ge=0)
69
+ sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class MVPGuidedCommissioningPackage:
74
+ """Loaded, content-addressed guided commissioning package."""
75
+
76
+ manifest_path: Path
77
+ spec: MVPGuidedCommissioningSpec
78
+ file_records: tuple[MVPGuidedCommissioningFile, ...]
79
+ package_sha256: str
80
+
81
+ @classmethod
82
+ def read(cls, manifest_path: str | Path) -> MVPGuidedCommissioningPackage:
83
+ requested_manifest = Path(manifest_path)
84
+ if requested_manifest.is_symlink():
85
+ raise ValueError("guided commissioning manifest must be a regular file")
86
+ manifest = requested_manifest.resolve()
87
+ if not manifest.is_file():
88
+ raise ValueError("guided commissioning manifest must be a regular file")
89
+ spec = MVPGuidedCommissioningSpec.model_validate_json(manifest.read_text())
90
+ root = manifest.parent
91
+ digest = hashlib.sha256()
92
+ canonical_spec = json.dumps(
93
+ spec.model_dump(mode="json"),
94
+ sort_keys=True,
95
+ separators=(",", ":"),
96
+ ).encode()
97
+ digest.update(len(canonical_spec).to_bytes(8, "big"))
98
+ digest.update(canonical_spec)
99
+ records: list[MVPGuidedCommissioningFile] = []
100
+ for relative in sorted(spec.files):
101
+ normalized = _workspace_relative_path(relative)
102
+ requested = root / normalized
103
+ path = requested.resolve()
104
+ if requested.is_symlink() or not path.is_relative_to(root) or not path.is_file():
105
+ raise ValueError(
106
+ f"guided commissioning file is missing, irregular, or escapes package: "
107
+ f"{relative!r}"
108
+ )
109
+ encoded = path.read_bytes()
110
+ sha256 = hashlib.sha256(encoded).hexdigest()
111
+ records.append(
112
+ MVPGuidedCommissioningFile(
113
+ path=normalized,
114
+ bytes=len(encoded),
115
+ sha256=sha256,
116
+ )
117
+ )
118
+ name = normalized.encode()
119
+ digest.update(len(name).to_bytes(8, "big"))
120
+ digest.update(name)
121
+ digest.update(len(encoded).to_bytes(8, "big"))
122
+ digest.update(encoded)
123
+ return cls(
124
+ manifest_path=manifest,
125
+ spec=spec,
126
+ file_records=tuple(records),
127
+ package_sha256=digest.hexdigest(),
128
+ )
129
+
130
+ @property
131
+ def root(self) -> Path:
132
+ return self.manifest_path.parent
133
+
134
+ def read_file(self, relative: str) -> bytes:
135
+ normalized = _workspace_relative_path(relative)
136
+ record = next(
137
+ (item for item in self.file_records if item.path == normalized),
138
+ None,
139
+ )
140
+ if record is None:
141
+ raise ValueError(f"file is not declared by guided commissioning: {relative!r}")
142
+ requested = self.root / normalized
143
+ path = requested.resolve()
144
+ if requested.is_symlink() or not path.is_relative_to(self.root) or not path.is_file():
145
+ raise ValueError(f"guided commissioning file identity changed: {relative!r}")
146
+ encoded = path.read_bytes()
147
+ if len(encoded) != record.bytes or hashlib.sha256(encoded).hexdigest() != record.sha256:
148
+ raise ValueError(f"guided commissioning file identity changed: {relative!r}")
149
+ return encoded
150
+
151
+ def assert_identity(self) -> None:
152
+ current = self.read(self.manifest_path)
153
+ if current.package_sha256 != self.package_sha256:
154
+ raise ValueError("guided commissioning package identity changed")
155
+
156
+ def descriptor(self) -> dict[str, Any]:
157
+ return {
158
+ "available": True,
159
+ "schema_version": self.spec.schema_version,
160
+ "name": self.spec.name,
161
+ "description": self.spec.description,
162
+ "capability": self.spec.capability,
163
+ "program_path": self.spec.program_path,
164
+ "validated_argv": list(self.spec.validated_argv),
165
+ "validation_summary_path": self.spec.validation_summary_path,
166
+ "operator_validation": self.spec.operator_validation,
167
+ "limitations": list(self.spec.limitations),
168
+ "files": [item.model_dump(mode="json") for item in self.file_records],
169
+ "package_sha256": self.package_sha256,
170
+ "scientific_evidence_eligible": False,
171
+ "policy": "operator_validated_starting_point_not_campaign_evidence",
172
+ }