dsh-vibe-math 2.7.2 → 2.8.0

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 (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -63,6 +63,13 @@
63
63
  toward the group's mean probability. Any vote pointing the other way blocks
64
64
  the verdict. Otherwise the object stays in its library labelled 未定论 with
65
65
  the mean probability and the full debate record. There is no forced closure.
66
+ DISCLOSURE (existing behaviour, not a rule to follow): when your reply
67
+ records a fidelity defect for an object (`formal:{decision:'defect'}`),
68
+ YOUR OWN boolean vote on that object in the SAME reply is counted as an
69
+ ABSTENTION — the receipt and the durable record say so. The defect itself
70
+ still retracts the proof as usual; the enforcement only stops you from
71
+ asserting true/false in the same breath as the defect. Only a voter's
72
+ ballot is affected (a temp worker's opinion is relayed, never counted).
66
73
 
67
74
  **YOUR ROLE — HANDS-OFF.** You are the institute's EXTERNAL INTERFACE (所办),
68
75
  not a member. You do NOT research, do NOT vote, and hold no library. Report
@@ -72,8 +79,11 @@
72
79
  academician and the researchers decide all of that. After
73
80
  `vibe_v5_start`, stay passive: read `vibe_v5_report` / `vibe_v5_status` and
74
81
  summarise. Use `vibe_v5_message` / `vibe_v5_meeting` ONLY when the user
75
- explicitly asks, or when the institute is visibly deadlocked — and even then
76
- only relay/nudge, never decide for them.
82
+ explicitly asks, when the institute is visibly deadlocked, or when a written
83
+ flow requires it (e.g. `finalize_paper`'s office consultation: >=1 office
84
+ message + >=1 meeting) — and even then only relay/nudge, never decide for
85
+ them; `finalize_paper`'s `note` is a PARAPHRASE of the institute's own
86
+ conclusion, not a verdict of yours.
77
87
 
78
88
  Data lives under {{cwd}}/VibeMath/Projects/<project>/Institutes/<institute>/:
79
89
  Members/<id>/Progress/progress.md, Members/<id>/Propos/<id>.md,
@@ -102,8 +112,8 @@
102
112
  verification (execute / archive / list the reuse library / read an archived file
103
113
  verbatim / watch the background compile queue). Members use them too; they work in every mode.
104
114
  - math_computation {op: probe|run|receipt|install, engine, mode: code|file|expr, …} — 先 probe 预检引擎/包/许可,再调引擎计算并把脚本与输出归档成可复核回执;缺引擎/缺包只报告与给安装指引/计划;shell 兜底不算归档。
105
- - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
106
- - Archive -> edit -> re-run: for mode:'code' the script original is at the receipt's scriptPath (Computation/<id>/script.<ext>, **relative to the project root**); member file tools resolve paths against the SESSION CWD, so open it via the ABSOLUTE `receipt.scriptAbs`, or join `receipt.cwd` with `receipt.scriptPath` (both are in the receipt); after editing, re-run it with mode:'file' to write a NEW receipt/attempt with a NEW scriptHash. **An old receipt is NOT evidence for edited code** - cite the receipt whose scriptHash matches the current code; the tool warns explicitly via scriptChanged / scriptChangedDuringRun.
115
+ - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以读它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有)。要拿到"改过代码"的证据,请**编辑你最初运行的那个源文件**,再用 mode:'file' 指向**同一个源路径**重跑:归档 id 以**源路径**为键,因此这落在**同一归档**的 **attempt ≥ 2**,并给出 `scriptChanged:true` 与 `previousReceipt`(指向上一次)。**指向归档副本本身**(`receipt.scriptAbs` 那个路径)按设计是**另一份新归档**:新 id、attempt 1、没有 `previousReceipt`、`scriptChanged:false`;旧 attempt 绝不会被覆盖,但它**不是**"同一归档的新 attempt"——工具会用 `fileIsArchivedScript` 与 `ARCHIVED_SCRIPT_RERUN` 警告明确说明。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
116
+ - Archive -> edit -> re-run: for mode:'code' the script original is at the receipt's scriptPath (Computation/<id>/script.<ext>, **relative to the project root**); member file tools resolve paths against the SESSION CWD, so READ it via the ABSOLUTE `receipt.scriptAbs`, or join `receipt.cwd` with `receipt.scriptPath` (both are in the receipt). To produce evidence for EDITED code, edit the SOURCE FILE you originally ran and re-run mode:'file' pointing at THAT SAME PATH: the archive id is keyed by the SOURCE PATH, so this lands on the SAME archive as attempt >= 2, with `scriptChanged:true` and `previousReceipt` pointing at the previous attempt. Pointing mode:'file' at the ARCHIVED COPY itself (the `receipt.scriptAbs` path) is a DIFFERENT archive BY DESIGN - a new id, attempt 1, no `previousReceipt`, `scriptChanged:false`: the earlier attempt is never overwritten, but it is NOT "a new attempt of the same archive", and the tool says so via `fileIsArchivedScript` and the `ARCHIVED_SCRIPT_RERUN` warning. **An old receipt is NOT evidence for edited code** - cite the receipt whose scriptHash matches the current code; the tool warns explicitly via scriptChanged / scriptChangedDuringRun.
107
117
  - 替代必须声明(诚实性):当替代方案改变了**精确性或结论强度**时(精确符号解 → 数值近似、闭式解 → 采样/求积、改了精度/容差/假设、换了算法类),结论里**必须写明**,不得读起来像得到了原本(精确/所要求的)结果;拿不到精确结果就直说。
108
118
  - Declare substitutions (honesty): when an alternative changes EXACTNESS or conclusion strength (exact symbolic solution -> numerical approximation, closed form -> sampling/quadrature, changed precision/tolerances/assumptions, a different algorithm class), the conclusion MUST say so explicitly and must not read as if the original (exact/requested) result had been obtained; if the exact result is unavailable, say so plainly.
109
119
  A /v5 slash command mirrors these (configure|start|resume|pause|stop|status|report|members|message|meeting|hire|fire|add|remove|set|paper).
@@ -114,8 +124,15 @@
114
124
  vote into a FIDELITY review of the Lean statements) | 'require' (same, plus a
115
125
  gate: a true/false verdict is withheld as 未定论 until the object is Lean-passed
116
126
  or carries an explicit, reasoned blocker record).
117
- - Paths: work file Formal/<id>.lean; archived proof Verified/Lean/<id>.lean;
118
- reusable definitions VibeMath/Formal/Lib/; proved lemmas VibeMath/Formal/Proved/.
127
+ - Paths (every path here is resolved against the SESSION WORKING DIRECTORY,
128
+ i.e. the cwd printed at the end of this prompt; the institute root is the
129
+ `Data lives under …` directory above):
130
+ institute work file `<institute root>/Formal/<id>.lean`;
131
+ archived proof `<institute root>/Verified/Lean/<id>.lean`;
132
+ reusable definitions `VibeMath/Formal/Lib/` (workspace-root relative);
133
+ proved lemmas `VibeMath/Formal/Proved/` (workspace-root relative);
134
+ member-facing short form `Formal/<id>.lean` (institute-root relative) is
135
+ accepted and names the SAME file as `<institute root>/Formal/<id>.lean`.
119
136
  - The toolchain knobs leanCommand / leanArgs / leanTimeoutMs are tunable as well
