dsh-math-modeling-agent 0.3.1 → 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 CHANGED
@@ -71,6 +71,10 @@ NOT_CHECKED → DERIVED → EXECUTED → VERIFIED → INDEPENDENTLY_VERIFIED →
71
71
 
72
72
  工具只是产生证据的方式,可以替换,工作流不变。
73
73
 
74
+ 本插件保留原 GitHub 项目的核心行为:问题/附件探查、子问题顺序求解、Modeler → Analyzer → Correction 迭代、逐轮日志和可恢复运行;对应关系见 `skills/math-modeling-agent/references/original-project-parity.md`。
75
+
76
+ 数学计算后端扩展位于 `skills/math-modeling-agent/scripts/computation/`:它只负责探测可调用后端、生成能力快照和校验计算记录,不捆绑 Mathematica/SageMath 等外部引擎。
77
+
74
78
  - **Python** 可选(推荐):需要计算时在运行目录内创建隔离环境(数值计算、数据分析、优化、仿真、绘图、独立重算)
75
79
  - **Lean** 可选:形式化验证;不自动安装;**形式命题被证明 ≠ 现实主张被证明**
76
80
  - **Wolfram** 可选:符号计算、解析推导、恒等式验证
@@ -108,7 +112,7 @@ D3 方向/D4 裁决),`run-state.mjs gate` 在每次状态转移前强制校
108
112
  安装:
109
113
 
110
114
  ```bash
111
- dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.3.1
115
+ dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.4.1
112
116
  dsh --profile web --dump-config # 检查组合层(应看到 dsh-math-modeling-agent-skills 行)
113
117
  dsh web # 重启以加载插件
114
118
  ```
@@ -118,6 +122,17 @@ dsh web # 重启以加载插件
118
122
  不要用 `npm install` 代替,那会装到错误位置。如本机镜像源同步滞后,可显式走官方源:
119
123
  `dsh plugin --profile web add dsh-math-modeling-agent --registry=https://registry.npmjs.org/`)
120
124
 
125
+ 维护者在发布后应分别核对 GitHub 解包目录、npm tarball 解包目录和 profile
126
+ 安装目录:
127
+
128
+ ```bash
129
+ node skills/math-modeling-agent/scripts/distribution-parity.mjs <source-root> <candidate-root>
130
+ ```
131
+
132
+ 该命令比较 package.json `files` allowlist 内所有文件的 SHA-256;它不把 AI
133
+ 解题中的偶发失败当作分发失败。`dsh --profile web --dump-config` 用于检查组合,
134
+ 但新版 DSH 可能重写 profile 的空 `cordis.yml`,不是严格只读操作。
135
+
121
136
  开始建模——直接描述任务即可。每个环节都是人与 LLM 的深度信息交换:LLM 给出带依据链的
122
137
  完整分析(题面原句/数据证据/文献/显式判断标记),你纠正、补充背景或提供自己的参考文献,
123
138
  LLM 更新并展示差异。建模前必须先做文献调研(AI 检索原理与方法文献,你可增删),每个候选
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-math-modeling-agent",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Evidence-driven mathematical modeling and verification skills for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: math-modeling-agent
3
- description: This skill should be used when the user asks to "建立数学模型", "解决数学建模题", "继续上次建模", "寻找新的建模方向", "solve this modeling problem", or needs an evidence-backed mathematical model with computation, research, verification, per-attempt reports, and resumable state.
4
- whenToUse: Use for open-ended mathematical modeling, prediction, optimization, estimation, simulation, mechanism, decision, or multi-part contest problems; do not use for auditing an already completed artifact without changing it.
3
+ description: 当用户要求分析数学建模题、读取题目附件、建立预测/优化/估计/仿真模型、比较建模方向、执行数值验证、进行鲁棒性分析或继续已有建模项目时使用。
4
+ whenToUse: 用于开放式数学建模、预测、优化、估计、仿真、机制分析、决策和多小问竞赛题;已有成果的独立审计改用 math-modeling-audit,不在此处静默改写被审计成果。
5
5
  user-invocable: true
6
6
  ---
7
7
 
@@ -17,7 +17,7 @@ Accept problem text, attachments, or an existing run directory.
17
17
 
