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,874 @@
|
|
|
1
|
+
"""Operator-side deployment and health checks for optional scientific runtimes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata as metadata
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import tomllib
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
from .models import StrictModel
|
|
20
|
+
from .mvp_agent import BubblewrapSandbox, MVPAgentConfig
|
|
21
|
+
from .mvp_skills import (
|
|
22
|
+
MVPCapabilityConfig,
|
|
23
|
+
MVPCapabilityInstallation,
|
|
24
|
+
MVPCapabilityRegistry,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
WARPX_CPU_CAPABILITY = "warpx-cpu-26.07"
|
|
28
|
+
WARPX_CUDA_CAPABILITY = "warpx-cuda-openpmd-26.07"
|
|
29
|
+
PINNED_WARPX_REVISION = "312d507407a1bf6f01ae43fb41b5c3a3700d053c"
|
|
30
|
+
|
|
31
|
+
_CAPABILITY_CONFIGS = {
|
|
32
|
+
WARPX_CPU_CAPABILITY: "warpx-cpu-26.07.json",
|
|
33
|
+
WARPX_CUDA_CAPABILITY: "warpx-cuda-openpmd-26.07.json",
|
|
34
|
+
}
|
|
35
|
+
_CORE_DISTRIBUTIONS = (
|
|
36
|
+
"simjecture",
|
|
37
|
+
"httpx",
|
|
38
|
+
"matplotlib",
|
|
39
|
+
"numpy",
|
|
40
|
+
"pandas",
|
|
41
|
+
"pydantic",
|
|
42
|
+
"scipy",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DeploymentProfile(StrEnum):
|
|
47
|
+
CORE = "core"
|
|
48
|
+
WARPX_CPU = "warpx-cpu"
|
|
49
|
+
WARPX_CUDA = "warpx-cuda"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_PROFILE_BY_CAPABILITY = {
|
|
53
|
+
WARPX_CPU_CAPABILITY: DeploymentProfile.WARPX_CPU,
|
|
54
|
+
WARPX_CUDA_CAPABILITY: DeploymentProfile.WARPX_CUDA,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DeploymentCheckStatus(StrEnum):
|
|
59
|
+
PASS = "pass"
|
|
60
|
+
FAIL = "fail"
|
|
61
|
+
WARNING = "warning"
|
|
62
|
+
PLANNED = "planned"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DeploymentCheck(StrictModel):
|
|
66
|
+
name: str = Field(min_length=1)
|
|
67
|
+
status: DeploymentCheckStatus
|
|
68
|
+
required: bool = True
|
|
69
|
+
detail: str = Field(min_length=1)
|
|
70
|
+
remedy: str | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DeploymentReport(StrictModel):
|
|
74
|
+
schema_version: Literal["0.1.0"] = "0.1.0"
|
|
75
|
+
profile: str = Field(min_length=1)
|
|
76
|
+
project_root: str = Field(min_length=1)
|
|
77
|
+
ready: bool
|
|
78
|
+
planned: bool = False
|
|
79
|
+
changed: bool = False
|
|
80
|
+
command: tuple[str, ...] = ()
|
|
81
|
+
checks: tuple[DeploymentCheck, ...]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_project_root(explicit: str | Path | None = None) -> Path:
|
|
85
|
+
"""Find the source checkout that owns deployment assets."""
|
|
86
|
+
|
|
87
|
+
if explicit is not None:
|
|
88
|
+
candidates = (Path(explicit).resolve(),)
|
|
89
|
+
else:
|
|
90
|
+
current = Path.cwd().resolve()
|
|
91
|
+
source_fallback = Path(__file__).resolve().parents[2]
|
|
92
|
+
candidates = (current, *current.parents, source_fallback)
|
|
93
|
+
|
|
94
|
+
seen: set[Path] = set()
|
|
95
|
+
for candidate in candidates:
|
|
96
|
+
if candidate in seen:
|
|
97
|
+
continue
|
|
98
|
+
seen.add(candidate)
|
|
99
|
+
if all(
|
|
100
|
+
(candidate / marker).exists()
|
|
101
|
+
for marker in ("pyproject.toml", "skills", "capabilities", "environments")
|
|
102
|
+
):
|
|
103
|
+
return candidate
|
|
104
|
+
requested = str(explicit) if explicit is not None else str(Path.cwd())
|
|
105
|
+
raise FileNotFoundError(
|
|
106
|
+
"deployment requires a source checkout containing pyproject.toml, skills/, "
|
|
107
|
+
f"capabilities/, and environments/; none was found from {requested}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _check(
|
|
112
|
+
name: str,
|
|
113
|
+
status: DeploymentCheckStatus,
|
|
114
|
+
detail: str,
|
|
115
|
+
*,
|
|
116
|
+
required: bool = True,
|
|
117
|
+
remedy: str | None = None,
|
|
118
|
+
) -> DeploymentCheck:
|
|
119
|
+
return DeploymentCheck(
|
|
120
|
+
name=name,
|
|
121
|
+
status=status,
|
|
122
|
+
required=required,
|
|
123
|
+
detail=" ".join(detail.split()),
|
|
124
|
+
remedy=" ".join(remedy.split()) if remedy else None,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _nested_value(payload: object, dotted_path: str) -> object:
|
|
129
|
+
current = payload
|
|
130
|
+
for part in dotted_path.split("."):
|
|
131
|
+
if not isinstance(current, dict) or part not in current:
|
|
132
|
+
raise KeyError(dotted_path)
|
|
133
|
+
current = current[part]
|
|
134
|
+
return current
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class DeploymentManager:
|
|
138
|
+
"""Provision project-local runtimes without granting authority to an agent."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, project_root: str | Path) -> None:
|
|
141
|
+
self.project_root = resolve_project_root(project_root)
|
|
142
|
+
self.capability_root = self.project_root / "capabilities"
|
|
143
|
+
self.runtime_root = self.project_root / ".runtime"
|
|
144
|
+
|
|
145
|
+
def _core_checks(self, *, probe: bool) -> list[DeploymentCheck]:
|
|
146
|
+
checks: list[DeploymentCheck] = []
|
|
147
|
+
supported_python = sys.version_info >= (3, 11)
|
|
148
|
+
checks.append(
|
|
149
|
+
_check(
|
|
150
|
+
"core.python",
|
|
151
|
+
DeploymentCheckStatus.PASS if supported_python else DeploymentCheckStatus.FAIL,
|
|
152
|
+
f"Python {sys.version.split()[0]} at {Path(sys.executable).resolve()}",
|
|
153
|
+
remedy="Install Python 3.11 or newer." if not supported_python else None,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
linux = sys.platform.startswith("linux")
|
|
157
|
+
checks.append(
|
|
158
|
+
_check(
|
|
159
|
+
"core.platform",
|
|
160
|
+
DeploymentCheckStatus.PASS if linux else DeploymentCheckStatus.FAIL,
|
|
161
|
+
f"platform={sys.platform}",
|
|
162
|
+
remedy="The Bubblewrap MVP currently requires Linux." if not linux else None,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
lock_path = self.project_root / "uv.lock"
|
|
166
|
+
locked_versions: dict[str, set[str]] = {}
|
|
167
|
+
try:
|
|
168
|
+
lock = tomllib.loads(lock_path.read_text())
|
|
169
|
+
packages = lock["package"]
|
|
170
|
+
if not isinstance(packages, list):
|
|
171
|
+
raise ValueError("uv.lock package table is not a list")
|
|
172
|
+
for package in packages:
|
|
173
|
+
if not isinstance(package, dict):
|
|
174
|
+
raise ValueError("uv.lock contains a non-table package")
|
|
175
|
+
name = str(package["name"])
|
|
176
|
+
version = str(package["version"])
|
|
177
|
+
locked_versions.setdefault(name, set()).add(version)
|
|
178
|
+
except (OSError, KeyError, TypeError, ValueError, tomllib.TOMLDecodeError) as error:
|
|
179
|
+
checks.append(
|
|
180
|
+
_check(
|
|
181
|
+
"core.lock",
|
|
182
|
+
DeploymentCheckStatus.FAIL,
|
|
183
|
+
f"cannot read the project lock: {error}",
|
|
184
|
+
remedy="Restore uv.lock and run `uv sync --frozen`.",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
checks.append(
|
|
189
|
+
_check(
|
|
190
|
+
"core.lock",
|
|
191
|
+
DeploymentCheckStatus.PASS,
|
|
192
|
+
f"loaded {len(packages)} locked package record(s) from {lock_path}",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
for distribution in _CORE_DISTRIBUTIONS:
|
|
196
|
+
try:
|
|
197
|
+
version = metadata.version(distribution)
|
|
198
|
+
except metadata.PackageNotFoundError:
|
|
199
|
+
checks.append(
|
|
200
|
+
_check(
|
|
201
|
+
f"core.package.{distribution}",
|
|
202
|
+
DeploymentCheckStatus.FAIL,
|
|
203
|
+
f"{distribution} is not installed",
|
|
204
|
+
remedy="Install the locked project environment with `uv sync --frozen`.",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
expected = locked_versions.get(distribution, set())
|
|
209
|
+
matches_lock = version in expected
|
|
210
|
+
checks.append(
|
|
211
|
+
_check(
|
|
212
|
+
f"core.package.{distribution}",
|
|
213
|
+
(
|
|
214
|
+
DeploymentCheckStatus.PASS
|
|
215
|
+
if matches_lock
|
|
216
|
+
else DeploymentCheckStatus.FAIL
|
|
217
|
+
),
|
|
218
|
+
(
|
|
219
|
+
f"{distribution}={version}; "
|
|
220
|
+
f"locked={','.join(sorted(expected)) or 'missing'}"
|
|
221
|
+
),
|
|
222
|
+
remedy=(
|
|
223
|
+
"Synchronize the exact environment with `uv sync --frozen`."
|
|
224
|
+
if not matches_lock
|
|
225
|
+
else None
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
bubblewrap = shutil.which("bwrap")
|
|
231
|
+
checks.append(
|
|
232
|
+
_check(
|
|
233
|
+
"core.bubblewrap",
|
|
234
|
+
DeploymentCheckStatus.PASS if bubblewrap else DeploymentCheckStatus.FAIL,
|
|
235
|
+
f"bubblewrap={bubblewrap}" if bubblewrap else "bubblewrap is unavailable",
|
|
236
|
+
remedy=(
|
|
237
|
+
"Install the Bubblewrap operating-system package before autonomous runs."
|
|
238
|
+
if not bubblewrap
|
|
239
|
+
else None
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
if bubblewrap and probe:
|
|
244
|
+
command = [
|
|
245
|
+
bubblewrap,
|
|
246
|
+
"--unshare-all",
|
|
247
|
+
"--die-with-parent",
|
|
248
|
+
"--new-session",
|
|
249
|
+
]
|
|
250
|
+
for system_root in ("/usr", "/lib", "/lib64"):
|
|
251
|
+
if Path(system_root).exists():
|
|
252
|
+
command.extend(("--ro-bind", system_root, system_root))
|
|
253
|
+
command.extend(("--dev", "/dev", "--proc", "/proc", "/usr/bin/true"))
|
|
254
|
+
try:
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
command,
|
|
257
|
+
check=False,
|
|
258
|
+
capture_output=True,
|
|
259
|
+
text=True,
|
|
260
|
+
timeout=20,
|
|
261
|
+
)
|
|
262
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
263
|
+
result_detail = str(error)
|
|
264
|
+
returncode = 1
|
|
265
|
+
else:
|
|
266
|
+
result_detail = result.stderr.strip() or "namespace creation succeeded"
|
|
267
|
+
returncode = result.returncode
|
|
268
|
+
checks.append(
|
|
269
|
+
_check(
|
|
270
|
+
"core.bubblewrap_namespace",
|
|
271
|
+
(
|
|
272
|
+
DeploymentCheckStatus.PASS
|
|
273
|
+
if returncode == 0
|
|
274
|
+
else DeploymentCheckStatus.FAIL
|
|
275
|
+
),
|
|
276
|
+
result_detail,
|
|
277
|
+
remedy=(
|
|
278
|
+
"Enable unprivileged user namespaces for Bubblewrap on this host."
|
|
279
|
+
if returncode != 0
|
|
280
|
+
else None
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
return checks
|
|
285
|
+
|
|
286
|
+
def _capability_config_path(self, capability: str) -> Path:
|
|
287
|
+
try:
|
|
288
|
+
name = _CAPABILITY_CONFIGS[capability]
|
|
289
|
+
except KeyError as error:
|
|
290
|
+
raise ValueError(f"unknown deployment capability {capability!r}") from error
|
|
291
|
+
return self.capability_root / name
|
|
292
|
+
|
|
293
|
+
def _configured_runtime_root(self, capability: str) -> Path:
|
|
294
|
+
path = self._capability_config_path(capability)
|
|
295
|
+
config = MVPCapabilityConfig.model_validate_json(path.read_bytes())
|
|
296
|
+
return (path.parent / config.runtime_root).resolve()
|
|
297
|
+
|
|
298
|
+
def _skill_resource(self, installation: MVPCapabilityInstallation) -> Path:
|
|
299
|
+
relative = Path(installation.manifest.preflight_resource or "")
|
|
300
|
+
path = self.project_root / "skills" / installation.manifest.skill / relative
|
|
301
|
+
if not path.is_file():
|
|
302
|
+
raise FileNotFoundError(f"capability preflight resource is unavailable: {path}")
|
|
303
|
+
return path
|
|
304
|
+
|
|
305
|
+
def _probe_capability(self, installation: MVPCapabilityInstallation) -> str:
|
|
306
|
+
manifest = installation.manifest
|
|
307
|
+
if not manifest.preflight_resource or not manifest.preflight_result:
|
|
308
|
+
return "capability has no declared preflight"
|
|
309
|
+
source = self._skill_resource(installation)
|
|
310
|
+
with tempfile.TemporaryDirectory(prefix="acs-capability-doctor-") as temporary:
|
|
311
|
+
workspace = Path(temporary)
|
|
312
|
+
program = workspace / manifest.preflight_resource
|
|
313
|
+
program.parent.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
shutil.copy2(source, program)
|
|
315
|
+
registry = MVPCapabilityRegistry((installation,))
|
|
316
|
+
sandbox = BubblewrapSandbox(
|
|
317
|
+
workspace,
|
|
318
|
+
MVPAgentConfig(
|
|
319
|
+
max_iterations=1,
|
|
320
|
+
max_wall_seconds=180,
|
|
321
|
+
max_command_seconds=120,
|
|
322
|
+
max_workspace_bytes=128 * 1024 * 1024,
|
|
323
|
+
max_file_bytes=64 * 1024 * 1024,
|
|
324
|
+
max_memory_bytes=4 * 1024 * 1024 * 1024,
|
|
325
|
+
),
|
|
326
|
+
registry,
|
|
327
|
+
)
|
|
328
|
+
result = sandbox.run_capability(
|
|
329
|
+
manifest.name,
|
|
330
|
+
(manifest.preflight_resource,),
|
|
331
|
+
timeout_seconds=120,
|
|
332
|
+
)
|
|
333
|
+
if result.returncode != 0:
|
|
334
|
+
detail = result.stderr.strip() or result.stdout.strip() or "no diagnostic output"
|
|
335
|
+
raise RuntimeError(
|
|
336
|
+
f"preflight command exited with {result.returncode}: {detail}"
|
|
337
|
+
)
|
|
338
|
+
result_path = workspace / manifest.preflight_result
|
|
339
|
+
if not result_path.is_file():
|
|
340
|
+
raise RuntimeError(
|
|
341
|
+
f"preflight did not create {manifest.preflight_result!r}"
|
|
342
|
+
)
|
|
343
|
+
payload = json.loads(result_path.read_text())
|
|
344
|
+
failed = [
|
|
345
|
+
path
|
|
346
|
+
for path in manifest.preflight_checks
|
|
347
|
+
if _nested_value(payload, path) is not True
|
|
348
|
+
]
|
|
349
|
+
if failed:
|
|
350
|
+
raise RuntimeError(f"preflight checks were false: {', '.join(failed)}")
|
|
351
|
+
return (
|
|
352
|
+
f"{manifest.name} passed {len(manifest.preflight_checks)} declared "
|
|
353
|
+
"preflight check(s)"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _capability_checks(
|
|
357
|
+
self,
|
|
358
|
+
capability: str,
|
|
359
|
+
*,
|
|
360
|
+
required: bool,
|
|
361
|
+
probe: bool,
|
|
362
|
+
) -> tuple[list[DeploymentCheck], MVPCapabilityInstallation | None]:
|
|
363
|
+
status_on_failure = (
|
|
364
|
+
DeploymentCheckStatus.FAIL if required else DeploymentCheckStatus.WARNING
|
|
365
|
+
)
|
|
366
|
+
config_path = self._capability_config_path(capability)
|
|
367
|
+
if not config_path.is_file():
|
|
368
|
+
return (
|
|
369
|
+
[
|
|
370
|
+
_check(
|
|
371
|
+
f"capability.{capability}.configuration",
|
|
372
|
+
status_on_failure,
|
|
373
|
+
f"missing capability manifest {config_path}",
|
|
374
|
+
required=required,
|
|
375
|
+
remedy="Restore the versioned capability manifest from the repository.",
|
|
376
|
+
)
|
|
377
|
+
],
|
|
378
|
+
None,
|
|
379
|
+
)
|
|
380
|
+
try:
|
|
381
|
+
installation = MVPCapabilityInstallation.read(config_path)
|
|
382
|
+
except (OSError, ValueError) as error:
|
|
383
|
+
return (
|
|
384
|
+
[
|
|
385
|
+
_check(
|
|
386
|
+
f"capability.{capability}.runtime",
|
|
387
|
+
status_on_failure,
|
|
388
|
+
str(error),
|
|
389
|
+
required=required,
|
|
390
|
+
remedy=(
|
|
391
|
+
"Run `simjecture install "
|
|
392
|
+
f"{_PROFILE_BY_CAPABILITY[capability].value}`."
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
],
|
|
396
|
+
None,
|
|
397
|
+
)
|
|
398
|
+
checks = [
|
|
399
|
+
_check(
|
|
400
|
+
f"capability.{capability}.runtime",
|
|
401
|
+
DeploymentCheckStatus.PASS,
|
|
402
|
+
(
|
|
403
|
+
f"runtime={installation.runtime_root}; "
|
|
404
|
+
f"contract_sha256={installation.contract_hash}"
|
|
405
|
+
),
|
|
406
|
+
required=required,
|
|
407
|
+
)
|
|
408
|
+
]
|
|
409
|
+
if probe:
|
|
410
|
+
try:
|
|
411
|
+
detail = self._probe_capability(installation)
|
|
412
|
+
except (OSError, ValueError, KeyError, RuntimeError, json.JSONDecodeError) as error:
|
|
413
|
+
if capability == WARPX_CPU_CAPABILITY:
|
|
414
|
+
remedy = (
|
|
415
|
+
"Inspect the runtime and rerun the CPU profile installer with --repair."
|
|
416
|
+
)
|
|
417
|
+
else:
|
|
418
|
+
remedy = (
|
|
419
|
+
"Preserve or move the CUDA runtime and dependency roots, then perform "
|
|
420
|
+
"a fresh audited installation."
|
|
421
|
+
)
|
|
422
|
+
checks.append(
|
|
423
|
+
_check(
|
|
424
|
+
f"capability.{capability}.preflight",
|
|
425
|
+
status_on_failure,
|
|
426
|
+
str(error),
|
|
427
|
+
required=required,
|
|
428
|
+
remedy=remedy,
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
checks.append(
|
|
433
|
+
_check(
|
|
434
|
+
f"capability.{capability}.preflight",
|
|
435
|
+
DeploymentCheckStatus.PASS,
|
|
436
|
+
detail,
|
|
437
|
+
required=required,
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
return checks, installation
|
|
441
|
+
|
|
442
|
+
@staticmethod
|
|
443
|
+
def _ready(checks: list[DeploymentCheck]) -> bool:
|
|
444
|
+
return not any(
|
|
445
|
+
item.required and item.status is DeploymentCheckStatus.FAIL for item in checks
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def doctor(self, profile: str = "all", *, probe: bool = True) -> DeploymentReport:
|
|
449
|
+
if profile not in {"all", *(item.value for item in DeploymentProfile)}:
|
|
450
|
+
raise ValueError(f"unknown deployment profile {profile!r}")
|
|
451
|
+
checks = self._core_checks(probe=probe)
|
|
452
|
+
capabilities: tuple[tuple[str, bool], ...]
|
|
453
|
+
if profile == DeploymentProfile.WARPX_CPU:
|
|
454
|
+
capabilities = ((WARPX_CPU_CAPABILITY, True),)
|
|
455
|
+
elif profile == DeploymentProfile.WARPX_CUDA:
|
|
456
|
+
capabilities = ((WARPX_CUDA_CAPABILITY, True),)
|
|
457
|
+
elif profile == "all":
|
|
458
|
+
capabilities = (
|
|
459
|
+
(WARPX_CPU_CAPABILITY, False),
|
|
460
|
+
(WARPX_CUDA_CAPABILITY, False),
|
|
461
|
+
)
|
|
462
|
+
else:
|
|
463
|
+
capabilities = ()
|
|
464
|
+
for capability, required in capabilities:
|
|
465
|
+
capability_checks, _ = self._capability_checks(
|
|
466
|
+
capability,
|
|
467
|
+
required=required,
|
|
468
|
+
probe=probe,
|
|
469
|
+
)
|
|
470
|
+
checks.extend(capability_checks)
|
|
471
|
+
return DeploymentReport(
|
|
472
|
+
profile=profile,
|
|
473
|
+
project_root=str(self.project_root),
|
|
474
|
+
ready=self._ready(checks),
|
|
475
|
+
checks=tuple(checks),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def _write_report(self, report: DeploymentReport) -> None:
|
|
479
|
+
directory = self.runtime_root / "deployment"
|
|
480
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
481
|
+
destination = directory / f"{report.profile}.json"
|
|
482
|
+
temporary = destination.with_suffix(".json.tmp")
|
|
483
|
+
temporary.write_text(report.model_dump_json(indent=2) + "\n")
|
|
484
|
+
os.replace(temporary, destination)
|
|
485
|
+
|
|
486
|
+
def _already_ready(self, profile: DeploymentProfile) -> DeploymentReport | None:
|
|
487
|
+
report = self.doctor(profile.value, probe=True)
|
|
488
|
+
return report if report.ready else None
|
|
489
|
+
|
|
490
|
+
def _install_cpu(
|
|
491
|
+
self,
|
|
492
|
+
*,
|
|
493
|
+
dry_run: bool,
|
|
494
|
+
repair: bool,
|
|
495
|
+
environment_manager: str | None,
|
|
496
|
+
capture_output: bool,
|
|
497
|
+
) -> DeploymentReport:
|
|
498
|
+
ready = self._already_ready(DeploymentProfile.WARPX_CPU)
|
|
499
|
+
if ready is not None:
|
|
500
|
+
self._write_report(ready)
|
|
501
|
+
return ready
|
|
502
|
+
checks = self._core_checks(probe=True)
|
|
503
|
+
if not self._ready(checks):
|
|
504
|
+
return DeploymentReport(
|
|
505
|
+
profile=DeploymentProfile.WARPX_CPU,
|
|
506
|
+
project_root=str(self.project_root),
|
|
507
|
+
ready=False,
|
|
508
|
+
checks=tuple(checks),
|
|
509
|
+
)
|
|
510
|
+
prefix = self._configured_runtime_root(WARPX_CPU_CAPABILITY)
|
|
511
|
+
managed_prefix = (prefix / "conda-meta").is_dir()
|
|
512
|
+
if prefix.exists() and not repair:
|
|
513
|
+
checks.append(
|
|
514
|
+
_check(
|
|
515
|
+
"install.warpx-cpu.existing_runtime",
|
|
516
|
+
DeploymentCheckStatus.FAIL,
|
|
517
|
+
f"an existing but unhealthy runtime was left unchanged at {prefix}",
|
|
518
|
+
remedy="Inspect it, then rerun with --repair to update a Conda-managed prefix.",
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
return DeploymentReport(
|
|
522
|
+
profile=DeploymentProfile.WARPX_CPU,
|
|
523
|
+
project_root=str(self.project_root),
|
|
524
|
+
ready=False,
|
|
525
|
+
checks=tuple(checks),
|
|
526
|
+
)
|
|
527
|
+
if prefix.exists() and repair and not managed_prefix:
|
|
528
|
+
checks.append(
|
|
529
|
+
_check(
|
|
530
|
+
"install.warpx-cpu.existing_runtime",
|
|
531
|
+
DeploymentCheckStatus.FAIL,
|
|
532
|
+
f"refusing to modify a non-Conda directory at {prefix}",
|
|
533
|
+
remedy="Move the directory aside and run the installer again.",
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
return DeploymentReport(
|
|
537
|
+
profile=DeploymentProfile.WARPX_CPU,
|
|
538
|
+
project_root=str(self.project_root),
|
|
539
|
+
ready=False,
|
|
540
|
+
checks=tuple(checks),
|
|
541
|
+
)
|
|
542
|
+
manager = (
|
|
543
|
+
shutil.which(environment_manager)
|
|
544
|
+
if environment_manager
|
|
545
|
+
else shutil.which("micromamba") or shutil.which("mamba")
|
|
546
|
+
)
|
|
547
|
+
if not manager:
|
|
548
|
+
checks.append(
|
|
549
|
+
_check(
|
|
550
|
+
"install.warpx-cpu.environment_manager",
|
|
551
|
+
DeploymentCheckStatus.FAIL,
|
|
552
|
+
"neither micromamba nor mamba is available",
|
|
553
|
+
remedy="Install Miniforge, Mambaforge, or Micromamba.",
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
return DeploymentReport(
|
|
557
|
+
profile=DeploymentProfile.WARPX_CPU,
|
|
558
|
+
project_root=str(self.project_root),
|
|
559
|
+
ready=False,
|
|
560
|
+
checks=tuple(checks),
|
|
561
|
+
)
|
|
562
|
+
environment_file = self.project_root / "environments" / "warpx-cpu.yml"
|
|
563
|
+
action = "update" if managed_prefix and repair else "create"
|
|
564
|
+
if action == "update":
|
|
565
|
+
command = (
|
|
566
|
+
manager,
|
|
567
|
+
"env",
|
|
568
|
+
"update",
|
|
569
|
+
"--yes",
|
|
570
|
+
"--prefix",
|
|
571
|
+
str(prefix),
|
|
572
|
+
"--file",
|
|
573
|
+
str(environment_file),
|
|
574
|
+
"--prune",
|
|
575
|
+
)
|
|
576
|
+
else:
|
|
577
|
+
command = (
|
|
578
|
+
manager,
|
|
579
|
+
"create",
|
|
580
|
+
"--yes",
|
|
581
|
+
"--prefix",
|
|
582
|
+
str(prefix),
|
|
583
|
+
"--file",
|
|
584
|
+
str(environment_file),
|
|
585
|
+
)
|
|
586
|
+
if dry_run:
|
|
587
|
+
checks.append(
|
|
588
|
+
_check(
|
|
589
|
+
"install.warpx-cpu.command",
|
|
590
|
+
DeploymentCheckStatus.PLANNED,
|
|
591
|
+
" ".join(command),
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
return DeploymentReport(
|
|
595
|
+
profile=DeploymentProfile.WARPX_CPU,
|
|
596
|
+
project_root=str(self.project_root),
|
|
597
|
+
ready=False,
|
|
598
|
+
planned=True,
|
|
599
|
+
command=command,
|
|
600
|
+
checks=tuple(checks),
|
|
601
|
+
)
|
|
602
|
+
result = subprocess.run(
|
|
603
|
+
command,
|
|
604
|
+
check=False,
|
|
605
|
+
text=True,
|
|
606
|
+
capture_output=capture_output,
|
|
607
|
+
)
|
|
608
|
+
if result.returncode != 0:
|
|
609
|
+
detail = result.stderr.strip() if capture_output else "environment manager failed"
|
|
610
|
+
checks.append(
|
|
611
|
+
_check(
|
|
612
|
+
"install.warpx-cpu.command",
|
|
613
|
+
DeploymentCheckStatus.FAIL,
|
|
614
|
+
detail or "environment manager failed without diagnostic output",
|
|
615
|
+
remedy="Review the Conda solver output; the existing prefix was not deleted.",
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
return DeploymentReport(
|
|
619
|
+
profile=DeploymentProfile.WARPX_CPU,
|
|
620
|
+
project_root=str(self.project_root),
|
|
621
|
+
ready=False,
|
|
622
|
+
command=command,
|
|
623
|
+
checks=tuple(checks),
|
|
624
|
+
)
|
|
625
|
+
report = self.doctor(DeploymentProfile.WARPX_CPU, probe=True)
|
|
626
|
+
report = report.model_copy(update={"changed": True, "command": command})
|
|
627
|
+
self._write_report(report)
|
|
628
|
+
return report
|
|
629
|
+
|
|
630
|
+
def _source_revision_check(self, source: Path) -> DeploymentCheck:
|
|
631
|
+
git = shutil.which("git")
|
|
632
|
+
if not source.is_dir():
|
|
633
|
+
return _check(
|
|
634
|
+
"install.warpx-cuda.source",
|
|
635
|
+
DeploymentCheckStatus.FAIL,
|
|
636
|
+
f"WarpX source directory is unavailable: {source}",
|
|
637
|
+
remedy=f"Check out WarpX revision {PINNED_WARPX_REVISION}.",
|
|
638
|
+
)
|
|
639
|
+
if not git:
|
|
640
|
+
return _check(
|
|
641
|
+
"install.warpx-cuda.source",
|
|
642
|
+
DeploymentCheckStatus.FAIL,
|
|
643
|
+
"git is unavailable, so the WarpX source revision cannot be verified",
|
|
644
|
+
remedy="Install Git and use the audited WarpX source checkout.",
|
|
645
|
+
)
|
|
646
|
+
result = subprocess.run(
|
|
647
|
+
(git, "-C", str(source), "rev-parse", "HEAD"),
|
|
648
|
+
check=False,
|
|
649
|
+
capture_output=True,
|
|
650
|
+
text=True,
|
|
651
|
+
)
|
|
652
|
+
observed = result.stdout.strip()
|
|
653
|
+
if result.returncode != 0 or observed != PINNED_WARPX_REVISION:
|
|
654
|
+
return _check(
|
|
655
|
+
"install.warpx-cuda.source",
|
|
656
|
+
DeploymentCheckStatus.FAIL,
|
|
657
|
+
f"observed source revision={observed or 'unavailable'}",
|
|
658
|
+
remedy=f"Use the audited WarpX revision {PINNED_WARPX_REVISION}.",
|
|
659
|
+
)
|
|
660
|
+
return _check(
|
|
661
|
+
"install.warpx-cuda.source",
|
|
662
|
+
DeploymentCheckStatus.PASS,
|
|
663
|
+
f"WarpX source revision={observed}",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
def _install_cuda(
|
|
667
|
+
self,
|
|
668
|
+
*,
|
|
669
|
+
source: str | Path | None,
|
|
670
|
+
jobs: int,
|
|
671
|
+
arch: str | None,
|
|
672
|
+
dry_run: bool,
|
|
673
|
+
capture_output: bool,
|
|
674
|
+
) -> DeploymentReport:
|
|
675
|
+
ready = self._already_ready(DeploymentProfile.WARPX_CUDA)
|
|
676
|
+
if ready is not None:
|
|
677
|
+
self._write_report(ready)
|
|
678
|
+
return ready
|
|
679
|
+
checks = self._core_checks(probe=True)
|
|
680
|
+
prefix = self._configured_runtime_root(WARPX_CUDA_CAPABILITY)
|
|
681
|
+
cuda_roots = (
|
|
682
|
+
prefix,
|
|
683
|
+
self.runtime_root / "cuda-toolkit-12.4",
|
|
684
|
+
self.runtime_root / "warpx-cuda-openpmd-deps",
|
|
685
|
+
)
|
|
686
|
+
existing_roots = tuple(path for path in cuda_roots if path.exists())
|
|
687
|
+
if existing_roots:
|
|
688
|
+
checks.append(
|
|
689
|
+
_check(
|
|
690
|
+
"install.warpx-cuda.existing_runtime",
|
|
691
|
+
DeploymentCheckStatus.FAIL,
|
|
692
|
+
(
|
|
693
|
+
"existing but unhealthy CUDA deployment roots were left unchanged: "
|
|
694
|
+
+ ", ".join(str(path) for path in existing_roots)
|
|
695
|
+
),
|
|
696
|
+
remedy=(
|
|
697
|
+
"Move the runtime and its project-local dependency prefixes aside, "
|
|
698
|
+
"then run a fresh audited installation."
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
)
|
|
702
|
+
if source is None:
|
|
703
|
+
checks.append(
|
|
704
|
+
_check(
|
|
705
|
+
"install.warpx-cuda.source",
|
|
706
|
+
DeploymentCheckStatus.FAIL,
|
|
707
|
+
"--source is required for a new CUDA build",
|
|
708
|
+
remedy=f"Supply a WarpX checkout at revision {PINNED_WARPX_REVISION}.",
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
else:
|
|
712
|
+
checks.append(self._source_revision_check(Path(source).resolve()))
|
|
713
|
+
if jobs < 1:
|
|
714
|
+
checks.append(
|
|
715
|
+
_check(
|
|
716
|
+
"install.warpx-cuda.jobs",
|
|
717
|
+
DeploymentCheckStatus.FAIL,
|
|
718
|
+
f"jobs={jobs}",
|
|
719
|
+
remedy="Choose at least one build job.",
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
if arch is not None and not arch.isdigit():
|
|
723
|
+
checks.append(
|
|
724
|
+
_check(
|
|
725
|
+
"install.warpx-cuda.arch",
|
|
726
|
+
DeploymentCheckStatus.FAIL,
|
|
727
|
+
f"invalid CUDA architecture={arch!r}",
|
|
728
|
+
remedy="Use digits such as 89, or omit --arch for detection.",
|
|
729
|
+
)
|
|
730
|
+
)
|
|
731
|
+
if not self._ready(checks):
|
|
732
|
+
return DeploymentReport(
|
|
733
|
+
profile=DeploymentProfile.WARPX_CUDA,
|
|
734
|
+
project_root=str(self.project_root),
|
|
735
|
+
ready=False,
|
|
736
|
+
checks=tuple(checks),
|
|
737
|
+
)
|
|
738
|
+
assert source is not None
|
|
739
|
+
bootstrap = self.project_root / "skills" / "warpx" / "scripts" / "bootstrap_local_cuda.sh"
|
|
740
|
+
if not bootstrap.is_file() or not os.access(bootstrap, os.X_OK):
|
|
741
|
+
checks.append(
|
|
742
|
+
_check(
|
|
743
|
+
"install.warpx-cuda.bootstrap",
|
|
744
|
+
DeploymentCheckStatus.FAIL,
|
|
745
|
+
f"CUDA bootstrap is unavailable or not executable: {bootstrap}",
|
|
746
|
+
remedy="Restore the versioned WarpX skill scripts.",
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
return DeploymentReport(
|
|
750
|
+
profile=DeploymentProfile.WARPX_CUDA,
|
|
751
|
+
project_root=str(self.project_root),
|
|
752
|
+
ready=False,
|
|
753
|
+
checks=tuple(checks),
|
|
754
|
+
)
|
|
755
|
+
command = (
|
|
756
|
+
str(bootstrap),
|
|
757
|
+
"--source",
|
|
758
|
+
str(Path(source).resolve()),
|
|
759
|
+
"--jobs",
|
|
760
|
+
str(jobs),
|
|
761
|
+
*(("--arch", arch) if arch is not None else ()),
|
|
762
|
+
)
|
|
763
|
+
if dry_run:
|
|
764
|
+
checks.append(
|
|
765
|
+
_check(
|
|
766
|
+
"install.warpx-cuda.command",
|
|
767
|
+
DeploymentCheckStatus.PLANNED,
|
|
768
|
+
" ".join(command),
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
return DeploymentReport(
|
|
772
|
+
profile=DeploymentProfile.WARPX_CUDA,
|
|
773
|
+
project_root=str(self.project_root),
|
|
774
|
+
ready=False,
|
|
775
|
+
planned=True,
|
|
776
|
+
command=command,
|
|
777
|
+
checks=tuple(checks),
|
|
778
|
+
)
|
|
779
|
+
result = subprocess.run(
|
|
780
|
+
command,
|
|
781
|
+
check=False,
|
|
782
|
+
text=True,
|
|
783
|
+
capture_output=capture_output,
|
|
784
|
+
)
|
|
785
|
+
if result.returncode != 0:
|
|
786
|
+
detail = result.stderr.strip() if capture_output else "CUDA bootstrap failed"
|
|
787
|
+
checks.append(
|
|
788
|
+
_check(
|
|
789
|
+
"install.warpx-cuda.command",
|
|
790
|
+
DeploymentCheckStatus.FAIL,
|
|
791
|
+
detail or "CUDA bootstrap failed without diagnostic output",
|
|
792
|
+
remedy="Preserve the build log and inspect the documented failure signatures.",
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
return DeploymentReport(
|
|
796
|
+
profile=DeploymentProfile.WARPX_CUDA,
|
|
797
|
+
project_root=str(self.project_root),
|
|
798
|
+
ready=False,
|
|
799
|
+
command=command,
|
|
800
|
+
checks=tuple(checks),
|
|
801
|
+
)
|
|
802
|
+
report = self.doctor(DeploymentProfile.WARPX_CUDA, probe=True)
|
|
803
|
+
report = report.model_copy(update={"changed": True, "command": command})
|
|
804
|
+
self._write_report(report)
|
|
805
|
+
return report
|
|
806
|
+
|
|
807
|
+
def install(
|
|
808
|
+
self,
|
|
809
|
+
profile: DeploymentProfile,
|
|
810
|
+
*,
|
|
811
|
+
dry_run: bool = False,
|
|
812
|
+
repair: bool = False,
|
|
813
|
+
environment_manager: str | None = None,
|
|
814
|
+
source: str | Path | None = None,
|
|
815
|
+
jobs: int = 8,
|
|
816
|
+
arch: str | None = None,
|
|
817
|
+
capture_output: bool = False,
|
|
818
|
+
) -> DeploymentReport:
|
|
819
|
+
if profile is DeploymentProfile.CORE:
|
|
820
|
+
report = self.doctor(DeploymentProfile.CORE, probe=True)
|
|
821
|
+
if not dry_run:
|
|
822
|
+
self._write_report(report)
|
|
823
|
+
return report
|
|
824
|
+
if profile is DeploymentProfile.WARPX_CPU:
|
|
825
|
+
return self._install_cpu(
|
|
826
|
+
dry_run=dry_run,
|
|
827
|
+
repair=repair,
|
|
828
|
+
environment_manager=environment_manager,
|
|
829
|
+
capture_output=capture_output,
|
|
830
|
+
)
|
|
831
|
+
if repair:
|
|
832
|
+
checks = self._core_checks(probe=True)
|
|
833
|
+
checks.append(
|
|
834
|
+
_check(
|
|
835
|
+
"install.warpx-cuda.repair",
|
|
836
|
+
DeploymentCheckStatus.FAIL,
|
|
837
|
+
"automatic in-place CUDA repair is intentionally unavailable",
|
|
838
|
+
remedy=(
|
|
839
|
+
"Preserve or move the existing instrument and dependency roots, "
|
|
840
|
+
"then perform a fresh audited build."
|
|
841
|
+
),
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
return DeploymentReport(
|
|
845
|
+
profile=DeploymentProfile.WARPX_CUDA,
|
|
846
|
+
project_root=str(self.project_root),
|
|
847
|
+
ready=False,
|
|
848
|
+
checks=tuple(checks),
|
|
849
|
+
)
|
|
850
|
+
return self._install_cuda(
|
|
851
|
+
source=source,
|
|
852
|
+
jobs=jobs,
|
|
853
|
+
arch=arch,
|
|
854
|
+
dry_run=dry_run,
|
|
855
|
+
capture_output=capture_output,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def print_deployment_report(report: DeploymentReport, *, as_json: bool) -> None:
|
|
860
|
+
if as_json:
|
|
861
|
+
print(report.model_dump_json(indent=2))
|
|
862
|
+
return
|
|
863
|
+
print(f"profile={report.profile}")
|
|
864
|
+
print(f"ready={str(report.ready).lower()}")
|
|
865
|
+
print(f"planned={str(report.planned).lower()}")
|
|
866
|
+
print(f"changed={str(report.changed).lower()}")
|
|
867
|
+
print(f"project_root={report.project_root}")
|
|
868
|
+
for item in report.checks:
|
|
869
|
+
print(
|
|
870
|
+
f"check={item.name} status={item.status.value} "
|
|
871
|
+
f"required={str(item.required).lower()} detail={item.detail}"
|
|
872
|
+
)
|
|
873
|
+
if item.remedy:
|
|
874
|
+
print(f"remedy={item.name}: {item.remedy}")
|