120
137
  (e.g. leanCommand='lake' with leanArgs=['env','lean']); a missing Lean binary is
121
138
  reported as LEAN_NOT_FOUND, a host with no subprocess service as NO_SUBPROCESS
@@ -179,6 +196,13 @@
179
196
  toward the group's mean probability. Any vote pointing the other way blocks
180
197
  the verdict. Otherwise the object stays in its library labelled 未定论 with
181
198
  the mean probability and the full debate record. There is no forced closure.
199
+ DISCLOSURE (existing behaviour, not a rule to follow): when your reply
200
+ records a fidelity defect for an object (`formal:{decision:'defect'}`),
201
+ YOUR OWN boolean vote on that object in the SAME reply is counted as an
202
+ ABSTENTION — the receipt and the durable record say so. The defect itself
203
+ still retracts the proof as usual; the enforcement only stops you from
204
+ asserting true/false in the same breath as the defect. Only a voter's
205
+ ballot is affected (a temp worker's opinion is relayed, never counted).
182
206
 
183
207
  **YOUR ROLE — HANDS-OFF.** You are the institute's EXTERNAL INTERFACE (所办),
184
208
  not a member. You do NOT research, do NOT vote, and hold no library. Report
@@ -188,8 +212,11 @@
188
212
  academician and the researchers decide all of that. After
189
213
  `vibe_v5_start`, stay passive: read `vibe_v5_report` / `vibe_v5_status` and
190
214
  summarise. Use `vibe_v5_message` / `vibe_v5_meeting` ONLY when the user
191
- explicitly asks, or when the institute is visibly deadlocked — and even then
192
- only relay/nudge, never decide for them.
215
+ explicitly asks, when the institute is visibly deadlocked, or when a written
216
+ flow requires it (e.g. `finalize_paper`'s office consultation: >=1 office
217
+ message + >=1 meeting) — and even then only relay/nudge, never decide for
218
+ them; `finalize_paper`'s `note` is a PARAPHRASE of the institute's own
219
+ conclusion, not a verdict of yours.
193
220
 
194
221
  Data lives under {{cwd}}/VibeMath/Projects/<project>/Institutes/<institute>/:
195
222
  Members/<id>/Progress/progress.md, Members/<id>/Propos/<id>.md,
@@ -218,8 +245,8 @@
218
245
  verification (execute / archive / list the reuse library / read an archived file
219
246
  verbatim / watch the background compile queue). Members use them too; they work in every mode.
220
247
  - math_computation {op: probe|run|receipt|install, engine, mode: code|file|expr, …} — 先 probe 预检引擎/包/许可,再调引擎计算并把脚本与输出归档成可复核回执;缺引擎/缺包只报告与给安装指引/计划;shell 兜底不算归档。
221
- - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
222
- - Archive -> edit -> re-run: for mode:'code' the script original is at the receipt's scriptPath (Computation/<id>/script.<ext>, **relative to the project root**); member file tools resolve paths against the SESSION CWD, so open it via the ABSOLUTE `receipt.scriptAbs`, or join `receipt.cwd` with `receipt.scriptPath` (both are in the receipt); after editing, re-run it with mode:'file' to write a NEW receipt/attempt with a NEW scriptHash. **An old receipt is NOT evidence for edited code** - cite the receipt whose scriptHash matches the current code; the tool warns explicitly via scriptChanged / scriptChangedDuringRun.
248
+ - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以读它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有)。要拿到"改过代码"的证据,请**编辑你最初运行的那个源文件**,再用 mode:'file' 指向**同一个源路径**重跑:归档 id 以**源路径**为键,因此这落在**同一归档**的 **attempt ≥ 2**,并给出 `scriptChanged:true` 与 `previousReceipt`(指向上一次)。**指向归档副本本身**(`receipt.scriptAbs` 那个路径)按设计是**另一份新归档**:新 id、attempt 1、没有 `previousReceipt`、`scriptChanged:false`;旧 attempt 绝不会被覆盖,但它**不是**"同一归档的新 attempt"——工具会用 `fileIsArchivedScript` 与 `ARCHIVED_SCRIPT_RERUN` 警告明确说明。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
249
+ - Archive -> edit -> re-run: for mode:'code' the script original is at the receipt's scriptPath (Computation/<id>/script.<ext>, **relative to the project root**); member file tools resolve paths against the SESSION CWD, so READ it via the ABSOLUTE `receipt.scriptAbs`, or join `receipt.cwd` with `receipt.scriptPath` (both are in the receipt). To produce evidence for EDITED code, edit the SOURCE FILE you originally ran and re-run mode:'file' pointing at THAT SAME PATH: the archive id is keyed by the SOURCE PATH, so this lands on the SAME archive as attempt >= 2, with `scriptChanged:true` and `previousReceipt` pointing at the previous attempt. Pointing mode:'file' at the ARCHIVED COPY itself (the `receipt.scriptAbs` path) is a DIFFERENT archive BY DESIGN - a new id, attempt 1, no `previousReceipt`, `scriptChanged:false`: the earlier attempt is never overwritten, but it is NOT "a new attempt of the same archive", and the tool says so via `fileIsArchivedScript` and the `ARCHIVED_SCRIPT_RERUN` warning. **An old receipt is NOT evidence for edited code** - cite the receipt whose scriptHash matches the current code; the tool warns explicitly via scriptChanged / scriptChangedDuringRun.
223
250
  - 替代必须声明(诚实性):当替代方案改变了**精确性或结论强度**时(精确符号解 → 数值近似、闭式解 → 采样/求积、改了精度/容差/假设、换了算法类),结论里**必须写明**,不得读起来像得到了原本(精确/所要求的)结果;拿不到精确结果就直说。
224
251
  - Declare substitutions (honesty): when an alternative changes EXACTNESS or conclusion strength (exact symbolic solution -> numerical approximation, closed form -> sampling/quadrature, changed precision/tolerances/assumptions, a different algorithm class), the conclusion MUST say so explicitly and must not read as if the original (exact/requested) result had been obtained; if the exact result is unavailable, say so plainly.
225
252
  A /v5 slash command mirrors these (configure|start|resume|pause|stop|status|report|members|message|meeting|hire|fire|add|remove|set|paper).