18
18
  Interaction model (references/interaction-protocol.md): execution details stay autonomous (file names, tool adapters, state fields), but every model-shaping decision is a DEEP three-beat exchange — LLM presents a complete analysis with its evidence chain, the user responds with substance (corrections, background, their own references), the LLM updates the artifact and shows the delta. Decision points: D0 six-section restatement / D1 routing once per task, D-R literature research before any candidate is built, D2 assumptions and D3 direction per subproblem, D2' on mid-subproblem discoveries, D4 verdict per round. Show the runlog digest in the chat after every round (references/report-contract.md); announce one status line before every transition; the user may interrupt at any time.
19
19
 
20
- Return a concise current status, supported claims, unresolved obligations, attempt delta, artifact path, and next action or final report.
20
+ Return a concise current status, supported claims, unresolved obligations, attempt delta, artifact path, and next action or final report. The legacy Modeler → Analyzer → Correction behavior mapping is recorded in `references/original-project-parity.md`.
21
21
 
22
22
  ## Default workflow
23
23
 
@@ -47,6 +47,19 @@ Return a concise current status, supported claims, unresolved obligations, attem
47
47
  - Continue only when a round closes an obligation, adds reproducible evidence, removes a blocker, tightens uncertainty, or refutes a candidate.
48
48
  - The run directory stays clean at every moment, and every failed path is preserved in its own folder (references/run-directory.md).
49
49
 
50
+ ## Computation tool extension
51
+
52
+ When a computation is required, first call the bundled backend probe:
53
+ `python scripts/computation/probe_backends.py`
54
+ (or the `.ps1` entrypoint when Python is unavailable). Choose only a backend
55
+ reported callable; record the primary route and fallback. Use
56
+ `scripts/computation/backend_inventory.py` with a run-local `--state-file`
57
+ when a persistent capability snapshot is needed. For reproducible/file or
58
+ high-assurance work, initialize and validate
59
+ `scripts/computation/computation_record.py`. These scripts discover and record
60
+ external backends; they are not mathematical engines and their successful
61
+ execution is not proof.
62
+
50
63
  ## Tool degradation
51
64
 
52
65
  Python is recommended but not required for installation. Lean and Wolfram are optional. If a required tool is unavailable, weaken the claim and report the missing obligation; never pretend execution or formal verification occurred.
@@ -0,0 +1,19 @@
1
+ # Original GitHub workflow parity
2
+
3
+ The DSH bundle is a faithful workflow migration of the original
4
+ MathModelingAgent project, not a replacement of its useful behavior.
5
+
6
+ | Original GitHub behavior | DSH realization |
7
+ |---|---|
8
+ | PDF/problem extraction in `code/agent.py` | intake D0 and the subproblem DAG |
9
+ | Excel attachment profiling in `code/data_analyzer.py` | INPUT_PROFILED and the run input manifest |
10
+ | sequential subproblem solving | dependency-ordered SPi loop |
11
+ | Modeler → Analyzer → Correction loop in `code/prompts.py` | explorer/modeler → verifier/analyzer → corrector/revise |
12
+ | per-round thinking and run logs | attempts/<n>/report.md, events.jsonl, and chat runlog digest |
13
+ | acceptance/rejection and stagnation control | D4 verdict, progress test, REVISE/FORK, and resumable non-SOLVED states |
14
+ | hash-based cache and resume | run-local artifacts, evidence links, and state recovery |
15
+
16
+ The migration deliberately replaces private chain-of-thought storage with
17
+ human-readable reasoning, evidence, and reproducible artifacts. The roles and
18
+ loop semantics remain; only the execution substrate changes from the legacy
19
+ Python CLI to DSH skills and state files.
@@ -7,8 +7,9 @@ produce reports and evidence. Never let a subagent mutate run-state directly.
7
7
 
8
8
  | Class | Role (lineage) | When | Count |
9
9
  |---|---|---|---|
