dsh-math-modeling-agent 0.2.8 → 0.4.1
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.
- package/README.md +16 -1
- package/package.json +1 -1
- package/skills/math-modeling-agent/SKILL.md +27 -14
- package/skills/math-modeling-agent/references/claims-evidence.md +3 -1
- package/skills/math-modeling-agent/references/interaction-protocol.md +13 -1
- package/skills/math-modeling-agent/references/math-grill.md +8 -0
- package/skills/math-modeling-agent/references/original-project-parity.md +19 -0
- package/skills/math-modeling-agent/references/report-contract.md +6 -0
- package/skills/math-modeling-agent/references/subagent-dispatch.md +3 -2
- package/skills/math-modeling-agent/references/tool-policy.md +11 -0
- package/skills/math-modeling-agent/references/workflow.md +32 -9
- package/skills/math-modeling-agent/scripts/computation/README.md +20 -0
- package/skills/math-modeling-agent/scripts/computation/backend-inventory.schema.json +78 -0
- package/skills/math-modeling-agent/scripts/computation/backend_inventory.ps1 +351 -0
- package/skills/math-modeling-agent/scripts/computation/backend_inventory.py +322 -0
- package/skills/math-modeling-agent/scripts/computation/computation_record.py +361 -0
- package/skills/math-modeling-agent/scripts/computation/probe_backends.ps1 +396 -0
- package/skills/math-modeling-agent/scripts/computation/probe_backends.py +230 -0
- package/skills/math-modeling-agent/scripts/distribution-parity.mjs +123 -0
- package/skills/math-modeling-agent/scripts/run-state.mjs +34 -7
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Initialize and validate reproducible mathematics/science computation records."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SCHEMA_VERSION = "1.0"
|
|
19
|
+
BACKENDS = {"mathematica", "primecount", "sagemath", "python", "other"}
|
|
20
|
+
AVAILABILITY = {"available", "unavailable", "unknown", "not-applicable"}
|
|
21
|
+
PRECISION_MODES = {"exact", "machine", "arbitrary", "interval", "mixed"}
|
|
22
|
+
EVIDENCE_LEVELS = {
|
|
23
|
+
"proof-certificate",
|
|
24
|
+
"formal-verification",
|
|
25
|
+
"exact-check",
|
|
26
|
+
"bounded-check",
|
|
27
|
+
"numerical-evidence",
|
|
28
|
+
}
|
|
29
|
+
HEX_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RecordError(ValueError):
|
|
33
|
+
"""A computation record is invalid."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def sha256_file(path: Path) -> str:
|
|
37
|
+
digest = hashlib.sha256()
|
|
38
|
+
with path.open("rb") as handle:
|
|
39
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
40
|
+
digest.update(chunk)
|
|
41
|
+
return digest.hexdigest()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_json(path: Path, value: Any) -> None:
|
|
45
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
payload = json.dumps(value, ensure_ascii=False, indent=2) + "\n"
|
|
47
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
48
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
|
|
49
|
+
)
|
|
50
|
+
try:
|
|
51
|
+
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
|
|
52
|
+
handle.write(payload)
|
|
53
|
+
handle.flush()
|
|
54
|
+
os.fsync(handle.fileno())
|
|
55
|
+
os.replace(temporary_name, path)
|
|
56
|
+
except BaseException:
|
|
57
|
+
try:
|
|
58
|
+
Path(temporary_name).unlink()
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
pass
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def relative_inside(path: Path, base_dir: Path, label: str) -> str:
|
|
65
|
+
resolved_path = path.resolve()
|
|
66
|
+
resolved_base = base_dir.resolve()
|
|
67
|
+
try:
|
|
68
|
+
relative = resolved_path.relative_to(resolved_base)
|
|
69
|
+
except ValueError as exc:
|
|
70
|
+
raise RecordError(f"{label} must be inside the record base directory") from exc
|
|
71
|
+
return relative.as_posix()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def resolve_safe_relative(base_dir: Path, raw_path: Any, label: str) -> Path:
|
|
75
|
+
if not isinstance(raw_path, str) or not raw_path.strip():
|
|
76
|
+
raise RecordError(f"{label} must be a nonempty relative path")
|
|
77
|
+
candidate = Path(raw_path)
|
|
78
|
+
if candidate.is_absolute():
|
|
79
|
+
raise RecordError(f"{label} must not be absolute")
|
|
80
|
+
resolved_base = base_dir.resolve()
|
|
81
|
+
resolved = (resolved_base / candidate).resolve()
|
|
82
|
+
try:
|
|
83
|
+
resolved.relative_to(resolved_base)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
raise RecordError(f"{label} escapes the base directory") from exc
|
|
86
|
+
return resolved
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def require_mapping(parent: dict[str, Any], key: str, label: str) -> dict[str, Any]:
|
|
90
|
+
value = parent.get(key)
|
|
91
|
+
if not isinstance(value, dict):
|
|
92
|
+
raise RecordError(f"{label}.{key} must be an object")
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def require_nonempty_string(parent: dict[str, Any], key: str, label: str) -> str:
|
|
97
|
+
value = parent.get(key)
|
|
98
|
+
if not isinstance(value, str) or not value.strip():
|
|
99
|
+
raise RecordError(f"{label}.{key} must be a nonempty string")
|
|
100
|
+
return value.strip()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def require_string_list(parent: dict[str, Any], key: str, label: str) -> list[str]:
|
|
104
|
+
value = parent.get(key)
|
|
105
|
+
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
|
106
|
+
raise RecordError(f"{label}.{key} must be a list of strings")
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_hash(raw_hash: Any, label: str) -> str:
|
|
111
|
+
if not isinstance(raw_hash, str) or not HEX_SHA256.fullmatch(raw_hash):
|
|
112
|
+
raise RecordError(f"{label} must be a lowercase SHA-256 hex digest")
|
|
113
|
+
return raw_hash
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def init_record(task_file: Path, record_path: Path, force: bool) -> None:
|
|
117
|
+
task_file = task_file.resolve()
|
|
118
|
+
record_path = record_path.resolve()
|
|
119
|
+
if not task_file.is_file():
|
|
120
|
+
raise RecordError(f"task file does not exist: {task_file}")
|
|
121
|
+
record_path.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
if record_path.exists() and not force:
|
|
123
|
+
raise RecordError(f"record already exists: {record_path}; pass --force to replace it")
|
|
124
|
+
task_relative = relative_inside(task_file, record_path.parent, "task file")
|
|
125
|
+
|
|
126
|
+
record = {
|
|
127
|
+
"schema_version": SCHEMA_VERSION,
|
|
128
|
+
"created_at_utc": datetime.now(timezone.utc).isoformat(),
|
|
129
|
+
"task": {
|
|
130
|
+
"file": task_relative,
|
|
131
|
+
"sha256": sha256_file(task_file),
|
|
132
|
+
"object": "",
|
|
133
|
+
"deliverables": [],
|
|
134
|
+
},
|
|
135
|
+
"mathematical_context": {
|
|
136
|
+
"assumptions": [],
|
|
137
|
+
"domain": "",
|
|
138
|
+
"precision": {
|
|
139
|
+
"mode": "exact",
|
|
140
|
+
"working_digits": None,
|
|
141
|
+
"target_tolerance": "",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
"implementation_discovery": {
|
|
145
|
+
backend: {
|
|
146
|
+
"candidate_implementations": [],
|
|
147
|
+
"existence_evidence": "",
|
|
148
|
+
"local_availability": "unknown",
|
|
149
|
+
"availability_evidence": "",
|
|
150
|
+
}
|
|
151
|
+
for backend in ("mathematica", "primecount", "sagemath", "python")
|
|
152
|
+
},
|
|
153
|
+
"decision": {
|
|
154
|
+
"selected_backend": "",
|
|
155
|
+
"backend_version": "",
|
|
156
|
+
"selection_reason": "",
|
|
157
|
+
"fallback_reason": "",
|
|
158
|
+
},
|
|
159
|
+
"execution": {
|
|
160
|
+
"status": "planned",
|
|
161
|
+
"interface": "",
|
|
162
|
+
"command_or_input": "",
|
|
163
|
+
"code_artifact": "",
|
|
164
|
+
},
|
|
165
|
+
"artifacts": [],
|
|
166
|
+
"result": {
|
|
167
|
+
"status": "pending",
|
|
168
|
+
"summary": "",
|
|
169
|
+
"result_artifact": "",
|
|
170
|
+
},
|
|
171
|
+
"verification": {
|
|
172
|
+
"methods": [],
|
|
173
|
+
"evidence_level": "",
|
|
174
|
+
"residual_or_error": "",
|
|
175
|
+
"limitations": [],
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
write_json(record_path, record)
|
|
179
|
+
print(json.dumps({"ok": True, "record": str(record_path)}, ensure_ascii=False))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def validate_record(record_path: Path, base_dir: Path | None) -> dict[str, Any]:
|
|
183
|
+
record_path = record_path.resolve()
|
|
184
|
+
if not record_path.is_file():
|
|
185
|
+
raise RecordError(f"record does not exist: {record_path}")
|
|
186
|
+
base = (base_dir or record_path.parent).resolve()
|
|
187
|
+
if not base.is_dir():
|
|
188
|
+
raise RecordError(f"base directory does not exist: {base}")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
record = json.loads(record_path.read_text(encoding="utf-8"))
|
|
192
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
193
|
+
raise RecordError(f"cannot read record JSON: {exc}") from exc
|
|
194
|
+
if not isinstance(record, dict):
|
|
195
|
+
raise RecordError("record root must be an object")
|
|
196
|
+
if record.get("schema_version") != SCHEMA_VERSION:
|
|
197
|
+
raise RecordError(f"schema_version must be {SCHEMA_VERSION}")
|
|
198
|
+
|
|
199
|
+
task = require_mapping(record, "task", "record")
|
|
200
|
+
require_nonempty_string(task, "object", "task")
|
|
201
|
+
deliverables = require_string_list(task, "deliverables", "task")
|
|
202
|
+
if not deliverables:
|
|
203
|
+
raise RecordError("task.deliverables must contain at least one item")
|
|
204
|
+
task_path = resolve_safe_relative(base, task.get("file"), "task.file")
|
|
205
|
+
if not task_path.is_file():
|
|
206
|
+
raise RecordError(f"task.file does not exist: {task_path}")
|
|
207
|
+
task_hash = validate_hash(task.get("sha256"), "task.sha256")
|
|
208
|
+
if sha256_file(task_path) != task_hash:
|
|
209
|
+
raise RecordError("task.sha256 does not match task.file")
|
|
210
|
+
|
|
211
|
+
context = require_mapping(record, "mathematical_context", "record")
|
|
212
|
+
require_string_list(context, "assumptions", "mathematical_context")
|
|
213
|
+
require_nonempty_string(context, "domain", "mathematical_context")
|
|
214
|
+
precision = require_mapping(context, "precision", "mathematical_context")
|
|
215
|
+
mode = require_nonempty_string(precision, "mode", "mathematical_context.precision")
|
|
216
|
+
if mode not in PRECISION_MODES:
|
|
217
|
+
raise RecordError(f"mathematical_context.precision.mode must be one of {sorted(PRECISION_MODES)}")
|
|
218
|
+
working_digits = precision.get("working_digits")
|
|
219
|
+
if working_digits is not None and (not isinstance(working_digits, int) or working_digits <= 0):
|
|
220
|
+
raise RecordError("mathematical_context.precision.working_digits must be null or a positive integer")
|
|
221
|
+
target_tolerance = precision.get("target_tolerance")
|
|
222
|
+
if not isinstance(target_tolerance, str):
|
|
223
|
+
raise RecordError("mathematical_context.precision.target_tolerance must be a string")
|
|
224
|
+
if mode != "exact" and not target_tolerance.strip():
|
|
225
|
+
raise RecordError("numerical precision modes require mathematical_context.precision.target_tolerance")
|
|
226
|
+
|
|
227
|
+
discovery = require_mapping(record, "implementation_discovery", "record")
|
|
228
|
+
for backend in ("mathematica", "primecount", "sagemath", "python"):
|
|
229
|
+
entry = require_mapping(discovery, backend, "implementation_discovery")
|
|
230
|
+
candidates = entry.get("candidate_implementations")
|
|
231
|
+
if not isinstance(candidates, list) or any(not isinstance(item, str) for item in candidates):
|
|
232
|
+
raise RecordError(
|
|
233
|
+
f"implementation_discovery.{backend}.candidate_implementations must be a list of strings"
|
|
234
|
+
)
|
|
235
|
+
require_nonempty_string(entry, "existence_evidence", f"implementation_discovery.{backend}")
|
|
236
|
+
availability = require_nonempty_string(
|
|
237
|
+
entry, "local_availability", f"implementation_discovery.{backend}"
|
|
238
|
+
)
|
|
239
|
+
if availability not in AVAILABILITY:
|
|
240
|
+
raise RecordError(
|
|
241
|
+
f"implementation_discovery.{backend}.local_availability must be one of {sorted(AVAILABILITY)}"
|
|
242
|
+
)
|
|
243
|
+
require_nonempty_string(
|
|
244
|
+
entry, "availability_evidence", f"implementation_discovery.{backend}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
decision = require_mapping(record, "decision", "record")
|
|
248
|
+
selected_backend = require_nonempty_string(decision, "selected_backend", "decision")
|
|
249
|
+
if selected_backend not in BACKENDS:
|
|
250
|
+
raise RecordError(f"decision.selected_backend must be one of {sorted(BACKENDS)}")
|
|
251
|
+
require_nonempty_string(decision, "backend_version", "decision")
|
|
252
|
+
require_nonempty_string(decision, "selection_reason", "decision")
|
|
253
|
+
require_nonempty_string(decision, "fallback_reason", "decision")
|
|
254
|
+
if selected_backend in discovery:
|
|
255
|
+
selected_entry = discovery[selected_backend]
|
|
256
|
+
if selected_entry.get("local_availability") != "available":
|
|
257
|
+
raise RecordError("the selected backend must have local_availability=available")
|
|
258
|
+
|
|
259
|
+
execution = require_mapping(record, "execution", "record")
|
|
260
|
+
if require_nonempty_string(execution, "status", "execution") != "complete":
|
|
261
|
+
raise RecordError("execution.status must be complete")
|
|
262
|
+
require_nonempty_string(execution, "interface", "execution")
|
|
263
|
+
require_nonempty_string(execution, "command_or_input", "execution")
|
|
264
|
+
code_artifact = require_nonempty_string(execution, "code_artifact", "execution")
|
|
265
|
+
|
|
266
|
+
artifacts = record.get("artifacts")
|
|
267
|
+
if not isinstance(artifacts, list) or not artifacts:
|
|
268
|
+
raise RecordError("artifacts must be a nonempty list")
|
|
269
|
+
roles: set[str] = set()
|
|
270
|
+
artifact_paths: set[str] = set()
|
|
271
|
+
verified_artifacts = []
|
|
272
|
+
for index, artifact in enumerate(artifacts):
|
|
273
|
+
label = f"artifacts[{index}]"
|
|
274
|
+
if not isinstance(artifact, dict):
|
|
275
|
+
raise RecordError(f"{label} must be an object")
|
|
276
|
+
role = require_nonempty_string(artifact, "role", label)
|
|
277
|
+
raw_path = require_nonempty_string(artifact, "path", label)
|
|
278
|
+
expected_hash = validate_hash(artifact.get("sha256"), f"{label}.sha256")
|
|
279
|
+
artifact_path = resolve_safe_relative(base, raw_path, f"{label}.path")
|
|
280
|
+
if not artifact_path.is_file():
|
|
281
|
+
raise RecordError(f"{label}.path does not exist: {artifact_path}")
|
|
282
|
+
actual_hash = sha256_file(artifact_path)
|
|
283
|
+
if actual_hash != expected_hash:
|
|
284
|
+
raise RecordError(f"{label}.sha256 does not match {raw_path}")
|
|
285
|
+
roles.add(role)
|
|
286
|
+
artifact_paths.add(Path(raw_path).as_posix())
|
|
287
|
+
verified_artifacts.append(
|
|
288
|
+
{"role": role, "path": Path(raw_path).as_posix(), "sha256": actual_hash}
|
|
289
|
+
)
|
|
290
|
+
if "code" not in roles or "result" not in roles:
|
|
291
|
+
raise RecordError("artifacts must include both code and result roles")
|
|
292
|
+
if Path(code_artifact).as_posix() not in artifact_paths:
|
|
293
|
+
raise RecordError("execution.code_artifact must name a hashed artifact")
|
|
294
|
+
|
|
295
|
+
result = require_mapping(record, "result", "record")
|
|
296
|
+
if require_nonempty_string(result, "status", "result") != "complete":
|
|
297
|
+
raise RecordError("result.status must be complete")
|
|
298
|
+
require_nonempty_string(result, "summary", "result")
|
|
299
|
+
result_artifact = require_nonempty_string(result, "result_artifact", "result")
|
|
300
|
+
if Path(result_artifact).as_posix() not in artifact_paths:
|
|
301
|
+
raise RecordError("result.result_artifact must name a hashed artifact")
|
|
302
|
+
|
|
303
|
+
verification = require_mapping(record, "verification", "record")
|
|
304
|
+
methods = require_string_list(verification, "methods", "verification")
|
|
305
|
+
if not methods or any(not method.strip() for method in methods):
|
|
306
|
+
raise RecordError("verification.methods must contain at least one nonempty method")
|
|
307
|
+
evidence_level = require_nonempty_string(
|
|
308
|
+
verification, "evidence_level", "verification"
|
|
309
|
+
)
|
|
310
|
+
if evidence_level not in EVIDENCE_LEVELS:
|
|
311
|
+
raise RecordError(f"verification.evidence_level must be one of {sorted(EVIDENCE_LEVELS)}")
|
|
312
|
+
residual_or_error = verification.get("residual_or_error")
|
|
313
|
+
if not isinstance(residual_or_error, str):
|
|
314
|
+
raise RecordError("verification.residual_or_error must be a string")
|
|
315
|
+
if mode != "exact" and not residual_or_error.strip():
|
|
316
|
+
raise RecordError("numerical precision modes require verification.residual_or_error")
|
|
317
|
+
require_string_list(verification, "limitations", "verification")
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"ok": True,
|
|
321
|
+
"record": str(record_path),
|
|
322
|
+
"base_dir": str(base),
|
|
323
|
+
"selected_backend": selected_backend,
|
|
324
|
+
"evidence_level": evidence_level,
|
|
325
|
+
"verified_artifacts": verified_artifacts,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
330
|
+
parser = argparse.ArgumentParser(
|
|
331
|
+
description="Initialize or validate a computation-record.json file."
|
|
332
|
+
)
|
|
333
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
334
|
+
|
|
335
|
+
init_parser = subparsers.add_parser("init", help="initialize a record from a task file")
|
|
336
|
+
init_parser.add_argument("--task-file", required=True, type=Path)
|
|
337
|
+
init_parser.add_argument("--record", required=True, type=Path)
|
|
338
|
+
init_parser.add_argument("--force", action="store_true")
|
|
339
|
+
|
|
340
|
+
validate_parser = subparsers.add_parser("validate", help="validate a completed record")
|
|
341
|
+
validate_parser.add_argument("--record", required=True, type=Path)
|
|
342
|
+
validate_parser.add_argument("--base-dir", type=Path)
|
|
343
|
+
return parser
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def main(argv: list[str] | None = None) -> int:
|
|
347
|
+
args = build_parser().parse_args(argv)
|
|
348
|
+
try:
|
|
349
|
+
if args.command == "init":
|
|
350
|
+
init_record(args.task_file, args.record, args.force)
|
|
351
|
+
return 0
|
|
352
|
+
result = validate_record(args.record, args.base_dir)
|
|
353
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
354
|
+
return 0
|
|
355
|
+
except (OSError, RecordError) as exc:
|
|
356
|
+
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
|
|
357
|
+
return 1
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
sys.exit(main())
|