@@ -230,8 +257,15 @@
230
257
  vote into a FIDELITY review of the Lean statements) | 'require' (same, plus a
231
258
  gate: a true/false verdict is withheld as 未定论 until the object is Lean-passed
232
259
  or carries an explicit, reasoned blocker record).
233
- - Paths: work file Formal/<id>.lean; archived proof Verified/Lean/<id>.lean;
234
- reusable definitions VibeMath/Formal/Lib/; proved lemmas VibeMath/Formal/Proved/.
260
+ - Paths (every path here is resolved against the SESSION WORKING DIRECTORY,
261
+ i.e. the cwd printed at the end of this prompt; the institute root is the
262
+ `Data lives under …` directory above):
263
+ institute work file `<institute root>/Formal/<id>.lean`;
264
+ archived proof `<institute root>/Verified/Lean/<id>.lean`;
265
+ reusable definitions `VibeMath/Formal/Lib/` (workspace-root relative);
266
+ proved lemmas `VibeMath/Formal/Proved/` (workspace-root relative);
267
+ member-facing short form `Formal/<id>.lean` (institute-root relative) is
268
+ accepted and names the SAME file as `<institute root>/Formal/<id>.lean`.
235
269
  - The toolchain knobs leanCommand / leanArgs / leanTimeoutMs are tunable as well
236
270
  (e.g. leanCommand='lake' with leanArgs=['env','lean']); a missing Lean binary is
237
271
  reported as LEAN_NOT_FOUND, a host with no subprocess service as NO_SUBPROCESS
