四个预设(v2/v3/v4/v5)共用同一个工具。规格与冻结件只在开发检出里(
_oneoff/**不随包发布,files[]不含它;随包读者请看本文件正文的契约与穷举表):_oneoff/math-computation-spec-v3.md(rev-3)、_oneoff/mc-P1-ready/INTERFACE-FREEZE.md、mc-P1-ready/{engines.md,tool-schema.json,receipt.md,prompts.md,guards.md}。 本文件只讲已实现的行为与明确不能保证的东西,不含性能或效果承诺。
math_computation 让成员先在运行时探测本机可用的计算引擎与包,再以统一接口执行计算,并把每次执行落成可复核回执:脚本原件、完整输出、实际 argv、解释器/引擎版本、退出码与耗时。它比"随手用宿主 shell 跑一条命令"多出来的只有三件事——探测、统一接口、归档。
P1 引擎集合:python、r、octave、julia(自由软件,全功能)+ matlab、maple、wolfram(商业:只探测与许可检查,永不安装,有证即可用)+ cli(通用逃生口,默认开启)。SageMath 属 P2,P1 不注册。
| 参数 | 取值 | 默认 | 说明 |
|---|---|---|---|
mathComputation |
off | auto | on |
auto |
off 是真 no-op:不探测、不执行、不在提示词里出现;auto 探测到引擎才工作 |
mathMode |
typed | typed+shell |
typed+shell |
只是提示词策略(见 §6):允许在工具不可用时用宿主 shell 兜底,但结论必须标注"未经工具归档";typed 不提 shell,并禁用 engine:'cli' |
mathEngines |
引擎名数组 | [python, r, octave, julia, matlab, maple, wolfram, cli] |
允许的引擎及其探测顺序;去掉 cli 即关闭逃生口 |
mathTimeoutMs |
正整数(≥1000) | 60000 |
单次执行上限;超时后主动终止进程 |
mathPackages |
字符串数组 | [] |
默认要求存在的包/工具箱(缺包只报告,不安装) |
mathInstallScope |
user | system |
user |
作用域只对当次调用生效,永不记忆;多数包管理器没有 system 模板,此时 system 请求会被拒绝 |
非法值一律回落默认(枚举非白名单 → 默认;mathTimeoutMs 非正 → 默认并抬到 ≥1000;mathEngines 过滤未知项、去重、空则回落默认)。
math_computation {
op: 'probe' | 'run' | 'receipt' | 'install',
engine?, mode?: 'code'|'file'|'expr', code?|file?|expr?,
packages?, timeoutMs?, captureFiles?, record?,
cli?: { command, argv?, stdin? }, // engine='cli' 必填
scope?: 'user'|'system', dryRun?, confirm? // op='install'
}
probe:只探测引擎/版本/许可与(可选的)包,不执行用户代码。没有可用引擎时返回MATH_ENGINE_NOT_FOUND+ 逐 OS 的用户安装指引。run:mode:'code'由工具把源码落成script.<ext>再执行;mode:'file'只接受项目内路径(复制进回执目录后执行);mode:'expr'走该引擎的 eval 模板(表达式只占一个 argv 元素)。engine:'cli'时 argv =cli.command+cli.argv,mode:'expr'被拒绝。receipt:对既有Computation/<id>/幂等重渲染receipt.md、补写缺失产物,不重跑。install:两步。先dryRun(默认)返回计划与planToken;执行必须把该 token 原样回传confirm,token 不符即拒绝。商业引擎一律拒绝。
失败码(11 个,全部 MATH_ 前缀):MATH_NOT_AVAILABLE、MATH_ENGINE_NOT_FOUND、MATH_ENGINE_LICENSE_REQUIRED、MATH_ENGINE_UNUSABLE、MATH_MISSING_PACKAGES、MATH_TIMEOUT、MATH_NONZERO_EXIT、MATH_ENGINE_BAD_ARGV、MATH_REFUSED、MATH_INVALID_ARGUMENT、MATH_NO_SUBPROCESS。每个"环境缺失"类失败都带 next(自装指引 / 代装计划 / 厂商链接 / 启用建议)。
"没有 subprocess 服务"与"没有引擎"是两件事(避免误导用户去装 python):
- 宿主若能声明自己没有 subprocess 服务(预设可选提供
hasSubprocess:()=>false),run与probe一律返回MATH_NO_SUBPROCESS+next.kind:'note'; - 宿主不提供该声明时:命令解析不到 ⇒
MATH_ENGINE_NOT_FOUND(附逐 OS 安装指引);能解析但spawn拿不到句柄(返回null)⇒MATH_NO_SUBPROCESS。 - 机读词汇表(穷举;权威副本只在开发检出里:
_oneoff/mc-P1-ready/tool-schema.json#refusalVocabulary(不随包发布),由 parity 守卫盯着,改名即红):code(11 个):MATH_NOT_AVAILABLEMATH_ENGINE_NOT_FOUNDMATH_ENGINE_LICENSE_REQUIREDMATH_ENGINE_UNUSABLEMATH_MISSING_PACKAGESMATH_TIMEOUTMATH_NONZERO_EXITMATH_ENGINE_BAD_ARGVMATH_REFUSEDMATH_INVALID_ARGUMENTMATH_NO_SUBPROCESS;next.kind(7 个):user-install(用户自装指引)、agent-install(代装计划/计划就绪)、vendor(商业引擎厂商指引)、enable(mathComputation:'off'的启用建议)、engine-override(bad-argv ⇒ 用mathEngineOverride覆盖模板)、reason(机读拒绝原因)、note(补充说明,如无 subprocess);next.reason(13 个,仅当next.kind==='reason'):policy、engine-not-allowed、missing-cli-command、expr-not-supported、mode-not-supported、path-outside-project、file-not-found、system-scope-unsupported、plan-token-mismatch、archive-missing、receipt-unparsable、no-subprocess、unsupported-version-syntax(包规格写法不被支持:版本求解交给包管理器,见 §5)。 调用方不必解析文案:按code+next.kind(必要时next.reason)分支即可;新增或改名必须同时更新这里与tool-schema.json,否则守卫变红(守卫还会校验每个reason:取值都是字符串字面量,避免以常量绕开"穷举"检查)。
<项目或研究所根>/Computation/<runId>/
script.<ext> # 唯一权威源码(先落盘再执行)
stdout.txt / stderr.txt # 完整输出(返回体只给 ≤64KB 截断版)
receipt.json / receipt.md # 机器可读 + 人读
captured/… # captureFiles 的项目内副本
runId = <preset>-<projectSlug>-<sha256(engine|mode|规范化的脚本|排序后的包需求)[0:12]>——不含墙钟:同引擎、同脚本、同包要求在同一项目下命中同一目录。receipt.json记录:引擎与版本、脚本路径与哈希、实际 argv、cwd、退出码/超时/耗时、截断标记、产物哈希、network:"not-enforced-by-plugin"。- 复跑:
math_computation { op:'run', engine:'<e>', mode:'file', file:'Computation/<id>/script.<ext>' },或直接照receipt.json的argv+cwd重放。 - 报告里"我算过"要给
Computation/<id>/receipt.json;回执是经验证据,不是证明——对象是否已验证仍由各预设既有的验证/共识路径决定。
回执与返回体都带 scriptPath(归档脚本周相对路径)与 scriptHash(脚本字节的 sha256),所以"这次跑的到底是哪份代码"是可查的:
mode:'code'把源码落成Computation/<id>/script.<ext>;agent 可以用普通文件工具打开并编辑它。- 要拿到"改过代码"的证据,编辑你最初运行的那个源文件,再用
mode:'file'指向同一个源路径重跑:归档 id 以源路径为键,所以这会写出同一归档 id 的新 attempt(新scriptHash);重跑归档副本本身(receipt.scriptAbs那个路径)则是另一份新归档(新 id、attempt 1、无历史,见 §3.5 下方说明),不会给出"同一归档的新 attempt"。 - 旧回执对修改后的代码无效。
mode:'file'的回执按路径归属 archive id(不是按内容),所以"同一文件编辑后重跑"落在同一 id 的新 attempt,并显式给出:scriptChanged:true:该文件当前哈希与上一份回执记录的scriptHash不一致(旧回执不再代表当前代码);scriptChangedDuringRun:true:文件在本次运行期间被改动(典型是另一个成员同时在编辑)——运行前后各取一次哈希才可能发现;- 两者都同时进入
warnings[](SCRIPT_CHANGED_SINCE_LAST_RECEIPT/SCRIPT_CHANGED_DURING_RUN)与receipt.md,绝不静默。
op:'receipt'会重新核对归档脚本的当前哈希与回执记录,返回scriptChanged/currentScriptHash;它只读返回既有数据,仅补写缺失的receipt.md,从不改写已有回执文件。归档目录不存在时返回MATH_REFUSED+next:{kind:'reason', reason:'archive-missing'}(显式拒绝,不是"参数错误");receipt.json存在但无法解析时仍是MATH_INVALID_ARGUMENT(reason:'receipt-unparsable',那确实是坏掉的产物)。- 哈希的能力边界(按设计,不是缺陷):
scriptHash/scriptChanged是内容哈希——"内容相同但换了 inode/mtime(删了重建、复制覆盖)"与"改了又改回原样"无法区分,因此也不会被告警;需要区分"文件被动过"请用宿主的版本守卫(ctx.fs的expected.version)。每会话模块实例的生命周期跟随该会话对象(v4 原有生命周期,非本轮引入)。 - 手改状态文件里的未知键不会被静默丢弃(round-4):v5 从研究所状态文件恢复参数时,白名单之外的键会被忽略,但会记入
vibe_v5_status的diagnostics({kind:'state-dropped-keys', where, keys})并打印警告;同一归一化器也服务vibe_v5_set/vibe_v5_configure,所以未知键在任何入口都不会无声消失。
- 哈希的能力边界(按设计,不是缺陷):
- 归档是追加式的:同一 id 的首次运行落在
Computation/<id>/,之后每次运行落在Computation/<id>/attempts/<n>/(n 从 2 开始,取第一个空位);已有文件永不覆盖。并发下同一 id 的两个运行由模块按 id 串行分配,因此一定分别拿到 attempt 1 / attempt 2,不会共用目录(跨进程并发见 §6 的限制说明)。 - 失败关闭(fail-closed):源文件在运行期间消失 ⇒
scriptChangedDuringRun:true;op:'receipt'读不到归档脚本(被删/移走)⇒scriptChanged:true+currentScriptHash:null。任何"读不到/对不上"都按"旧回执不可用"处理,绝不静默当真。 - 保留上限:每个 id 最多
MATH_ARCHIVE_MAX_ATTEMPTS_PER_RUN=20 个 attempt、每个项目最多MATH_ARCHIVE_MAX_RUNS=200 个 run 目录(后者仅在宿主提供可选listDir时检查)。超过只告警(ARCHIVE_RETENTION_EXCEEDED),永不自动删除。
验证者(或复核代理)要查的六件事:① 打开 receipt.json 确认 scriptPath;② 重新计算该脚本 sha256,与 scriptHash 逐字相等;③ 确认 scriptChanged 与 scriptChangedDuringRun 均为 false(否则该回执不足以支撑结论);④ 确认 sourceFile(若有)正是被验证的源码,且 attempt/attemptDir 是当前代码对应的最新 attempt;⑤ 按 argv + cwd 重放,结果一致;⑥ 一旦改代码,必须重跑并引用新回执,报告里的旧回执撤下。
- 用户自己装:工具在缺引擎时返回逐 OS 命令(Windows/macOS/Linux)与官方链接;商业引擎只给厂商安装包与激活说明。
- 代理代装:
plan → confirm(token 绑定该计划,内容变了就失效);默认 user 作用域(pip --user、用户 depot、R_LIBS_USER、Octave 用户包目录…);system 作用域必须每次显式指定且不被记住;每次执行写审计Computation/installs/<planToken>.json(计划、作用域、管理器、命令、退出码、before/after、卸载命令模板)。
诚实的回滚说明:包安装没有通用回滚。工具保证的是——记下确切命令与 before/after 版本(卸载可验证)、默认 user 作用域限制影响面、提供各管理器的卸载模板;不做自动回滚。
python 的安装计划按解释器自身环境选择包管理器,而不是固定 pip:
| 检测到的环境 | 计划里的命令 | 依据 / 说明 |
|---|---|---|
conda 环境(解释器路径含 /envs/、/conda、/miniconda*、/anaconda*、/mambaforge、/miniforge)且 conda 可解析 |
conda install -y -c conda-forge <pkg>(卸载 conda remove -y <pkg>) |
路径标记 + 可解析确认 |
同上但只有 mamba |
mamba install -y -c conda-forge <pkg>(卸载 mamba remove -y <pkg>) |
同上 |
uv 与解释器同目录(uv 管理的 venv) |
uv pip install <pkg>(卸载 uv pip uninstall <pkg>) |
同目录才算,PATH 上别处的 uv 不算 |
| 其它 | python -m pip install --user <pkg>(卸载 python -m pip uninstall -y <pkg>) |
文档化回退,且计划里标 managerAssumed:true |
计划、返回消息与审计 JSON 都带 manager / managerAssumed / managerWhy——歧义时回退 pip 并明说"计划假设用 pip",绝不猜。
这三个引擎没有系统级模板,scope:'system' 一律返回 MATH_REFUSED + next.reason='system-scope-unsupported',消息里带每引擎的理由:
- R:用户库是
R_LIBS_USER;系统级要写发行版包目录(需 root 或发行版包管理器)。 - Octave:
pkg install装进用户包目录;系统级要写 Octave 的share/packages(需 root)。 - Julia:
Pkg.add装进当前活动环境 / 用户 depot(JULIA_DEPOT_PATH);"系统级"不是 Julia 的概念。
python 走 pip 时仍有真实的系统模板(不带 --user);conda/mamba/uv 没有系统模板,各自带理由。
-
策略:版本求解由包管理器负责,本工具只检查包是否存在(按 base name 探测与报缺),安装时把规格原样透传。计划里带
versionPolicy说明。 -
接受的写法:
名称、名称[extras]、名称<op>版本,其中<op>∈==>=<=~=!=><=(例如 pip 的numpy==1.2、conda 的numpy=1.2)。 -
拒绝的写法:空格、
;、|、&、$、反引号、@、括号等 shell 危险或管理器不认识的语法 ⇒MATH_INVALID_ARGUMENT+next.reason='unsupported-version-syntax'(不会把可疑字符串丢给 shell)。 -
冻结参与集(freeze/prune)规则(v4,round-9 P2/D4 的设计决定):验证/会议开始时把当时的名册快照下来(
rosterSnapshot)并记录rosterVersion,所有视图(status()的consensus/meeting/verify、以及内部的allSpoke/allVoted判据)一律读快照,不再从活的residents重新推导。三条配套语义:① 移除成员时同时从冻结集里剔除它(removeMember),否则在飞投票永远凑不齐、验证/会议会卡死——v4 既有契约是"移除即释放等待"(e2e-v4-fixesT26/T31 钉住这一点);② 新增成员不进入已冻结的集合(冻结的意义);③ 成员集真的变化时rosterVersion递增,让任何视图都能看出自己读到的是否已过期。约定:没有进行中的验证/会议时,status().frozenParticipants为显式null(不是空数组、也不是旧的活名册),rosterVersion仍然给出当前值。- 读法(推荐):
status().frozen是原子三元组{version, participants, kind}——participants是快照本身,version是冻结时记录、并在每次名册变动时重新盖章的版本(绝不是"活计数器配旧集合"),kind∈'verify'/'meeting'(vibe-math-v4.js:3609;六条守卫见tests/formal-verify-v4.test.mjs:1582-1596,其中包含"frozen.version必须等于快照记录的版本"与"hire/fire 后活计数器严格递增")。frozenParticipants已标注 deprecated(:3610):它是frozen.participants的别名,只为兼容旧读取者保留;新代码请读frozen,不要把别名与status().rosterVersion(活计数器)混用。
- 读法(推荐):
-
N1:编辑脚本后的"变更检测"到底对什么生效。归档 id 以源路径为键(
mode:'file'用file:<项目内相对路径>,mode:'code'/'expr'用脚本文本):- 编辑同一个源文件再跑 ⇒ 同一个归档 id、
attempt ≥ 2(落在Computation/<id>/attempts/<n>/)、scriptChanged:true、previousReceipt{runId,attempt,scriptHash}指向上一 attempt,且上一 attempt 的脚本与回执绝不改写。这是 P2a 承诺的核对流程,已在真机与共享套件里双向验证。 - 指向另一个源路径(例如上一 attempt 的副本、或新写的文件)⇒ 按设计就是新归档:新 id、
attempt:1、scriptChanged:false、previousReceipt:null,且sourceHashBefore == sourceHashAfter(文件在跑之前就已经是编辑后的内容)。这不是变更检测失效,而是"该 id 从来没有历史回执"。 - 运行响应现在显式带
runId与attempt(此前只有receipt.runId,调用方根本无法核对"是不是同一个归档")。 mode:'file'指向Computation/…的归档脚本副本是允许的:projectRel()只做词法归一(拒绝绝对路径与越出项目根的..,不限制目录名),所以该路径会被正常读取并执行。但按设计那是另一份新归档:新 id、attempt 1、无previousReceipt、scriptChanged:false,旧 attempt 不会被覆盖。响应会用fileIsArchivedScript:true与警告码ARCHIVED_SCRIPT_RERUN说明"这不是同一归档的新 attempt",避免把"新归档无历史"误读成"变更检测失效"。- 关于 nonce:归档 id 不含任何随机数,给定 (engine, mode, 键, packages) 完全确定;若你在某个路径里看到 nonce 样的后缀,那是调用方自己的工作目录命名,不是模块产生的。
- 编辑同一个源文件再跑 ⇒ 同一个归档 id、
-
N4:
project字段与 receipt 路径里的项目名是两个东西。project是配置层的项目标识(未配置项目时由工作区路径派生,所以会出现C-Users-…-vmself6这种 slug,它是显示/配置值);而 receipt/归档路径一律用Projects/<当前项目名>(默认default)。两者不一致是层次不同,不是命名错误;改名会破坏既有状态与归档路径,故只在此说明。 -
冻结视图的版本语义(v4 P2):冻结视图(
status().consensus、report().verify)报的是快照当时的rosterVersion,而status().rosterVersion是当前活计数器;成员集变化后两者应当不同——这个差值正是"你读到的是过期参与集"的信号。断言方式:两个冻结视图彼此相等,活计数器 ≥ 快照版本(tests/formal-verify-v4.test.mjs的 D4 跨视图用例)。 -
跨引擎位级一致(真机):python 与 R 的计算结果在 16 位有效数字上一致;第 17 位是
digits=22打印出的 double 精度噪声,不是错误答案。别把它当成缺陷。
projects:[]与project:"default"+frameworkRoot并存:它们描述的是两个不同层次——project是当前工作项目名(默认default),frameworkRoot是该预设的框架根目录(语料/状态/论文都相对它解析),而projects是可切换的项目清单(尚未配置过项目时为空数组是诚实的)。结论:不改行为,只在此说明字段含义(改名会破坏既有状态文件与文档)。cli逃生口的归档脚本扩展名是.txt,原生引擎是各自扩展名(.py/.R/.jl…):这是有意为之——cli的cli.command可能是任何解释器/工具,我们不知道它期待什么扩展名,用一个中性扩展名.txt反而更诚实(不会暗示"这一定是 python")。内容与scriptHash与原生路径完全一致(同一 runId 计算)。结论:不改行为,在此说明。
- 发现顺序(PATH 永远优先):① PATH 上的描述符候选名(
python3→python→py、Rscript→R、octave→octave-cli、julia);② 以上都失败、且宿主声明了可选字段runtimeRoots+listDirAbs时,最后扫描 DSH 自带运行时<root>/dsh-runtimes/*/dependencies/<engine>/<候选名>{,.exe,.cmd}——树名通配(不写死dsh-primary-runtime),且只接受描述符自己的候选名(不会塞进无关二进制)。<root>由宿主给出(当前四套预设:若DSH_HOME已设置则以它为唯一根,否则用用户主目录下的.dsh——显式设置即隔离发现,测试与定制部署都靠这个)。 - 找到可用引擎就报它的真实版本(
engineInfo.path+version),不再给安装指引;只有一个可用引擎都找不到时,才按 OS 给安装指引——而且是对请求的那个引擎(不是"第一个允许的引擎")。op:'probe'的engine/engineInfo就是请求的引擎;请求的引擎缺失时,code/message/next全部指向它(历史上这里会回显python)。 mathEngineOverride不是发现手段:它只按引擎名覆盖 argv 模板(如{python:{versionArgv,scriptArgv,evalArgv,packageProbe}}),不能指定一个可执行文件。所以MATH_ENGINE_NOT_FOUND的正解是:装上引擎、把解释器放进 PATH、让上面的 DSH 运行时被扫到,或用engine:'cli'+cli.command显式给命令。- round-9 响应字段(真机发现后新增,文档与实现同步):
- 引擎发现顺序:PATH → 宿主自带的运行时(
runtimeRoots+listDirAbs)→ 各操作系统的已知安装目录(mathInstallRoots(engine, env, win),覆盖 R / Octave / Julia / MATLAB 与 Windows 上的 Python;Maple / Wolfram 布局不固定,未纳入)。存在性一律靠列目录证明。宿主若提供可选的installRoots(engine),则以它为准(不再读模块进程的ProgramFiles/LOCALAPPDATA),测试与自定义布局因此都不依赖机器环境。
- 引擎发现顺序:PATH → 宿主自带的运行时(
source与foundVia:回执与engineInfo的source是粗粒度标签(cli=显式命令;其余一律path);发现来源看新增的foundVia(path=PATH/宿主解析、host-runtime=DSH 自带运行时、known-install=各 OS 已知安装目录;显式命令时为cli)。probe返回configured(配置里启用的引擎全集)与absent[](每个缺席引擎带why:not-found-on-this-machine/needs-a-caller-supplied-command),message 也会点名"已配置但本机未发现:…"——可用性行不再静默地只报一部分(F3)。run的成功响应与MATH_MISSING_PACKAGES失败都带versionPolicy与constraintsNotEnforced:[…]:明确"版本约束只按 base name 查存在、从不校验",并点名本次未校验的约束(无约束时为空数组)——杜绝"静默丢弃约束"(F7)。MATH_TIMEOUT与MATH_NONZERO_EXIT的证据字段一致:都带argv、receipt、exit(超时为null)与stderr(同样的截断上限),超时另有timedOut:true(F5)。超时的部分输出还进持久回执:receipt.json带partialStdout/partialStderr(各取最后 2000 字符,math-computation.js:1089-1090;响应里的stdout/stderr走同一来源,:1131-1132),所以事后只看回执也能看到超时前产生了什么。
- 商业模板的"需人工确认"来源会到用户手里(item 2 实测):描述符里的
verify/verifyReason(例如 maple 的"CLI 拼写随版本变化,需在持证机器上确认")现在会出现在probe的每引擎条目与engineInfo,以及厂商指引next(vendor载荷、以及引擎未找到时的安装指引)里;字段只在描述符声明了verify时出现,未声明的引擎不打印空槽。共享套件 §30 断言"理由到达响应"与"无声明则无空槽",_oneoff/auditR2/verify-provenance-proof.mjs以同一谓词证明单点去除理由会变红。 engine:'cli'的三条行为:① 包预检按cli.command的族执行(python*/Rscript/octave/julia用对应描述符的探测);命令族不可识别 ⇒ 跳过预检并给出PACKAGE_PRECHECK_SKIPPED警告(不误报缺包、不阻塞逃生口)。②mode:'code'/'file'时归档脚本的绝对路径会自动追加到 argv 末尾(回执里cli.scriptAppended: true;cli.argv仍是调用方给的 argv)——否则会掉进 REPL、exit=0而 stdout 为空,读起来像成功。但若调用方 argv 自带程序槽(-c/-m/-e/--eval/--command),则调用方 argv 优先、不追加(否则脚本会被当作多余参数,例如python -c 'print(1)' script.txt会把脚本喂给sys.argv[1]);此时回执给cli.scriptAppended:false+cli.scriptSkipped说明原因。③engineInfo.version取自真实的版本探测(族可识别时),不再是"unknown"。- 真实宿主上的"空 spawn"与失败可见性(round-7 实测):真实会话里宿主的
subprocess通路在会话早期可能对一次 spawn 返回空(exit=null)。因此:版本探测最多尝试 3 次(20s / 45s / 45s + 退避),且只在看起来像冷启动/空返回(timedOut、exit===null、spawned=false)时重试——确定性失败(非零退出、输出无法解析)不重试。仍然失败时返回MATH_ENGINE_UNUSABLE并携带机读诊断probe: {argv, exit, timedOut, ms, spawned, stderrTail, attempts, retried, retryDiag};引擎已找到就绝不再给"去装引擎"的指引(那会误导),改为next.kind:'note'。包预检同样重试;若预检始终跑不起来,不判缺包、不阻塞,给出PACKAGE_PRECHECK_UNKNOWN警告(附 attempts/argv)——"探测失败"永远不会被读成"包没装"。 - 包存在性探测的 argv 形态:python 的探针代码遍历
sys.argv[1:],所以包名按每个一个 argv 项传入(<pkgs...>);R/Julia/Octave 的模板把逗号串插进代码(<pkgs>/__PKGS__)。真机上曾经把numpy,pandas当成一个包名,导致"已装的包也报缺"。 - 探针 argv 里不得出现 shell 元字符(round-7 实测):真实宿主的 subprocess 通路会拒绝含
|(以及%)的 argv——表现为一次空 spawn(exit=null),而不是报错。因此所有探测/许可证代码都改成不含|/%的形式(每包一行名称 ok|missing),解析器两种形态都接受(名称:状态与名称 状态)。新增/修改任何探针模板时都要保持这条。 - argv 模板的占位符替换是"字符串级"的(审查实测):
<script>/<expr>会在任意位置被替换(String(a).split('<script>').join(abs)),因此像 matlab 的run('<script>')这种内嵌写法也能正确展开;此前用的是整元素相等判断,matlab 实际收到的是字面量run('<script>'),脚本从未执行且不报错。共享套件现在遍历整张描述符表断言"任何 spawn 都不得带未替换的占位符",parity 守卫断言"描述符不得使用未知占位符、且每个用到的占位符都有实现"。 - 残余不确定性(如实记录):r 与 wolfram 的 run argv 尚未在夹具中被逐字观测(这两个引擎的 run 在该夹具下被拒绝),它们的 argv 依赖同一段字符串级替换代码;若要逐字证据,需在装有该引擎的机器上各跑一次
mode:'code'(或扩展夹具让其 run 通过)。 - 响应必须永远可 JSON 序列化(round-7 回归):诊断对象曾被写成自引用(
retryDiag指向父对象),导致JSON.stringify(response)抛Converting circular structure to JSON,调用方看不到任何结果。现在诊断经jsonSafeDiag()投影为纯字段(argv/exit/timedOut/ms/spawned/stderrTail/attempts/retried/retryDiag),共享套件有一条横切不变量:probe/run/receipt/install的 13 种响应(成功与失败)都必须能JSON.stringify。新增任何响应字段都要过这条。 - 探测的 cwd 必须"一定存在"(round-7 真实根因):版本探测/包预检/许可探测曾用
projectRoot()作 cwd;全新会话里该目录还不存在,而宿主对"cwd 不存在"的 spawn 返回spawned:true+exit:null(重试无用——同 cwd 同结果),于是新会话第一次probe会得到"引擎存在但不可用"。现在这三类探测统一走probeCwd(H):优先宿主可选字段probeCwd,否则 OS 临时目录(TEMP/TMPDIR/TMP),最后才回退到项目根;真正的run仍用项目根(run 会先写回执,写盘本身会创建目录树,所以它一直是好的)。
能强制:入参 schema(闭合;未知键拒绝)、词法级项目内路径守卫(见下)、cwd、超时并终止进程、输出上限、回执落盘、插件自身的写域(只写 Computation/)、安装两步与作用域默认、同一 archive id 的串行分配(并发运行不会共用 attempt 目录)。
不能强制(如实声明,勿当成承诺):
- 路径守卫是字符串级的:
projectRel只做词法规范化(拒绝绝对路径、盘符、..越界),不做 realpath、不解析符号链接/junction/硬链接。项目内一个指向外部的链接可以绕过该守卫拿到mode:'file'的读取与执行;真正的容器化由宿主ctx.fs后端负责(resolve/contains),插件不重复实现。审计 A 的用例把这一限制钉成了断言(tests/math-computation-shared.test.mjs§16c)。 - 脚本内部的网络访问与文件访问——宿主
subprocess.spawn没有 policy 槽(env层只做凭据/DSH_*清洗与显式合并,不能限制网络或写盘;参见dsh-subprocess-local的runner-launch-*.js与vibe-math-v5.js中 audit M8 注释),任何被执行的计算进程都在宿主 fs 策略之外; mathMode只是提示词策略:模型仍可能直接调用宿主 shell,插件无法阻止,也无法为 shell 路径生成回执。因此默认策略要求:走 shell 得出的结论必须标注"未经工具归档(shell 路径)",只有工具路径的计算才算可复核支撑材料;- 替代必须声明(提示词规则,同样不是强制):当替代方案改变了精确性或结论强度(精确符号解 → 数值近似、闭式解 → 采样/求积、改了精度/容差/假设、换了算法类),结论里必须写明,不得读起来像得到了原本(精确/所要求的)结果;拿不到精确结果就直说。规则行由
MATH_SUBSTITUTION_RULE_LINE/_EN提供(已并入MATH_RULE_LINES/_EN与四套 persona 两个文本块),但能否照做取决于模型——插件只能要求,不能验证。 - 商业引擎的 CLI 模板随版本变化(Maple 尤甚):描述符带
VERIFY标记,每次运行都回显实际 argv;若引擎以用法/选项错退出,返回MATH_ENGINE_BAD_ARGV+next.kind='engine-override'(指向mathEngineOverride覆盖模板),而不是裸MATH_NONZERO_EXIT。有许可的机器应验证这三个模板。 - 真强制(禁网/限权)需要宿主 sandbox/policy 支持——列为待上游需求,不在本轮范围。
验证边界(诚实声明,2026-10-01 更新):
- 已在真机上验证过的(本机,真实引擎、真实子进程、真实回执):python 3.12.10(用户级安装,镜像二进制 SHA256 校验通过;numpy/pandas/sympy/scipy/matplotlib/mpmath 经 TUNA PyPI 安装)与 R 4.6.1(CRAN 镜像;
⚠️ 机器级安装、不在 PATH、该镜像未发布校验和 ⇒ 记为未验证校验和)。- 四个预设都能挂载并暴露同一个 15 参数
math_computation;四个预设在同一真实引擎上给出相同数字(det=-2、sum=1.6439345666815597、integrate(x²,0,1)=1/3、simplify(sqrt(8))=2*sqrt(2);R 侧[1] -2、[1] 2/3)。 - native 与
cli逃生口产出完全相同的数字与相同的scriptHash(仅元数据不同)。 - 边界码在真机上确认:语法错误 ⇒
MATH_NONZERO_EXIT(带 stderr + 回执,不是MATH_INVALID_ARGUMENT);超时 ⇒MATH_TIMEOUT(进程被终止);R 警告 ⇒exit=0+ stderr + 回执;缺包 ⇒MATH_MISSING_PACKAGES且不产生回执目录;numpy>=1只按存在性接受;独立 shell 重跑与回执里的 stdout 逐字一致。
- 四个预设都能挂载并暴露同一个 15 参数
- 仍未验证的:Octave / Julia 未安装(本机没有
winget,scoop 的依赖拉取被raw.githubusercontent阻断);MATLAB / Maple / Wolfram 的商业模板仍标VERIFY(需许可机器确认,mathEngineOverride可覆盖模板);插件无法强制禁网/限权(宿主subprocess.spawn没有 policy 槽,需宿主 sandbox)。 - 引擎发现顺序与"装了却不在 PATH"的处理见 §5.4;假 subprocess seam(
tests/helpers/math-computation-fake-seam.mjs)仍是回归主力,但已不再是唯一的执行证据。
并发下的完整性:同一 archive id 的分配与"占用"(receipt.json 落盘)在一个插件实例内是串行的,因此两个并发同 id 运行会分别拿到 attempt 1 / attempt 2,绝不会共用目录。跨进程并发仍依赖宿主的独占创建原语(当前 fs 接口没有暴露,列为已知限制)。
父目录由宿主创建(round-3 记录,勿再当缺陷):宿主的文本写会自动创建缺失的父目录——dsh-fs-local/lib/index.js 的 writeFileAtomic 在落盘前执行 mkdir(dirname(absolutePath), { recursive: true })。因此 Computation/<runId>/、Computation/<runId>/attempts/<n>/ 以及异步作业镜像 Formal/Jobs/ 不需要预先存在;ensureDirs() 仍然列出它们只是为了空项目里的可发现性(v2/v3/v4/v5 现已都含 Formal/Jobs 与 Computation)。真机上"严格 fs 拒绝创建父目录"的实验与真实宿主行为矛盾,不能作为失败证据。
模型本来就有宿主 shell 可用(默认 mathMode='typed+shell' 就允许把它当兜底),所以 cli 不扩大爆炸半径;区别在于 cli 仍在工具内:照样有 cwd、超时、输出上限、路径守卫与回执(engine 记为 cli:<command>)。要收紧:把 mathMode 设为 typed,或从 mathEngines 去掉 'cli'——两种情况下 engine:'cli' 都返回 MATH_REFUSED。
math_computation 产出经验证据(回执可复核);形式化证明仍是可选的 Lean 路径(formalVerify: off|encourage|require,默认 off)。计算回执不能顶替 Lean 的 passed/blocked,也不能把对象标为"已验证"。
vibe_math_set_params(v2/v3)、vibe_v4_set(v4)、vibe_v5_set(v5)以及各自的控制面:v2/v3 是 vibe_math_setup(交互式 schema)/vibe_math_status/vibe_math_template/vibe_math_save_settings,v4 是 vibe_v4_configure/vibe_v4_status(没有 template/setup),v5 是 vibe_v5_configure/vibe_v5_status(同样没有 template/setup);六个参数名在四套、README 双语参数表与本文中拼写一致(由 tests/audit-math-computation-parity.mjs 与 audit-math-computation-contract.mjs 盯着)。
- 安装失败的判别(不新增失败码):
install失败用op:'install'+installedSoFar(已成功的命令 argv)+timedOut区分,不新增失败码(MATH_FAILURE_CODES属冻结接口,扩展需单独的接口变更批次);成功路径同样给出installedSoFar,调用方无需特判。