10
- | explorer | parallel candidate exploration (legacy Modeler) | CANDIDATES_READY, >= 2 independent directions | 1 per direction |
11
- | verifier | blind re-verification (legacy Analyzer/Correction) | before terminal: key numeric/feasibility claims | Standard: 1 per task; High-Assurance: 1 per claim |
10
+ | explorer | parallel candidate exploration (original Modeler lineage) | CANDIDATES_READY, >= 2 independent directions | 1 per direction |
11
+ | verifier | blind re-verification (original Analyzer lineage) | before terminal: key numeric/feasibility claims | Standard: 1 per task; High-Assurance: 1 per claim |
12
+ | corrector | apply accepted critique and produce the next attempt (original Correction lineage) | after a failed/conditional verdict | 1 per selected direction |
12
13
  | auditor | artifact-only audit (math-modeling-audit skill) | before SOLVED | High-Assurance: mandatory; else optional |
13
14
 
14
15
  ## Brief template (self-contained; subagents see no parent context)
@@ -22,6 +22,17 @@ e.g. "本阶段用 Python+OSQP 做约束优化;Wolfram 对本题非必需,
22
22
  The problem never silently depends on a single tool: every phase names its
23
23
  backend, its fallback, and the degradation path when the backend fails.
24
24
 
25
+ ## Computation backend extension
26
+
27
+ Before selecting an external computation backend, run
28
+ `scripts/computation/probe_backends.py` (or its PowerShell compatibility entry)
29
+ and use only the live capability result. Persist a backend snapshot with
30
+ `scripts/computation/backend_inventory.py` and an explicit run-local
31
+ `--state-file` when the phase needs a stable inventory. Use
32
+ `scripts/computation/computation_record.py` for reproducible/file or
33
+ high-assurance computation. A probe, inventory, or record validation does not
34
+ prove a mathematical claim.
35
+
25
36
  ## Python
26
37
 
27
38
  Use `scripts/python-environment.mjs`. Prefer uv, then an existing Python runtime plus venv. Keep the environment under the run directory. Install only required normalized PyPI package names; VCS URLs, local wheels, or arbitrary indexes require explicit user approval. Record lock, seed, command, workdir, timeout, stdout/stderr, exit code, and generated files.
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## States
10
10
 
11
- TRIAGE → SCOPE_FROZEN → INPUT_PROFILED → CLAIMS_REGISTERED → CANDIDATES_READY → ATTEMPT → EXECUTE → VERIFY.
11
+ TRIAGE → RESEARCH → SCOPE_FROZEN → INPUT_PROFILED → CLAIMS_REGISTERED → CANDIDATES_READY → ATTEMPT → EXECUTE → VERIFY.
12
12
 
13
13
  VERIFY may move to REVISE, RESEARCH, FORK, an independent audit, or one terminal status:
14
14
  SOLVED, PARTIAL, CONDITIONAL, INCONCLUSIVE, REFUTED, INFEASIBLE, UNIDENTIFIABLE, BLOCKED, CANCELLED.
@@ -19,16 +19,17 @@ SOLVED, PARTIAL, CONDITIONAL, INCONCLUSIVE, REFUTED, INFEASIBLE, UNIDENTIFIABLE,
19
19
  - **逐问层**(SPi 循环):子问题分析 → 子问题假设 Si(D2-SPi)→ ATTEMPT 建模求解 → VERIFY 该问验证 → 该问鲁棒性(扰动计划→交互→执行)→ D4 裁决 → REVISE(本问修订)或进入 SPi+1(reason: "SPi 完成,进入 SPi+1")。
20
20
  - **收束层**:跨问联动检查(终态 gate 校验全部子问题 DONE + 依赖满足)→ 全题鲁棒性汇总 → 终态硬门 → SOLVED + final-report(整体叙事,非拼凑)。
21
21
 
22
- ## Pre-modeling literature research (v2.6, mandatory)
22
+ ## Pre-modeling literature research (mandatory)
23
23
 
24
- CLAIMS_REGISTERED → RESEARCH → CANDIDATES_READY. Before any candidate is
25
- built, run the D-R exchange (interaction-protocol.md): search principles and
26
- methods by direction, write research/sources.jsonl, attach a literature column
27
- to the candidate matrix. Every direction must be traceable to at least one
28
- source engineering background, methodology paper, textbook, or official
29
- documentation; a literature-free direction must be explicitly marked
24
+ TRIAGE → RESEARCH → D-R → SCOPE_FROZEN. After the initial task triage and
25
+ before D0 restatement is frozen, run the D-R exchange
26
+ (interaction-protocol.md): search principles, engineering background, and
27
+ methods by direction; write research/sources.jsonl; attach a literature
28
+ column to the candidate matrix. Every direction must be traceable to at least
29
+ one source. A literature-free direction must be explicitly marked
30
30
  "原创方向" with its derivation basis. Never search for solutions to the