@@ -12,6 +12,21 @@ import { MATH_ENGINES, MATH_ENGINE_ORDER, mathEngineCandidates, mathEngineArgErr
12
12
 
13
13
  export const MATH_TOOL_NAME = 'math_computation'
14
14
 
15
+ // Sweep ruling: the cli policy is declared ONCE, in the descriptor (math-engines.js `cli.policy`).
16
+ // These two readers are the only place the rule is applied, so descriptor and behaviour cannot drift.
17
+ const CLI_POLICY = (MATH_ENGINES.cli && MATH_ENGINES.cli.policy) || {}
18
+
19
+ // Item-2 ruling: a commercial template's "VERIFY" provenance must REACH the user, and it is declared
20
+ // ONCE (descriptor `verify` + `verifyReason`). Assemble the fields ONLY when a reason exists, so an
21
+ // engine that declares nothing prints no empty slot.
22
+ function verifyFields(engine) {
23
+ const d = MATH_ENGINES[engine] || {}
24
+ if (!d.verify) return {}
25
+ const out = { verify: true }
26
+ if (typeof d.verifyReason === 'string' && d.verifyReason) out.verifyReason = d.verifyReason
27
+ return out
28
+ }
29
+
15
30
  export const MATH_PARAM_NAMES = Object.freeze([
16
31
  'mathComputation', 'mathMode', 'mathEngines', 'mathTimeoutMs', 'mathPackages', 'mathInstallScope',
17
32
  ])
@@ -73,11 +88,12 @@ export const MATH_PERSONA_TOOL_LINE = '- math_computation {op: probe|run|receipt
73
88
  // to both persona blocks) so an agent cannot miss that (a) the script is archived, (b) it may edit
74
89
  // it, and (c) re-running is what produces evidence for the edited code.
75
90
  export const MATH_ARCHIVE_WORKFLOW_LINE = '- 归档→编辑→重跑:mode:\'code\' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);'
76
- + '**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);'
77
- + '编辑后用 mode:\'file\' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——'
78
- + '报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。'
91
+ + '**成员的文件工具是按会话 cwd 解析的**,所以读它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有)。'
92
+ + '要拿到"改过代码"的证据,请**编辑你最初运行的那个源文件**,再用 mode:\'file\' 指向**同一个源路径**重跑:归档 id 以**源路径**为键,因此这落在**同一归档**的 **attempt ≥ 2**,并给出 `scriptChanged:true` 与 `previousReceipt`(指向上一次)。'
93
+ + '**指向归档副本本身**(`receipt.scriptAbs` 那个路径)按设计是**另一份新归档**:新 id、attempt 1、没有 `previousReceipt`、`scriptChanged:false`;旧 attempt 绝不会被覆盖,但它**不是**"同一归档的新 attempt"——工具会用 `fileIsArchivedScript` 与 `ARCHIVED_SCRIPT_RERUN` 警告明确说明。'
94
+ + '**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。'
79
95
 
80
- export const MATH_ARCHIVE_WORKFLOW_LINE_EN = '- Archive -> edit -> re-run: for mode:\'code\' the script original is at the receipt\'s scriptPath (Computation/<id>/script.<ext>, **relative to the project root**); member file tools resolve paths against the SESSION CWD, so open it via the ABSOLUTE `receipt.scriptAbs`, or join `receipt.cwd` with `receipt.scriptPath` (both are in the receipt); after editing, re-run it with mode:\'file\' to write a NEW receipt/attempt with a NEW scriptHash. **An old receipt is NOT evidence for edited code** - cite the receipt whose scriptHash matches the current code; the tool warns explicitly via scriptChanged / scriptChangedDuringRun.'
96
+ export const MATH_ARCHIVE_WORKFLOW_LINE_EN = '- Archive -> edit -> re-run: for mode:\'code\' the script original is at the receipt\'s scriptPath (Computation/<id>/script.<ext>, **relative to the project root**); member file tools resolve paths against the SESSION CWD, so READ it via the ABSOLUTE `receipt.scriptAbs`, or join `receipt.cwd` with `receipt.scriptPath` (both are in the receipt). To produce evidence for EDITED code, edit the SOURCE FILE you originally ran and re-run mode:\'file\' pointing at THAT SAME PATH: the archive id is keyed by the SOURCE PATH, so this lands on the SAME archive as attempt >= 2, with `scriptChanged:true` and `previousReceipt` pointing at the previous attempt. Pointing mode:\'file\' at the ARCHIVED COPY itself (the `receipt.scriptAbs` path) is a DIFFERENT archive BY DESIGN - a new id, attempt 1, no `previousReceipt`, `scriptChanged:false`: the earlier attempt is never overwritten, but it is NOT "a new attempt of the same archive", and the tool says so via `fileIsArchivedScript` and the `ARCHIVED_SCRIPT_RERUN` warning. **An old receipt is NOT evidence for edited code** - cite the receipt whose scriptHash matches the current code; the tool warns explicitly via scriptChanged / scriptChangedDuringRun.'
81
97
 
82
98
  // Round-6 (A): honesty about SUBSTITUTIONS. An alternative that weakens exactness or conclusion
83
99
  // strength must be declared, and the conclusion must never read as if the requested (exact) result
@@ -255,8 +271,10 @@ export function validateMathArgs(args, params) {
255
271
  if (mode !== 'code' && mode !== 'file' && mode !== 'expr') return bad('mode must be code|file|expr for op=run')
256
272
  const engine = args.engine || 'auto'
257
273
  if (engine === 'cli') {
258
- if (p.mathMode !== 'typed+shell') return bad('engine=cli is disabled while mathMode=' + p.mathMode, 'MATH_REFUSED', next('reason', { reason: 'policy' }))
259
- if (p.mathEngines.indexOf('cli') === -1) return bad('engine=cli is not in mathEngines', 'MATH_REFUSED', next('reason', { reason: 'engine-not-allowed' }))
274
+ // Sweep ruling (single source): the cli policy lives in the DESCRIPTOR; the code reads it here
275
+ // rather than hard-coding 'typed+shell', so the two can no longer drift apart.
276
+ if (CLI_POLICY.requiresMathMode && p.mathMode !== CLI_POLICY.requiresMathMode) return bad('engine=cli is disabled while mathMode=' + p.mathMode + ' (requires ' + CLI_POLICY.requiresMathMode + ')', 'MATH_REFUSED', next('reason', { reason: 'policy', requiresMathMode: CLI_POLICY.requiresMathMode }))
277
+ if (CLI_POLICY.requiresEngineInList && p.mathEngines.indexOf('cli') === -1) return bad('engine=cli is not in mathEngines', 'MATH_REFUSED', next('reason', { reason: 'engine-not-allowed', requiresEngineInList: true }))
260
278
  if (!isObj(args.cli)) return bad('engine=cli requires cli:{command,argv}', 'MATH_REFUSED', next('reason', { reason: 'missing-cli-command' }))
261
279
  if (mode === 'expr') return bad('cli does not support mode=expr', 'MATH_REFUSED', next('reason', { reason: 'expr-not-supported' }))
262
280
  }
@@ -277,6 +295,19 @@ export function validateMathArgs(args, params) {
277
295
 
278
296
  // ── failure / return shells ─────────────────────────────────────────────────────────────────────
279
297
  function next(kind, extra) { return Object.assign({ kind: kind }, extra || {}) }
298
+ /**
299
+ * S6: ONE wording for the version policy, so the install plan, the run path and the missing-package
300
+ * failure cannot drift into different sentences describing the same rule. Grep anchor:
301
+ * MATH_VERSION_POLICY (and its single call site family mathVersionPolicy).
302
+ */
303
+ /** S6: the ONE short clause that states the policy; the long sentence and the two human messages
304
+ * below are all built from it, so the semantics cannot drift. Grep anchor: MATH_VERSION_POLICY_CLAUSE. */
305
+ const MATH_VERSION_POLICY_CLAUSE = '只检查是否存在;版本求解交给包管理器'
306
+ const MATH_VERSION_POLICY = 'version-constraints-are-existence-only: 只按 base name 检查是否存在,约束本身从不校验(' + MATH_VERSION_POLICY_CLAUSE + ')'
307
+ function mathVersionPolicy(pinned) {
308
+ return MATH_VERSION_POLICY + ((pinned && pinned.length) ? ';本次未校验的约束:' + pinned.join(', ') : ';本次没有版本约束')
309
+ }
310
+
280
311
  function fail(code, engine, message, extra) {
281
312
  const out = { ok: false, op: null, engine: engine || null, code: code, message: message }
282
313
  if (extra && extra.next) out.next = extra.next
@@ -285,6 +316,8 @@ function fail(code, engine, message, extra) {
285
316
  if (extra && extra.missing) out.missing = extra.missing
286
317
  if (extra && extra.engines) out.engines = extra.engines
287
318
  if (extra && extra.available !== undefined) out.available = extra.available
319
+ if (extra && extra.configured) out.configured = extra.configured
320
+ if (extra && Array.isArray(extra.absent)) out.absent = extra.absent
288
321
  if (extra && extra.packages) out.packages = extra.packages
289
322
  // round-7 (live-session fix): the probe diagnostic must survive into the response, otherwise
290
323
  // "存在但不可用" is opaque (a real session showed exactly that).
@@ -318,6 +351,8 @@ async function userInstallNext(H, engine) {
318
351
  packageManager: mgr,
319
352
  packageManagerAvailable: mgr ? available : null,
320
353
  vendorUrl: d.vendor || null,
354
+ // Item-2: the not-found guidance carries the same provenance when the descriptor declares it.
355
+ ...verifyFields(engine),
321
356
  note: (mgr && !available)
322
357
  ? ('本机没有检测到包管理器 ' + mgr + ':建议命令无法运行,请按上面的官方地址手动安装(或先自行安装一个包管理器)。')
323
358
  : null,
@@ -345,6 +380,9 @@ function adaptHost(host) {
345
380
  // round-7 (fix 2): optional bundled-runtime discovery (absolute roots + absolute listing).
346
381
  runtimeRoots: typeof h.runtimeRoots === 'function' ? h.runtimeRoots : null,
347
382
  listDirAbs: typeof h.listDirAbs === 'function' ? h.listDirAbs : null,
383
+ // round-B (F4): an OPTIONAL host field - a host (or a test) that knows its own install layout can
384
+ // supply the per-engine candidate roots, so discovery never has to trust this process's environment.
385
+ installRoots: typeof h.installRoots === 'function' ? h.installRoots : null,
348
386
  log: typeof h.log === 'function' ? h.log : function () {},
349
387
  }
350
388
  adapted.__mathHost = true
@@ -367,12 +405,12 @@ export async function probeMathEngines(host, opts) {
367
405
  if (name === 'cli') continue // cli needs a caller-supplied command, so it is not auto-detected
368
406
  const hit = await resolveCandidate(H, d)
369
407
  if (!hit) continue
370
- const v = await probeVersion(H, d, hit)
408
+ const v = await probeVersion(H, d, hit.exe)
371
409
  if (!v.ok) {
372
410
  if (d.license === 'commercial') continue // installed but not usable: reported by op=run as LICENSE_REQUIRED
373
411
  continue
374
412
  }
375
- engines.push({ name: name, version: v.version, path: hit, license: d.license })
413
+ engines.push(Object.assign({ name: name, version: v.version, path: hit.exe, foundVia: hit.via, license: d.license }, verifyFields(name)))
376
414
  }
377
415
  const result = { ok: true, engines: engines, available: engines.length > 0 }
378
416
  PROBE_CACHE.set(H, result)
@@ -383,34 +421,68 @@ export async function probeMathEngines(host, opts) {
383
421
  // looked at the server process's PATH (R 4.6.1 landed in C:\Program Files\R\R-4.6.1\bin and was NOT
384
422
  // added to PATH). Add the known per-OS INSTALL locations, globbed by version, still AFTER PATH and
385
423
  // after the bundled runtime; existence is always proven by LISTING, never assumed.
424
+ // round-B (F4/F3): the candidate roots come from the HOST when it can supply them (`H.installRoots`),
425
+ // and only otherwise from this process's environment - so a test (or a host with its own layout) never
426
+ // depends on the ambient machine. ONE generic walker serves every engine: adding an engine is a table
427
+ // entry (roots + descriptor candidates), never a new code path. Existence is always proven by LISTING.
428
+ export function mathInstallRoots(engineName, env, win) {
429
+ const pf = String(env.ProgramFiles || 'C:/Program Files')
430
+ const pf86 = String(env['ProgramFiles(x86)'] || 'C:/Program Files (x86)')
431
+ const local = env.LOCALAPPDATA ? String(env.LOCALAPPDATA) : null
432
+ const home = env.USERPROFILE || env.HOME || null
433
+ const table = {
434
+ r: win
435
+ ? [pf + '/R', pf86 + '/R', local ? local + '/Programs/R' : null]
436
+ : ['/usr/lib/R', '/usr/lib64/R', '/Library/Frameworks/R.framework/Resources', '/usr/local/lib/R', '/opt/R'],
437
+ python: win
438
+ ? [local ? local + '/Programs/Python' : null, pf]
439
+ : ['/usr/local', '/usr', '/opt/python', '/opt/homebrew'],
440
+ octave: win
441
+ ? [pf + '/GNU Octave', local ? local + '/Programs/GNU Octave' : null]
442
+ : ['/usr/lib/octave', '/usr/local/octave', '/opt/octave', '/usr/share/octave'],
443
+ julia: win
444
+ ? [local ? local + '/Programs' : null, pf, home ? home + '/.juliaup/bin' : null]
445
+ : ['/opt', '/usr/local', home ? home + '/.juliaup/bin' : null],
446
+ matlab: win ? [pf + '/MATLAB'] : ['/usr/local/MATLAB', '/Applications'],
447
+ }
448
+ return (table[engineName] || []).filter((x) => typeof x === 'string' && x)
449
+ }
450
+
386
451
  async function knownInstallCandidates(H, engineName) {
387
452
  if (typeof H.listDirAbs !== 'function') return []
388
453
  const cands = mathEngineCandidates(engineName)
389
454
  if (!cands) return []
390
455
  const env = (typeof process !== 'undefined' && process.env) || {}
391
456
  const win = !!(typeof process !== 'undefined' && process.platform === 'win32')
457
+ let injected = null
458
+ if (typeof H.installRoots === 'function') {
459
+ try {
460
+ const r = await H.installRoots(engineName)
461
+ if (Array.isArray(r)) injected = r.filter((x) => typeof x === 'string' && x)
462
+ } catch (e) { injected = null }
463
+ }
464
+ // A JS empty array is TRUTHY: a host that answers `[]` must fall back to the built-in table,
465
+ // otherwise the whole per-OS stage is silently suppressed (task-2).
466
+ const roots = (Array.isArray(injected) && injected.length) ? injected : mathInstallRoots(engineName, env, win)
392
467
  const exts = win ? ['', '.exe', '.cmd'] : ['']
393
468
  const out = []
394
- const listDirs = async (dir) => { try { return ((await H.listDirAbs(dir)) || []).filter((e) => e && e.type === 'directory').map((e) => e.name) } catch (e) { return [] } }
395
- const listFiles = async (dir) => { try { return ((await H.listDirAbs(dir)) || []).filter((e) => e && e.type === 'file').map((e) => e.name) } catch (e) { return [] } }
469
+ const listNames = async (dir, kind) => {
470
+ try { return ((await H.listDirAbs(dir)) || []).filter((e) => e && e.type === kind).map((e) => e.name) } catch (e) { return [] }
471
+ }
396
472
  const match = async (dir) => {
397
- const files = await listFiles(dir)
473
+ const files = await listNames(dir, 'file')
398
474
  for (const c of cands) for (const x of exts) if (files.indexOf(c + x) !== -1) out.push(dir + '/' + c + x)
399
475
  }
400
- if (engineName === 'r') {
401
- const roots = win
402
- ? [String(env.ProgramFiles || 'C:/Program Files') + '/R', String(env['ProgramFiles(x86)'] || 'C:/Program Files (x86)') + '/R', env.LOCALAPPDATA ? String(env.LOCALAPPDATA) + '/Programs/R' : null]
403
- : ['/usr/lib/R', '/Library/Frameworks/R.framework/Resources', '/usr/local/lib/R', '/opt/R', '/usr/lib64/R']
404
- for (const root of roots) {
405
- if (!root) continue
406
- for (const d of await listDirs(root)) await match(root + '/' + d + '/bin') // windows versioned dirs
407
- await match(root + '/bin') // unix / framework layout
476
+ for (const root of roots) {
477
+ await match(root) // flat layout (juliaup, /usr/local)
478
+ await match(root + '/bin') // unix / framework layout
479
+ for (const d of await listNames(root, 'directory')) {
480
+ await match(root + '/' + d) // versioned flat (Python312, Julia-1.10)
481
+ await match(root + '/' + d + '/bin') // versioned install (R-4.6.1/bin)
482
+ for (const s of await listNames(root + '/' + d, 'directory')) {
483
+ await match(root + '/' + d + '/' + s + '/bin') // nested layout (Octave-9.2.0/mingw64/bin)
484
+ }
408
485
  }
409
- } else if (engineName === 'python' && win) {
410
- const local = env.LOCALAPPDATA ? String(env.LOCALAPPDATA) + '/Programs/Python' : null
411
- for (const d of (local ? await listDirs(local) : [])) await match(local + '/' + d)
412
- const pf = String(env.ProgramFiles || 'C:/Program Files')
413
- for (const d of await listDirs(pf)) if (/^Python[0-9]/i.test(d)) await match(pf + '/' + d)
414
486
  }
415
487
  return Array.from(new Set(out))
416
488
  }
@@ -421,17 +493,17 @@ async function resolveCandidate(H, d) {
421
493
  for (const c of cands) {
422
494
  try {
423
495
  const p = await H.resolveExecutable(c)
424
- if (typeof p === 'string' && p) return p
496
+ if (typeof p === 'string' && p) return { exe: p, via: 'path' }
425
497
  } catch (e) { /* not this one */ }
426
498
  }
427
499
  // round-7 (fix 2): DSH ships its own runtimes; PATH always wins, so this is a LAST resort. The
428
500
  // tree name is globbed (never hard-coded) and executable names come from the descriptor candidates.
429
501
  for (const p of await dshRuntimeCandidates(H, d.name)) {
430
- if (typeof p === 'string' && p) return p
502
+ if (typeof p === 'string' && p) return { exe: p, via: 'host-runtime' }
431
503
  }
432
504
  // round-9: then the known per-OS install locations (an engine installed without touching PATH).
433
505
  for (const p of await knownInstallCandidates(H, d.name)) {
434
- if (typeof p === 'string' && p) return p
506
+ if (typeof p === 'string' && p) return { exe: p, via: 'known-install' }
435
507
  }
436
508
  return null
437
509
  }
@@ -576,7 +648,7 @@ async function resolveEngine(H, requested, params, args) {
576
648
  const d = applyOverride({ name: name, desc: d0 }, params, args)
577
649
  if (params.mathEngines.indexOf(name) === -1) return fail('MATH_REFUSED', name, '引擎 ' + name + ' 不在 mathEngines 允许列表内', { next: next('reason', { reason: 'engine-not-allowed' }) })
578
650
  if (name === 'cli') {
579
- if (params.mathMode !== 'typed+shell') return fail('MATH_REFUSED', name, 'cli 被策略禁用(mathMode=' + params.mathMode + ')', { next: next('reason', { reason: 'policy' }) })
651
+ if (CLI_POLICY.requiresMathMode && params.mathMode !== CLI_POLICY.requiresMathMode) return fail('MATH_REFUSED', name, 'cli 被策略禁用(需要 mathMode=' + CLI_POLICY.requiresMathMode + ',当前 ' + params.mathMode + ')', { next: next('reason', { reason: 'policy', requiresMathMode: CLI_POLICY.requiresMathMode }) })
580
652
  let exe = null
581
653
  try { exe = await H.resolveExecutable(args.cli.command) } catch (e) { exe = null }
582
654
  if (!exe) return fail('MATH_ENGINE_NOT_FOUND', name, 'cli 命令无法解析:' + args.cli.command, { next: await userInstallNext(H, 'cli') })
@@ -590,16 +662,16 @@ async function resolveEngine(H, requested, params, args) {
590
662
  cliProbe = fv.diag || null
591
663
  if (fv.ok) cliVersion = fv.version
592
664
  }
593
- return { ok: true, name: name, desc: d, exe: exe, version: cliVersion, family: fam || null, probe: cliProbe }
665
+ return { ok: true, name: name, desc: d, exe: exe, version: cliVersion, family: fam || null, probe: cliProbe, via: 'cli' }
594
666
  }
595
667
  const hit = await resolveCandidate(H, d)
596
668
  if (!hit) continue
597
- const v = await probeVersionWithRetry(H, d, hit)
669
+ const v = await probeVersionWithRetry(H, d, hit.exe)
598
670
  if (!v.ok) {
599
671
  if (d.license === 'commercial' || d.versionOptional) {
600
- const lic = await checkLicence(H, d, hit)
601
- if (!lic.ok) return fail('MATH_ENGINE_LICENSE_REQUIRED', name, name + ' 已安装但许可不可用(仅厂商可激活)', { next: next('vendor', { engine: name, url: d.vendor || '' }) })
602
- return { ok: true, name: name, desc: d, exe: hit, version: 'unknown' }
672
+ const lic = await checkLicence(H, d, hit.exe)
673
+ if (!lic.ok) return fail('MATH_ENGINE_LICENSE_REQUIRED', name, name + ' 已安装但许可不可用(仅厂商可激活)', { next: next('vendor', Object.assign({ engine: name, url: d.vendor || '' }, verifyFields(name))) })
674
+ return { ok: true, name: name, desc: d, exe: hit.exe, version: 'unknown', via: hit.via }
603
675
  }
604
676
  // round-7 (live-session fix): the engine WAS found - so an install guide would be misleading
605
677
  // (the user does not need to install anything; the host path is what failed). Report the probe
@@ -609,9 +681,9 @@ async function resolveEngine(H, requested, params, args) {
609
681
  next: next('note', { guidance: '引擎已找到但探测失败:请检查宿主 subprocess 通路(冷启动/超时/权限),或用 mathEngineOverride 调整 versionArgv 模板;已装的引擎不需要重装。' }),
610
682
  })
611
683
  }
612
- const lic = await checkLicence(H, d, hit)
613
- if (!lic.ok) return fail('MATH_ENGINE_LICENSE_REQUIRED', name, name + ' 已安装但许可不可用(仅厂商可激活)', { next: next('vendor', { engine: name, url: d.vendor || '' }) })
614
- return { ok: true, name: name, desc: d, exe: hit, version: v.version }
684
+ const lic = await checkLicence(H, d, hit.exe)
685
+ if (!lic.ok) return fail('MATH_ENGINE_LICENSE_REQUIRED', name, name + ' 已安装但许可不可用(仅厂商可激活)', { next: next('vendor', Object.assign({ engine: name, url: d.vendor || '' }, verifyFields(name))) })
686
+ return { ok: true, name: name, desc: d, exe: hit.exe, version: v.version, via: hit.via }
615
687
  }
616
688
  const first = requested === 'auto' ? (params.mathEngines[0] || 'python') : requested
617
689
  return fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(试过:' + names.join(', ') + ')', { next: await userInstallNext(H, first) })
@@ -709,11 +781,14 @@ function assembleArgv(det, mode, payload) {
709
781
  }
710
782
  }
711
783
  if (mode === 'code' || mode === 'file') {
712
- return { argv: [det.exe].concat((d.scriptArgv || []).map((a) => a === '<script>' ? payload.scriptAbs : a)) }
784
+ // Sweep finding (HIGH): substitution must be STRING-level, not exact-element equality - matlab's
785
+ // descriptor embeds the placeholder (`run('<script>')`), so an exact match let the literal
786
+ // `run('<script>')` reach the engine and the archived script never ran.
787
+ return { argv: [det.exe].concat((d.scriptArgv || []).map((a) => String(a).split('<script>').join(payload.scriptAbs))) }
713
788
  }
714
789
  if (mode === 'expr') {
715
790
  if (!d.evalArgv) return { refused: 'engine does not support mode=expr' }
716
- return { argv: [det.exe].concat(d.evalArgv.map((a) => a === '<expr>' ? payload.expr : a)) }
791
+ return { argv: [det.exe].concat(d.evalArgv.map((a) => String(a).split('<expr>').join(payload.expr))) }
717
792
  }
718
793
  return { refused: 'unknown mode' }
719
794
  }
@@ -865,6 +940,14 @@ async function opProbe(H, args, params) {
865
940
  const probeDet = familyProbeDet(chosen)
866
941
  if (want.length && probeDet) packages = await probePackages(H, probeDet, want)
867
942
  else if (want.length && chosen && chosen.ok && chosen.name === 'cli') packages = { requested: want.map((s) => parsePackageSpec(s).name || s), found: {}, precheckSkipped: 'engine=cli:无法从命令名判断引擎族(不阻塞)' }
943
+ // S2 + round-9 F3: the absence list must exist BEFORE the early failure return, because the
944
+ // failure a user actually hits must carry the same evidence as the success shape.
945
+ const absentFor = (names) => params.mathEngines
946
+ .filter((e) => names.indexOf(e) === -1)
947
+ .map((e) => ((MATH_ENGINES[e] && MATH_ENGINES[e].resolveFrom === 'cli.command')
948
+ ? { engine: e, why: 'needs-a-caller-supplied-command' }
949
+ : { engine: e, why: 'not-found-on-this-machine' }))
950
+
868
951
  if (requested && chosen && !chosen.ok) {
869
952
  // Policy/absence refusal for the REQUESTED engine, with its own guidance.
870
953
  const out = Object.assign({}, chosen)
@@ -876,7 +959,7 @@ async function opProbe(H, args, params) {
876
959
  }
877
960
  if (!probe.available && !(chosen && chosen.ok)) {
878
961
  const first = requested || params.mathEngines[0] || 'python'
879
- const out = fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(请求:' + first + ')', { next: await userInstallNext(H, first), engines: [], available: false, packages: packages })
962
+ const out = fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(请求:' + first + ')', { next: await userInstallNext(H, first), engines: [], available: false, configured: params.mathEngines.slice(), absent: absentFor(probe.engines.map((e) => e.name)), packages: packages })
880
963
  out.op = 'probe'
881
964
  return out
882
965
  }
@@ -884,18 +967,15 @@ async function opProbe(H, args, params) {
884
967
  // available even when auto-detection found nothing - otherwise probe denied what run would do.
885
968
  const engines = probe.engines.slice()
886
969
  if (chosen && chosen.ok && !engines.some((e) => e.name === chosen.name)) {
887
- engines.push({ name: chosen.name, path: chosen.exe, version: chosen.version, license: (MATH_ENGINES[chosen.name] || {}).license })
970
+ engines.push(Object.assign({ name: chosen.name, path: chosen.exe, version: chosen.version, license: (MATH_ENGINES[chosen.name] || {}).license }, verifyFields(chosen.name)))
888
971
  }
889
- const engineInfo = (chosen && chosen.ok) ? { name: chosen.name, path: chosen.exe, version: chosen.version } : null
972
+ const engineInfo = (chosen && chosen.ok) ? Object.assign({ name: chosen.name, path: chosen.exe, version: chosen.version, foundVia: chosen.via }, verifyFields(chosen.name)) : null
890
973
  // round-9 (F3): the availability line must never be silently partial - name the CONFIGURED engines
891
974
  // that were not found (and why), instead of just listing the ones that happened to resolve.
892
975
  const foundNames = engines.map((e) => e.name)
893
976
  // Per-engine absence explanation. The key is `why` (NOT the reserved machine-readable vocabulary
894
977
  // key), so the parity guard's exhaustive `next.reason` set stays exactly the refusal vocabulary.
895
- const absent = params.mathEngines.filter((e) => foundNames.indexOf(e) === -1).map((e) => {
896
- if (MATH_ENGINES[e] && MATH_ENGINES[e].resolveFrom === 'cli.command') return { engine: e, why: 'needs-a-caller-supplied-command' }
897
- return { engine: e, why: 'not-found-on-this-machine' }
898
- })
978
+ const absent = absentFor(foundNames)
899
979
  return {
900
980
  ok: true, op: 'probe', engine: engineInfo ? engineInfo.name : null, engineInfo: engineInfo,
901
981
  engines: engines, available: true, packages: packages,
@@ -922,7 +1002,7 @@ async function opRun(H, args, params) {
922
1002
  const det = await resolveEngine(H, args.engine || 'auto', params, args)
923
1003
  if (!det.ok) return det
924
1004
  const desc = applyOverride(det, params, args)
925
- const det2 = { ok: true, name: det.name, desc: desc, exe: det.exe, version: det.version }
1005
+ const det2 = { ok: true, name: det.name, desc: desc, exe: det.exe, version: det.version, via: det.via }
926
1006
  // The generic escape hatch is recorded as `cli:<command>` (spec §4.3 / guards §18): the model and
927
1007
  // the receipt must be able to tell WHICH command was run, not just that it was "cli".
928
1008
  const cliCommand = args.cli && typeof args.cli.command === 'string' ? args.cli.command : ''
@@ -931,7 +1011,7 @@ async function opRun(H, args, params) {
931
1011
  const want = (args.packages && args.packages.length) ? args.packages : params.mathPackages
932
1012
  // round-9 (F7): version constraints are NOT enforced - we only check existence by base name.
933
1013
  const pinnedSpecs = want.filter((sp) => parsePackageSpec(sp).pinned)
934
- const versionPolicy = 'version-constraints-are-existence-only: 只按 base name 检查是否存在,约束本身从不校验(版本求解交给包管理器)' + (pinnedSpecs.length ? ';本次未校验的约束:' + pinnedSpecs.join(', ') : ';本次没有版本约束')
1014
+ const versionPolicy = mathVersionPolicy(pinnedSpecs)
935
1015
  // round-7 (finding 3): for cli the precheck runs against the FAMILY of `cli.command` (same
936
1016
  // discovery-shaped logic); an unrecognisable command SKIPS the precheck instead of reporting a
937
1017
  // false "missing" that would block the escape hatch.
@@ -942,7 +1022,7 @@ async function opRun(H, args, params) {
942
1022
  // timeout) must not be read as "missing" - that misled a real session.
943
1023
  const missing = (probeDet && !pk.probeFailed) ? want.map((s) => parsePackageSpec(s).name || s).filter((p) => Object.prototype.hasOwnProperty.call(pk.found, p) && !pk.found[p]) : []
944
1024
  if (missing.length) {
945
- return fail('MATH_MISSING_PACKAGES', det.name, '需要的包未安装:' + missing.join(', ') + '(只检查是否存在;版本求解交给包管理器)', {
1025
+ return fail('MATH_MISSING_PACKAGES', det.name, '需要的包未安装:' + missing.join(', ') + '(' + MATH_VERSION_POLICY_CLAUSE + ')', {
946
1026
  missing: missing,
947
1027
  next: next('agent-install', { engine: det.name, packages: missing, specs: want.slice(), dryRun: true }),
948
1028
  packages: pk,
@@ -1040,7 +1120,7 @@ async function opRun(H, args, params) {
1040
1120
  schema: 'vibe-math/math-computation-receipt@1',
1041
1121
  runId: runId, preset: H.designator, project: slug(root), op: 'run', mode: mode,
1042
1122
  attempt: attempt, attemptDir: dir, baseRunDir: baseDir,
1043
- engine: { name: engineLabel, path: det.exe, version: det.version, source: det.name === 'cli' ? 'cli' : 'path' },
1123
+ engine: { name: engineLabel, path: det.exe, version: det.version, source: det.name === 'cli' ? 'cli' : 'path', foundVia: det2.via },
1044
1124
  script: { path: scriptRel, sha256: scriptHash, bytes: Buffer.byteLength(scriptText, 'utf8') },
1045
1125
  scriptPath: scriptRel, scriptHash: scriptHash,
1046
1126
  // round-8 (P0/D3): member file tools are session-cwd relative, so the ABSOLUTE locations must be
@@ -1058,6 +1138,9 @@ async function opRun(H, args, params) {
1058
1138
  argv: assembled.argv.slice(),
1059
1139
  cwd: root,
1060
1140
  packages: { requested: want.slice(), found: pk.found },
1141
+ // round-B (F6): the constraint policy is durable evidence too - a receipt read months later must
1142
+ // show that constraints were existence-only and WHICH ones were not checked.
1143
+ versionPolicy: versionPolicy, constraintsNotEnforced: pinnedSpecs.slice(),
1061
1144
  exit: r.timedOut ? null : (r.exit === undefined ? null : r.exit),
1062
1145
  timedOut: !!r.timedOut,
1063
1146
  ms: Number(r.ms || 0),
@@ -1119,11 +1202,13 @@ async function opRun(H, args, params) {
1119
1202
  next: next('engine-override', { engine: det.name, argv: assembled.argv.slice(), hint: MATH_BAD_ARGV_HINT }),
1120
1203
  })
1121
1204
  out.exit = r.exit
1205
+ out.stdout = fullOut.slice(0, MATH_CAPS.stdout)
1122
1206
  out.stderr = fullErr.slice(0, MATH_CAPS.stderr)
1123
1207
  return Object.assign(out, archiveFields())
1124
1208
  }
1125
1209
  const out = fail('MATH_NONZERO_EXIT', engineLabel, '引擎以非零退出码结束:' + r.exit, { argv: assembled.argv.slice(), receipt: receiptRef })
1126
1210
  out.exit = r.exit
1211
+ out.stdout = fullOut.slice(0, MATH_CAPS.stdout)
1127
1212
  out.stderr = fullErr.slice(0, MATH_CAPS.stderr)
1128
1213
  return Object.assign(out, archiveFields())
1129
1214
  }
@@ -1212,7 +1297,7 @@ async function opInstall(H, args, params) {
1212
1297
  if (!d) return fail('MATH_INVALID_ARGUMENT', engineName, 'unknown engine: ' + engineName)
1213
1298
  if (d.license === 'commercial' || !d.install) {
1214
1299
  return fail('MATH_REFUSED', engineName, engineName + ' 永不代装(商业引擎只给厂商指引)', {
1215
- next: next('vendor', { engine: engineName, url: d.vendor || '' }),
1300
+ next: next('vendor', Object.assign({ engine: engineName, url: d.vendor || '' }, verifyFields(engineName))),
1216
1301
  })
1217
1302
  }
1218
1303
  const scope = args.scope || params.mathInstallScope || 'user' // per-call only; never persisted anywhere
@@ -1248,7 +1333,7 @@ async function opInstall(H, args, params) {
1248
1333
  const plan = {
1249
1334
  engine: engineName, scope: scope, manager: activeManager, commands: commands,
1250
1335
  managerAssumed: !!(managerInfo && managerInfo.assumed), managerWhy: managerInfo ? managerInfo.why : '',
1251
- versionPolicy: '版本求解交给包管理器(本工具只检查是否已安装,并把 pkg<op>version 原样透传)',
1336
+ versionPolicy: mathVersionPolicy(args.packages || []),
1252
1337
  note: scope === 'user' ? '将安装到用户级目录' : '仅本次系统作用域(不会被记住)',
1253
1338
  }
1254
1339
  const planToken = sha256(JSON.stringify({ engine: engineName, packages: args.packages, scope: scope, commands: commands }) + '|planVersion=1').slice(0, 32)
@@ -1269,12 +1354,18 @@ async function opInstall(H, args, params) {
1269
1354
  const code = (r && r.timedOut) ? 'MATH_TIMEOUT' : 'MATH_NONZERO_EXIT'
1270
1355
  const out = fail(code, engineName, '安装失败(' + c.argv.join(' ') + '):' + (r ? ('exit=' + r.exit) : 'no subprocess'), { next: next('note', { audit: 'Computation/installs/' + planToken + '.json' }) })
1271
1356
  out.audit = 'Computation/installs/' + planToken + '.json'
1357
+ // S4/S5 (non-breaking): install failures are discriminated by op + installedSoFar + timedOut,
1358
+ // NOT by a new failure code (MATH_FAILURE_CODES is frozen). Mirrors the `out.audit`
1359
+ // post-assignment and needs nothing from fail()'s key whitelist.
1360
+ out.op = 'install'
1361
+ out.timedOut = !!(r && r.timedOut)
1362
+ out.installedSoFar = results.filter((x) => x.exit === 0).map((x) => x.argv)
1272
1363
  return out
1273
1364
  }
1274
1365
  }
1275
1366
  const audit = { schema: 'vibe-math/math-computation-install@1', planToken: planToken, scope: scope, manager: activeManager, commands: commands, results: results, exit: 0, timedOut: false, installed: args.packages, before: {}, after: {}, rollback: rollbackFor(d, exe, args.packages, uninstallTmpl), network: 'not-enforced-by-plugin' }
1276
1367
  await H.writeText('Computation/installs/' + planToken + '.json', JSON.stringify(audit, null, 2))
1277
- return { ok: true, op: 'install', engine: engineName, executed: true, scope: scope, audit: 'Computation/installs/' + planToken + '.json', message: '已安装:' + args.packages.join(', ') + '(审计:Computation/installs/' + planToken + '.json)' }
1368
+ return { ok: true, op: 'install', engine: engineName, executed: true, scope: scope, installedSoFar: commands.map((c) => c.argv), audit: 'Computation/installs/' + planToken + '.json', message: '已安装:' + args.packages.join(', ') + '(审计:Computation/installs/' + planToken + '.json)' }
1278
1369
  }
1279
1370
 
1280
1371
  // ── round-6 (B/D): package specs + Python package-manager dispatch ──────────────────────────────
@@ -1287,7 +1378,7 @@ export function parsePackageSpec(spec) {
1287
1378
  const raw = String(spec == null ? '' : spec).trim()
1288
1379
  if (!raw) return { ok: false, problem: 'empty package spec' }
1289
1380
  if (!PKG_SPEC_RE.test(raw)) {
1290
- return { ok: false, problem: 'unsupported version syntax: "' + raw + '"(版本求解交给包管理器:请用它能理解的形式,例如 pip 的 pkg==1.2 或 conda 的 pkg=1.2;本工具只检查是否已安装)' }
1381
+ return { ok: false, problem: 'unsupported version syntax: "' + raw + '"(' + MATH_VERSION_POLICY_CLAUSE + ':请用它能理解的形式,例如 pip 的 pkg==1.2 或 conda 的 pkg=1.2)' }
1291
1382
  }
1292
1383
  const name = raw.replace(/(?:==|>=|<=|~=|!=|>|<|=).*$/, '').replace(/\[.*$/, '')
1293
1384
  return { ok: true, name: name, spec: raw, pinned: raw !== name }