31
- specific contest problem.
31
+ specific contest problem. Mid-solve evidence gaps use a recorded
32
+ RESEARCH backtrack, not an implicit bypass of the initial research phase.
32
33
 
33
34
  ## Interaction gates (v2)
34
35
 
@@ -0,0 +1,20 @@
1
+ # DSH computation tool extension
2
+
3
+ These entrypoints are adapted from the user-provided
4
+ `math-science-computation` toolkit. They are computation-support tools, not
5
+ mathematical engines: they discover callable external backends, persist a
6
+ local capability snapshot, and validate reproducible computation records.
7
+
8
+ ## Entrypoints
9
+
10
+ - `probe_backends.py` / `probe_backends.ps1`: read-only discovery of
11
+ Wolfram/Mathematica, primecount, SageMath, Python, and Python library
12
+ availability. External probes are bounded and return structured failures.
13
+ - `backend_inventory.py` / `backend_inventory.ps1`: read or refresh a
14
+ capability snapshot. Pass a run-local `--state-file`/`-StateFile`; do not
15
+ use an implicit shared machine-wide path.
16
+ - `computation_record.py`: initialize or validate a reproducibility record
17
+ for every probeable backend, including primecount.
18
+
19
+ The external backend remains responsible for the actual mathematical
20
+ calculation. A successful probe or record validation is not a proof.
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://local.codex.invalid/math-science-computation/backend-inventory.schema.json",
4
+ "title": "Math and science computation local backend inventory",
5
+ "type": "object",
6
+ "required": [
7
+ "inventory_schema_version",
8
+ "created_at_utc",
9
+ "updated_at_utc",
10
+ "local",
11
+ "mcp",
12
+ "invalidations"
13
+ ],
14
+ "properties": {
15
+ "inventory_schema_version": { "const": "1.0" },
16
+ "created_at_utc": { "type": "string", "format": "date-time" },
17
+ "updated_at_utc": { "type": "string", "format": "date-time" },
18
+ "local": {
19
+ "type": "object",
20
+ "required": ["schema_version", "probed_at_utc", "host", "mathematica", "primecount", "sagemath", "python"],
21
+ "properties": {
22
+ "schema_version": { "const": "1.0" },
23
+ "probed_at_utc": { "type": "string", "format": "date-time" },
24
+ "host": {
25
+ "type": "object",
26
+ "required": ["system", "architecture"],
27
+ "properties": {
28
+ "system": { "enum": ["Windows", "Darwin", "Linux"] },
29
+ "architecture": { "type": "string", "minLength": 1 }
30
+ },
31
+ "additionalProperties": true
32
+ },
33
+ "mathematica": { "type": "object" },
34
+ "primecount": { "type": "object" },
35
+ "sagemath": { "type": "object" },
36
+ "python": { "type": "object" }
37
+ },
38
+ "additionalProperties": true
39
+ },
40
+ "mcp": {
41
+ "type": "object",
42
+ "required": ["authority", "persisted_status", "required_action"],
43
+ "properties": {
44
+ "authority": { "const": "current_session_tool_discovery_and_call" },
45
+ "persisted_status": { "const": "historical_only" },
46
+ "required_action": { "type": "string" },
47
+ "mathematica": {
48
+ "type": "object",
49
+ "required": ["server_name", "protocol_version", "server_version", "wolfram_language_version", "observed_at_utc", "evidence"],
50
+ "properties": {
51
+ "server_name": { "type": "string", "minLength": 1 },
52
+ "protocol_version": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
53
+ "server_version": { "type": "string", "minLength": 1 },
54
+ "wolfram_language_version": { "type": "string", "minLength": 1 },
55
+ "observed_at_utc": { "type": "string", "format": "date-time" },
56
+ "evidence": { "const": "initialize_handshake_and_evaluator" }
57
+ },
58
+ "additionalProperties": false
59
+ }
60
+ },
61
+ "additionalProperties": false
62
+ },
63
+ "invalidations": {
64
+ "type": "array",
65
+ "items": {
66
+ "type": "object",
67
+ "required": ["backend", "reason", "recorded_at_utc"],
68
+ "properties": {
69
+ "backend": { "enum": ["mathematica", "primecount", "sagemath", "python"] },
70
+ "reason": { "enum": ["user_requested", "path_missing", "execution_failed", "version_mismatch"] },
71
+ "recorded_at_utc": { "type": "string", "format": "date-time" }
72
+ },
73
+ "additionalProperties": false
74
+ }
75
+ }
76
+ },
77
+ "additionalProperties": false
78
+ }
@@ -0,0 +1,351 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [ValidateSet('ReadOrCreate', 'Refresh', 'Invalidate', 'RecordMcp')]
4
+ [string]$Mode = 'ReadOrCreate',
5
+ [string]$StateFile = '',
6
+ [ValidateSet('all', 'mathematica', 'primecount', 'sagemath', 'python')]
7
+ [string[]]$Backend = @('all'),
8
+ [string]$ReasonCode = '',
9
+ [int]$MaxAgeHours = 168,
10
+ [string]$ProbeScript = (Join-Path $PSScriptRoot 'probe_backends.ps1'),
11
+ [string]$ProbeJsonFile = '',
12
+ [string]$PythonCommand = 'python',
13
+ [string]$SageCommand = '',
14
+ [string]$WslDistro = '',
15
+ [string]$WslSageCommand = 'sage',
16
+ [string]$PrimecountCommand = '',
17
+ [string]$McpServerName = '',
18
+ [string]$McpProtocolVersion = '',
19
+ [string]$McpServerVersion = '',
20
+ [string]$McpWolframLanguageVersion = '',
21
+ [string]$McpObservedAtUtc = ''
22
+ )
23
+
24
+ $ErrorActionPreference = 'Stop'
25
+ $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
26
+ if ($Mode -eq 'Invalidate' -and $ReasonCode -notmatch '^[a-z][a-z0-9_]{0,31}$') {
27
+ throw "Invalidate mode requires a bounded lowercase reason code."
28
+ }
29
+
30
+ function Get-DefaultStateFile {
31
+ if ($env:MATH_SCIENCE_BACKEND_INVENTORY) {
32
+ return $env:MATH_SCIENCE_BACKEND_INVENTORY
33
+ }
34
+ return (Join-Path (Join-Path (Join-Path ([IO.Path]::GetTempPath()) 'DSH') 'math-science-computation') 'backend-inventory.json')
35
+ }
36
+
37
+ function Get-HostIdentity {
38
+ $system = if ([Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([Runtime.InteropServices.OSPlatform]::Windows)) {
39
+ 'Windows'
40
+ }
41
+ elseif ([Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([Runtime.InteropServices.OSPlatform]::OSX)) {
42
+ 'Darwin'
43
+ }
44
+ else {
45
+ 'Linux'
46
+ }
47
+ $rawArchitecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
48
+ $architecture = switch ($rawArchitecture) {
49
+ 'x64' { 'x86_64' }
50
+ 'amd64' { 'x86_64' }
51
+ 'arm64' { 'arm64' }
52
+ 'x86' { 'x86' }
53
+ default { $rawArchitecture }
54
+ }
55
+ return [ordered]@{ system = $system; architecture = $architecture; powershell_edition = $PSVersionTable.PSEdition }
56
+ }
57
+
58
+ function Read-Inventory {
59
+ param([Parameter(Mandatory = $true)][string]$Path)
60
+
61
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
62
+ return $null
63
+ }
64
+ try {
65
+ $inventory = Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json -AsHashtable
66
+ if ($inventory.inventory_schema_version -ne '1.0' -or $inventory.local.schema_version -ne '1.0') {
67
+ return $null
68
+ }
69
+ return $inventory
70
+ }
71
+ catch {
72
+ return $null
73
+ }
74
+ }
75
+
76
+ function Invoke-LocalProbe {
77
+ if ($ProbeJsonFile) {
78
+ return (Get-Content -Raw -LiteralPath $ProbeJsonFile | ConvertFrom-Json -AsHashtable)
79
+ }
80
+ if (-not (Test-Path -LiteralPath $ProbeScript -PathType Leaf)) {
81
+ throw "Backend probe script is unavailable."
82
+ }
83
+
84
+ $probeArguments = @{
85
+ PythonCommand = $PythonCommand
86
+ SageCommand = $SageCommand
87
+ WslDistro = $WslDistro
88
+ WslSageCommand = $WslSageCommand
89
+ PrimecountCommand = $PrimecountCommand
90
+ }
91
+ $raw = & $ProbeScript @probeArguments
92
+ if ($LASTEXITCODE -notin @($null, 0)) {
93
+ throw "Backend probe failed with a nonzero exit code."
94
+ }
95
+ return (($raw -join "`n") | ConvertFrom-Json -AsHashtable)
96
+ }
97
+
98
+ function New-Inventory {
99
+ param([Parameter(Mandatory = $true)][hashtable]$Local)
100
+
101
+ $now = [DateTime]::UtcNow.ToString('o')
102
+ return [ordered]@{
103
+ inventory_schema_version = '1.0'
104
+ created_at_utc = $now
105
+ updated_at_utc = $now
106
+ local = $Local
107
+ mcp = [ordered]@{
108
+ authority = 'current_session_tool_discovery_and_call'
109
+ persisted_status = 'historical_only'
110
+ required_action = 'Build a current-session overlay and live-check only the selected MCP backend.'
111
+ }
112
+ invalidations = @()
113
+ }
114
+ }
115
+
116
+ function New-McpObservation {
117
+ $required = [ordered]@{
118
+ server_name = $McpServerName
119
+ protocol_version = $McpProtocolVersion
120
+ server_version = $McpServerVersion
121
+ wolfram_language_version = $McpWolframLanguageVersion
122
+ }
123
+ $missing = @($required.GetEnumerator() | Where-Object { -not $_.Value.Trim() } | ForEach-Object Key)
124
+ if ($missing.Count -gt 0) {
125
+ throw ('RecordMcp requires: ' + ($missing -join ', '))
126
+ }
127
+ if ($McpProtocolVersion -notmatch '^\d{4}-\d{2}-\d{2}$') {
128
+ throw 'MCP protocol version must use the negotiated YYYY-MM-DD form.'
129
+ }
130
+ $observedAt = if ($McpObservedAtUtc) { $McpObservedAtUtc } else { [DateTime]::UtcNow.ToString('o') }
131
+ try { [void][DateTimeOffset]::Parse($observedAt) }
132
+ catch { throw 'MCP observation time must be an ISO-8601 timestamp.' }
133
+ return [ordered]@{
134
+ server_name = $McpServerName
135
+ protocol_version = $McpProtocolVersion
136
+ server_version = $McpServerVersion
137
+ wolfram_language_version = $McpWolframLanguageVersion
138
+ observed_at_utc = $observedAt
139
+ evidence = 'initialize_handshake_and_evaluator'
140
+ }
141
+ }
142
+
143
+ function Write-InventoryAtomic {
144
+ param(
145
+ [Parameter(Mandatory = $true)][hashtable]$Inventory,
146
+ [Parameter(Mandatory = $true)][string]$Path
147
+ )
148
+
149
+ $parent = Split-Path -Parent $Path
150
+ if (-not $parent) {
151
+ $parent = (Get-Location).Path
152
+ $Path = Join-Path $parent $Path
153
+ }
154
+ [IO.Directory]::CreateDirectory($parent) | Out-Null
155
+ $temporary = Join-Path $parent ('.backend-inventory-' + [Guid]::NewGuid().ToString('N') + '.tmp')
156
+ try {
157
+ $json = $Inventory | ConvertTo-Json -Depth 12
158
+ [IO.File]::WriteAllText($temporary, $json, [Text.UTF8Encoding]::new($false))
159
+ [IO.File]::Move($temporary, $Path, $true)
160
+ }
161
+ finally {
162
+ if (Test-Path -LiteralPath $temporary -PathType Leaf) {
163
+ Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
164
+ }
165
+ }
166
+ }
167
+
168
+ function Get-MissingBackendPaths {
169
+ param([Parameter(Mandatory = $true)][hashtable]$Inventory)
170
+
171
+ $missing = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
172
+ foreach ($installation in @($Inventory.local.mathematica.installations)) {
173
+ if ($installation.executable -and -not (Test-Path -LiteralPath $installation.executable -PathType Leaf)) {
174
+ [void]$missing.Add('mathematica')
175
+ }
176
+ }
177
+ $checks = @(
178
+ @('mathematica', $Inventory.local.mathematica.wolframscript.path),
179
+ @('primecount', $Inventory.local.primecount.path),
180
+ @('sagemath', $Inventory.local.sagemath.native.path),
181
+ @('python', $Inventory.local.python.path)
182
+ )
183
+ foreach ($check in $checks) {
184
+ if ($check[1] -and -not (Test-Path -LiteralPath $check[1] -PathType Leaf)) {
185
+ [void]$missing.Add([string]$check[0])
186
+ }
187
+ }
188
+ return @($missing)
189
+ }
190
+
191
+ function Test-InventoryExpired {
192
+ param([Parameter(Mandatory = $true)][hashtable]$Inventory)
193
+
194
+ if ($MaxAgeHours -le 0) {
195
+ return $false
196
+ }
197
+ try {
198
+ $updated = [DateTimeOffset]::Parse([string]$Inventory.updated_at_utc)
199
+ return ([DateTimeOffset]::UtcNow - $updated).TotalHours -ge $MaxAgeHours
200
+ }
201
+ catch {
202
+ return $true
203
+ }
204
+ }
205
+
206
+ function Test-InventoryHostMismatch {
207
+ param([Parameter(Mandatory = $true)][hashtable]$Inventory)
208
+
209
+ $current = Get-HostIdentity
210
+ $stored = $Inventory.local.host
211
+ if (-not $stored) {
212
+ return $true
213
+ }
214
+ return ($stored.system -ne $current.system -or $stored.architecture -ne $current.architecture)
215
+ }
216
+
217
+ function Merge-Backends {
218
+ param(
219
+ [Parameter(Mandatory = $true)][hashtable]$Inventory,
220
+ [Parameter(Mandatory = $true)][hashtable]$FreshLocal,
221
+ [Parameter(Mandatory = $true)][string[]]$Names
222
+ )
223
+
224
+ $selected = if ($Names -contains 'all') {
225
+ @('mathematica', 'primecount', 'sagemath', 'python')
226
+ }
227
+ else {
228
+ @($Names | Select-Object -Unique)
229
+ }
230
+ foreach ($name in $selected) {
231
+ $Inventory.local[$name] = $FreshLocal[$name]
232
+ }
233
+ $Inventory.local.probed_at_utc = $FreshLocal.probed_at_utc
234
+ $Inventory.local.host = $FreshLocal.host
235
+ $Inventory.updated_at_utc = [DateTime]::UtcNow.ToString('o')
236
+ return $Inventory
237
+ }
238
+
239
+ function Write-Result {
240
+ param(
241
+ [Parameter(Mandatory = $true)][hashtable]$Inventory,
242
+ [Parameter(Mandatory = $true)][string]$CacheStatus,
243
+ [string[]]$RefreshedBackends = @(),
244
+ [string[]]$InvalidPaths = @(),
245
+ [bool]$BackendStarted = $false,
246
+ [string]$WriteError = ''
247
+ )
248
+
249
+ $stopwatch.Stop()
250
+ $output = [ordered]@{
251
+ inventory_schema_version = $Inventory.inventory_schema_version
252
+ snapshot_updated_at_utc = $Inventory.updated_at_utc
253
+ cache = [ordered]@{
254
+ status = $CacheStatus
255
+ state_file = $StateFile
256
+ elapsed_ms = $stopwatch.ElapsedMilliseconds
257
+ backend_started = $BackendStarted
258
+ refreshed_backends = @($RefreshedBackends)
259
+ invalid_path_backends = @($InvalidPaths)
260
+ write_error = $WriteError
261
+ }
262
+ local = $Inventory.local
263
+ mcp = [ordered]@{
264
+ status = 'session_probe_required'
265
+ authority = 'current_session_tool_discovery_and_call'
266
+ note = 'The persisted snapshot is not evidence that an MCP tool is callable in this session.'
267
+ recorded_mathematica_observation = $Inventory.mcp.mathematica
268
+ }
269
+ }
270
+ $output | ConvertTo-Json -Depth 12 -Compress
271
+ }
272
+
273
+ if (-not $StateFile) {
274
+ $StateFile = Get-DefaultStateFile
275
+ }
276
+ $StateFile = [IO.Path]::GetFullPath($StateFile)
277
+ $inventory = Read-Inventory -Path $StateFile
278
+ $missingBackends = @()
279
+
280
+ if ($Mode -eq 'RecordMcp') {
281
+ $observation = New-McpObservation
282
+ $backendStarted = $false
283
+ if (-not $inventory) {
284
+ $freshLocal = Invoke-LocalProbe
285
+ if ($freshLocal.schema_version -ne '1.0') { throw 'Unsupported backend probe schema.' }
286
+ if ($freshLocal.mathematica -is [Collections.IDictionary] -and $freshLocal.mathematica.Contains('mcp')) {
287
+ [void]$freshLocal.mathematica.Remove('mcp')
288
+ }
289
+ $inventory = New-Inventory -Local $freshLocal
290
+ $backendStarted = $true
291
+ }
292
+ $inventory.mcp.mathematica = $observation
293
+ $inventory.updated_at_utc = [DateTime]::UtcNow.ToString('o')
294
+ Write-InventoryAtomic -Inventory $inventory -Path $StateFile
295
+ Write-Result -Inventory $inventory -CacheStatus 'mcp_recorded' -BackendStarted $backendStarted
296
+ exit 0
297
+ }
298
+
299
+ if ($Mode -eq 'ReadOrCreate' -and $inventory) {
300
+ $missingBackends = @(Get-MissingBackendPaths -Inventory $inventory)
301
+ if ($missingBackends.Count -eq 0 -and -not (Test-InventoryExpired -Inventory $inventory) -and -not (Test-InventoryHostMismatch -Inventory $inventory)) {
302
+ Write-Result -Inventory $inventory -CacheStatus 'hit' -BackendStarted $false
303
+ exit 0
304
+ }
305
+ }
306
+
307
+ $freshLocal = Invoke-LocalProbe
308
+ if ($freshLocal.schema_version -ne '1.0') {
309
+ throw "Unsupported backend probe schema."
310
+ }
311
+ if ($freshLocal.mathematica -is [Collections.IDictionary] -and $freshLocal.mathematica.Contains('mcp')) {
312
+ [void]$freshLocal.mathematica.Remove('mcp')
313
+ }
314
+
315
+ $refreshed = @('mathematica', 'primecount', 'sagemath', 'python')
316
+ $cacheStatus = 'created'
317
+ if (-not $inventory) {
318
+ $inventory = New-Inventory -Local $freshLocal
319
+ }
320
+ else {
321
+ $cacheStatus = 'refreshed'
322
+ if ($Mode -eq 'Invalidate') {
323
+ $targets = if ($Backend -contains 'all') { @('mathematica', 'primecount', 'sagemath', 'python') } else { @($Backend) }
324
+ $inventory.invalidations = @($inventory.invalidations) + @($targets | ForEach-Object {
325
+ [ordered]@{ backend = $_; reason = $ReasonCode; recorded_at_utc = [DateTime]::UtcNow.ToString('o') }
326
+ })
327
+ if ($inventory.invalidations.Count -gt 20) {
328
+ $inventory.invalidations = @($inventory.invalidations | Select-Object -Last 20)
329
+ }
330
+ $refreshed = $targets
331
+ }
332
+ elseif ($Mode -eq 'Refresh') {
333
+ $refreshed = if ($Backend -contains 'all') { @('mathematica', 'primecount', 'sagemath', 'python') } else { @($Backend) }
334
+ }
335
+ elseif ($missingBackends.Count -gt 0) {
336
+ $refreshed = $missingBackends
337
+ }
338
+ $inventory = Merge-Backends -Inventory $inventory -FreshLocal $freshLocal -Names $refreshed
339
+ }
340
+
341
+ $writeError = ''
342
+ try {
343
+ Write-InventoryAtomic -Inventory $inventory -Path $StateFile
344
+ }
345
+ catch {
346
+ $writeError = $_.Exception.GetType().Name
347
+ $cacheStatus = 'write_failed'
348
+ }
349
+ Write-Result -Inventory $inventory -CacheStatus $cacheStatus -RefreshedBackends $refreshed -InvalidPaths $missingBackends -BackendStarted $true -WriteError $writeError
350
+ if ($writeError) { exit 1 }
351
+ exit 0