dsh-vibe-math 2.7.1 → 2.7.2

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 (47) hide show
  1. package/README.en.md +681 -690
  2. package/README.md +8 -17
  3. package/cordis.patch.yml +6 -6
  4. package/docs/AUDIT-CHECKLIST.md +518 -518
  5. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  6. package/docs/final-paper.md +172 -172
  7. package/docs/formal-verification.md +588 -588
  8. package/docs/math-computation.md +187 -163
  9. package/docs/release-notes/README.en.md +48 -0
  10. package/docs/release-notes/README.md +47 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  13. package/docs/release-notes/TEMPLATE.md +120 -0
  14. package/docs/test-timing.md +154 -154
  15. package/package.json +208 -203
  16. package/prompt-corpus-persona/persona-corpus.json +8 -8
  17. package/prompt-corpus-persona/persona-corpus.md +12 -12
  18. package/prompt-corpus-v2/formal-verify-v2.json +121 -109
  19. package/prompt-corpus-v2/formal-verify-v2.md +223 -107
  20. package/prompt-corpus-v3/formal-verify-v3.json +100 -76
  21. package/prompt-corpus-v3/formal-verify-v3.md +268 -70
  22. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  23. package/prompt-corpus-v4/formal-verify-v4.md +20 -20
  24. package/prompt-corpus-v5/prompt-corpus-v5.json +81 -81
  25. package/prompt-corpus-v5/prompt-corpus-v5.md +138 -128
  26. package/tests/audit-math-computation-contract.mjs +43 -1
  27. package/tests/audit-math-computation-sensitivity.mjs +2 -2
  28. package/tests/audit-v5-sensitivity.mjs +3 -1
  29. package/tests/e2e-v5-round2.test.mjs +114 -5
  30. package/tests/formal-verify-v2.test.mjs +38 -0
  31. package/tests/formal-verify-v3.test.mjs +33 -0
  32. package/tests/formal-verify-v4.test.mjs +87 -0
  33. package/tests/math-computation-shared.test.mjs +126 -1
  34. package/vibe-math-v2/agent.cordis.yml +2 -2
  35. package/vibe-math-v2/math-computation.js +122 -11
  36. package/vibe-math-v2/vibe-math-v2.js +75 -11
  37. package/vibe-math-v3/agent.cordis.yml +2 -2
  38. package/vibe-math-v3/math-computation.js +122 -11
  39. package/vibe-math-v3/vibe-math-v3.js +60 -13
  40. package/vibe-math-v4/agent.cordis.yml +4 -4
  41. package/vibe-math-v4/math-computation.js +122 -11
  42. package/vibe-math-v4/vibe-math-v4.js +55 -15
  43. package/vibe-math-v5/agent.cordis.yml +4 -4
  44. package/vibe-math-v5/math-computation.js +122 -11
  45. package/vibe-math-v5/vibe-math-v5.js +137 -28
  46. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1660 -1655
  47. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -72,11 +72,12 @@ export const MATH_PERSONA_TOOL_LINE = '- math_computation {op: probe|run|receipt
72
72
  // P2a: the archive -> edit -> re-run workflow. Injected into every preset's rule block (and appended
73
73
  // to both persona blocks) so an agent cannot miss that (a) the script is archived, (b) it may edit
74
74
  // it, and (c) re-running is what produces evidence for the edited code.
75
- export const MATH_ARCHIVE_WORKFLOW_LINE = '- 归档→编辑→重跑:mode:\'code\' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>),你可以用普通文件工具打开并编辑它;'
75
+ export const MATH_ARCHIVE_WORKFLOW_LINE = '- 归档→编辑→重跑:mode:\'code\' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);'
76
+ + '**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);'
76
77
  + '编辑后用 mode:\'file\' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——'
77
78
  + '报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。'
78
79
 
79
- 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>) and you may open/edit it with your normal file tools; 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.'
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.'
80
81
 
81
82
  // Round-6 (A): honesty about SUBSTITUTIONS. An alternative that weakens exactness or conclusion
82
83
  // strength must be declared, and the conclusion must never read as if the requested (exact) result
@@ -288,14 +289,39 @@ function fail(code, engine, message, extra) {
288
289
  // round-7 (live-session fix): the probe diagnostic must survive into the response, otherwise
289
290
  // "存在但不可用" is opaque (a real session showed exactly that).
290
291
  if (extra && extra.probe) out.probe = jsonSafeDiag(extra.probe)
292
+ // round-9 (F7): the version-constraint policy travels with failures too (no silent drop).
293
+ if (extra && extra.versionPolicy) out.versionPolicy = extra.versionPolicy
294
+ if (extra && extra.constraintsNotEnforced) out.constraintsNotEnforced = extra.constraintsNotEnforced
291
295
  return out
292
296
  }
293
297
 
294
- function userInstallNext(engine) {
298
+ // round-9 (real-engine finding): the suggested install command may not be runnable on this machine
299
+ // (e.g. `winget` is absent), and handing a user a command that cannot run is misleading. So the
300
+ // command is emitted ONLY when its package manager is actually resolvable; otherwise we return the
301
+ // official vendor/download pointer plus an explicit note. Async because resolution goes through the host.
302
+ async function userInstallNext(H, engine) {
295
303
  const d = MATH_ENGINES[engine] || {}
296
304
  const per = d.userInstall || {}
297
305
  const platform = process && process.platform === 'win32' ? 'windows' : (process && process.platform === 'darwin' ? 'macos' : 'linux')
298
- return next('user-install', { engine: engine, perOs: per, command: per[platform] || '', platform: platform })
306
+ const raw = per[platform] || ''
307
+ const mgr = (function () { const m = /^\s*(winget|brew|apt-get|apt|choco|scoop|dnf|pacman|zypper)\b/.exec(String(raw)); return m ? m[1] : null })()
308
+ let available = true
309
+ if (mgr) {
310
+ try { available = !!(await H.resolveExecutable(mgr)) } catch (e) { available = false }
311
+ }
312
+ return next('user-install', {
313
+ engine: engine,
314
+ perOs: per,
315
+ command: (mgr && !available) ? '' : raw,
316
+ suggestedCommand: raw,
317
+ platform: platform,
318
+ packageManager: mgr,
319
+ packageManagerAvailable: mgr ? available : null,
320
+ vendorUrl: d.vendor || null,
321
+ note: (mgr && !available)
322
+ ? ('本机没有检测到包管理器 ' + mgr + ':建议命令无法运行,请按上面的官方地址手动安装(或先自行安装一个包管理器)。')
323
+ : null,
324
+ })
299
325
  }
300
326
 
301
327
  // ── host adaptation ─────────────────────────────────────────────────────────────────────────────
@@ -353,6 +379,42 @@ export async function probeMathEngines(host, opts) {
353
379
  return result
354
380
  }
355
381
 
382
+ // round-9 (real-engine finding): an engine can be INSTALLED yet invisible, because discovery only
383
+ // 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
+ // added to PATH). Add the known per-OS INSTALL locations, globbed by version, still AFTER PATH and
385
+ // after the bundled runtime; existence is always proven by LISTING, never assumed.
386
+ async function knownInstallCandidates(H, engineName) {
387
+ if (typeof H.listDirAbs !== 'function') return []
388
+ const cands = mathEngineCandidates(engineName)
389
+ if (!cands) return []
390
+ const env = (typeof process !== 'undefined' && process.env) || {}
391
+ const win = !!(typeof process !== 'undefined' && process.platform === 'win32')
392
+ const exts = win ? ['', '.exe', '.cmd'] : ['']
393
+ 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 [] } }
396
+ const match = async (dir) => {
397
+ const files = await listFiles(dir)
398
+ for (const c of cands) for (const x of exts) if (files.indexOf(c + x) !== -1) out.push(dir + '/' + c + x)
399
+ }
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
408
+ }
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
+ }
415
+ return Array.from(new Set(out))
416
+ }
417
+
356
418
  async function resolveCandidate(H, d) {
357
419
  const cands = mathEngineCandidates(d.name)
358
420
  if (!cands) return null
@@ -362,11 +424,15 @@ async function resolveCandidate(H, d) {
362
424
  if (typeof p === 'string' && p) return p
363
425
  } catch (e) { /* not this one */ }
364
426
  }
365
- // round-7 (fix 2): DSH ships its own runtimes; PATH always wins, so this is the LAST resort. The
427
+ // round-7 (fix 2): DSH ships its own runtimes; PATH always wins, so this is a LAST resort. The
366
428
  // tree name is globbed (never hard-coded) and executable names come from the descriptor candidates.
367
429
  for (const p of await dshRuntimeCandidates(H, d.name)) {
368
430
  if (typeof p === 'string' && p) return p
369
431
  }
432
+ // round-9: then the known per-OS install locations (an engine installed without touching PATH).
433
+ for (const p of await knownInstallCandidates(H, d.name)) {
434
+ if (typeof p === 'string' && p) return p
435
+ }
370
436
  return null
371
437
  }
372
438
 
@@ -513,7 +579,7 @@ async function resolveEngine(H, requested, params, args) {
513
579
  if (params.mathMode !== 'typed+shell') return fail('MATH_REFUSED', name, 'cli 被策略禁用(mathMode=' + params.mathMode + ')', { next: next('reason', { reason: 'policy' }) })
514
580
  let exe = null
515
581
  try { exe = await H.resolveExecutable(args.cli.command) } catch (e) { exe = null }
516
- if (!exe) return fail('MATH_ENGINE_NOT_FOUND', name, 'cli 命令无法解析:' + args.cli.command, { next: userInstallNext('cli') })
582
+ if (!exe) return fail('MATH_ENGINE_NOT_FOUND', name, 'cli 命令无法解析:' + args.cli.command, { next: await userInstallNext(H, 'cli') })
517
583
  // round-7 (finding 5): report the REAL version when the command's family is recognisable (the
518
584
  // descriptor for that family carries the version probe); unknown commands stay 'unknown'.
519
585
  let cliVersion = 'unknown'
@@ -548,7 +614,7 @@ async function resolveEngine(H, requested, params, args) {
548
614
  return { ok: true, name: name, desc: d, exe: hit, version: v.version }
549
615
  }
550
616
  const first = requested === 'auto' ? (params.mathEngines[0] || 'python') : requested
551
- return fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(试过:' + names.join(', ') + ')', { next: userInstallNext(first) })
617
+ return fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(试过:' + names.join(', ') + ')', { next: await userInstallNext(H, first) })
552
618
  }
553
619
 
554
620
  // ── package probe ───────────────────────────────────────────────────────────────────────────────
@@ -810,7 +876,7 @@ async function opProbe(H, args, params) {
810
876
  }
811
877
  if (!probe.available && !(chosen && chosen.ok)) {
812
878
  const first = requested || params.mathEngines[0] || 'python'
813
- const out = fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(请求:' + first + ')', { next: userInstallNext(first), engines: [], available: false, packages: packages })
879
+ const out = fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(请求:' + first + ')', { next: await userInstallNext(H, first), engines: [], available: false, packages: packages })
814
880
  out.op = 'probe'
815
881
  return out
816
882
  }
@@ -821,10 +887,22 @@ async function opProbe(H, args, params) {
821
887
  engines.push({ name: chosen.name, path: chosen.exe, version: chosen.version, license: (MATH_ENGINES[chosen.name] || {}).license })
822
888
  }
823
889
  const engineInfo = (chosen && chosen.ok) ? { name: chosen.name, path: chosen.exe, version: chosen.version } : null
890
+ // round-9 (F3): the availability line must never be silently partial - name the CONFIGURED engines
891
+ // that were not found (and why), instead of just listing the ones that happened to resolve.
892
+ const foundNames = engines.map((e) => e.name)
893
+ // Per-engine absence explanation. The key is `why` (NOT the reserved machine-readable vocabulary
894
+ // 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
+ })
824
899
  return {
825
900
  ok: true, op: 'probe', engine: engineInfo ? engineInfo.name : null, engineInfo: engineInfo,
826
901
  engines: engines, available: true, packages: packages,
827
- message: '可用引擎:' + engines.map((e) => e.name + ' ' + e.version).join('、') + (engineInfo ? '(请求 ' + engineInfo.name + ':' + engineInfo.version + ')' : ''),
902
+ configured: params.mathEngines.slice(), absent: absent,
903
+ message: '可用引擎:' + engines.map((e) => e.name + ' ' + e.version).join('、')
904
+ + (engineInfo ? '(请求 ' + engineInfo.name + ':' + engineInfo.version + ')' : '')
905
+ + (absent.length ? ';已配置但本机未发现:' + absent.map((a) => a.engine).join('、') : ';已配置的引擎都在'),
828
906
  }
829
907
  }
830
908
 
@@ -851,6 +929,9 @@ async function opRun(H, args, params) {
851
929
  const engineLabel = det.name === 'cli' ? ('cli:' + cliCommand) : det.name
852
930
 
853
931
  const want = (args.packages && args.packages.length) ? args.packages : params.mathPackages
932
+ // round-9 (F7): version constraints are NOT enforced - we only check existence by base name.
933
+ const pinnedSpecs = want.filter((sp) => parsePackageSpec(sp).pinned)
934
+ const versionPolicy = 'version-constraints-are-existence-only: 只按 base name 检查是否存在,约束本身从不校验(版本求解交给包管理器)' + (pinnedSpecs.length ? ';本次未校验的约束:' + pinnedSpecs.join(', ') : ';本次没有版本约束')
854
935
  // round-7 (finding 3): for cli the precheck runs against the FAMILY of `cli.command` (same
855
936
  // discovery-shaped logic); an unrecognisable command SKIPS the precheck instead of reporting a
856
937
  // false "missing" that would block the escape hatch.
@@ -865,6 +946,7 @@ async function opRun(H, args, params) {
865
946
  missing: missing,
866
947
  next: next('agent-install', { engine: det.name, packages: missing, specs: want.slice(), dryRun: true }),
867
948
  packages: pk,
949
+ versionPolicy: versionPolicy, constraintsNotEnforced: pinnedSpecs.slice(),
868
950
  })
869
951
  }
870
952
 
@@ -872,10 +954,17 @@ async function opRun(H, args, params) {
872
954
  let scriptText = ''
873
955
  let fileRel = null
874
956
  let sourceHashBefore = null
957
+ // round-9 (N1): `mode:'file'` is keyed by the SOURCE PATH, so re-running an ORIGINAL source lands on
958
+ // the same archive id (attempt>=2, scriptChanged). Pointing it at an ARCHIVED script instead targets
959
+ // a different path and therefore a NEW archive id by design - the prior attempt is never touched, but
960
+ // there is no previous receipt FOR THAT ID, so scriptChanged/previousReceipt are necessarily empty.
961
+ // That is not a bug, but it must be SAID rather than looking like broken change detection.
962
+ let archivedScriptSource = false
875
963
  if (mode === 'code') scriptText = args.code
876
964
  else if (mode === 'file') {
877
965
  fileRel = projectRel('', args.file)
878
966
  if (fileRel === null) return fail('MATH_REFUSED', det.name, 'file 必须位于项目内:' + args.file, { next: next('reason', { reason: 'path-outside-project' }) })
967
+ archivedScriptSource = /^Computation\//.test(String(fileRel))
879
968
  const txt = await H.readText(fileRel)
880
969
  if (txt === undefined) return fail('MATH_REFUSED', det.name, '找不到文件:' + args.file, { next: next('reason', { reason: 'file-not-found' }) })
881
970
  scriptText = txt
@@ -939,6 +1028,7 @@ async function opRun(H, args, params) {
939
1028
  }
940
1029
 
941
1030
  const warnings = []
1031
+ if (archivedScriptSource) warnings.push(warning('ARCHIVED_SCRIPT_RERUN', 'mode:\'file\' 指向的是**归档脚本**(Computation/…):按设计归档 id 以**源路径**为键,所以这是一次**新归档**(新目录、attempt 1),没有该 id 的历史回执 ⇒ scriptChanged/previousReceipt 为空是**预期**,且**旧 attempt 绝不会被覆盖**。要做"编辑→重跑→attempt≥2 + scriptChanged"的核对,请把 mode:\'file\' 指向**原始源文件**并编辑它,而不是指向归档副本。'))
942
1032
  if (!probeDet && want.length) warnings.push(warning('PACKAGE_PRECHECK_SKIPPED', 'engine=cli:无法从命令名判断引擎族,已跳过包预检(不阻塞执行,也不会误报缺包);如需预检请直接用 python/r/octave/julia 或改用可识别的解释器路径。'))
943
1033
  if (pk && pk.probeFailed && want.length) warnings.push(warning('PACKAGE_PRECHECK_UNKNOWN', '包预检未能执行(宿主 subprocess 在这几次尝试里返回空/超时,会话早期常见):本次不判定缺包、不阻塞执行;如需确定性结论请重试或直接用对应引擎。probe=' + JSON.stringify(pk.argv) + ' attempts=' + JSON.stringify(pk.attempts)))
944
1034
  if (fullOut.length > MATH_CAPS.stdout) warnings.push(warning('OUTPUT_TRUNCATED', 'stdout 超过 64KB,完整版见 ' + dir + '/stdout.txt'))
@@ -953,7 +1043,15 @@ async function opRun(H, args, params) {
953
1043
  engine: { name: engineLabel, path: det.exe, version: det.version, source: det.name === 'cli' ? 'cli' : 'path' },
954
1044
  script: { path: scriptRel, sha256: scriptHash, bytes: Buffer.byteLength(scriptText, 'utf8') },
955
1045
  scriptPath: scriptRel, scriptHash: scriptHash,
1046
+ // round-8 (P0/D3): member file tools are session-cwd relative, so the ABSOLUTE locations must be
1047
+ // in the receipt: `scriptAbs` is directly openable, `cwd` is the project root to join with.
1048
+ scriptAbs: scriptAbs, cwd: root,
956
1049
  sourceFile: fileRel || null,
1050
+ fileIsArchivedScript: archivedScriptSource,
1051
+ // round-9 (N2): a TIMEOUT must still carry the evidence it produced - the capped partial output
1052
+ // lives in the receipt as well as in stdout.txt/stderr.txt on disk.
1053
+ partialStdout: String(fullOut).slice(-2000),
1054
+ partialStderr: String(fullErr).slice(-2000),
957
1055
  sourceHashBefore: sourceHashBefore, sourceHashAfter: sourceHashAfter,
958
1056
  scriptChanged: scriptChanged, scriptChangedDuringRun: scriptChangedDuringRun,
959
1057
  previousReceipt: prevReceipt ? { runId: prevReceipt.runId, attempt: prevReceipt.attempt || 1, scriptHash: prevReceipt.scriptHash || (prevReceipt.script && prevReceipt.script.sha256) || null } : null,
@@ -979,12 +1077,20 @@ async function opRun(H, args, params) {
979
1077
 
980
1078
  const shell = {
981
1079
  ok: true, op: 'run', engine: engineLabel, mode: mode,
1080
+ runId: runId, attempt: attempt,
1081
+ versionPolicy: versionPolicy, constraintsNotEnforced: pinnedSpecs.slice(),
982
1082
  engineInfo: receipt.engine,
983
1083
  argv: assembled.argv.slice(),
984
1084
  scriptPath: scriptRel, scriptHash: scriptHash,
1085
+ scriptAbs: scriptAbs, cwd: root,
985
1086
  attempt: attempt, attemptDir: dir, baseRunDir: baseDir,
986
1087
  scriptChanged: scriptChanged, scriptChangedDuringRun: scriptChangedDuringRun,
987
1088
  sourceFile: fileRel || null,
1089
+ fileIsArchivedScript: archivedScriptSource,
1090
+ // round-9 (N2): a TIMEOUT must still carry the evidence it produced - the capped partial output
1091
+ // lives in the receipt as well as in stdout.txt/stderr.txt on disk.
1092
+ partialStdout: String(fullOut).slice(-2000),
1093
+ partialStderr: String(fullErr).slice(-2000),
988
1094
  previousReceipt: receipt.previousReceipt,
989
1095
  cliScriptAppended: !!(receipt.cli && receipt.cli.scriptAppended),
990
1096
  packages: pk.found,
@@ -994,8 +1100,13 @@ async function opRun(H, args, params) {
994
1100
  warnings: warnings,
995
1101
  }
996
1102
  if (receipt.timedOut) {
997
- return Object.assign(fail('MATH_TIMEOUT', engineLabel, '执行超时(' + timeoutMs + 'ms)已被终止', { argv: assembled.argv.slice(), receipt: receiptRef }),
998
- archiveFields(), { ms: receipt.ms, timedOut: true })
1103
+ // round-9 (F5): failure EVIDENCE must be uniform - a timeout carries the SAME fields a non-zero
1104
+ // exit does (exit + stderr), plus timedOut:true.
1105
+ const out = fail('MATH_TIMEOUT', engineLabel, '执行超时(' + timeoutMs + 'ms)已被终止', { argv: assembled.argv.slice(), receipt: receiptRef })
1106
+ out.exit = null
1107
+ out.stdout = String(fullOut).slice(0, MATH_CAPS.stdout)
1108
+ out.stderr = String(fullErr).slice(0, MATH_CAPS.stderr)
1109
+ return Object.assign(out, archiveFields(), { ms: receipt.ms, timedOut: true })
999
1110
  }
1000
1111
  if (r.exit !== 0) {
1001
1112
  const blob = fullErr + '\n' + fullOut
@@ -918,6 +918,7 @@ export function apply(ctx) {
918
918
  // 引理只在摘要给"索引"(id+标题),完整叙述在各方向文件 Progress/<qid>/<dirId>.md(由代理或调度器写入)
919
919
  if (d.lemmas && d.lemmas.length) { lines.push('**引理索引**:' + d.lemmas.map(function (l) { return '「' + l.title + '」(' + l.id + ')' }).join(';')) }
920
920
  lines.push('**完整叙述**:见 `Progress/' + qid + '/' + d.id + '.md`')
921
+ lines.push(PAPER_PATH_NOTE)
921
922
  for (const j of (d.journal || [])) {
922
923
  lines.push('')
923
924
  lines.push('### 第 ' + j.round + ' 轮|' + (j.agent || '') + '|' + (j.at || ''))
@@ -1309,7 +1310,22 @@ export function apply(ctx) {
1309
1310
  // 并发计数由 agentRegistry 推导,无需手工 +1。
1310
1311
  await saveAll()
1311
1312
  }
1312
- async function interruptChild(childId) { try { subagents.interrupt(childId, { kind: 'ancestor', agent: rootAgent }) } catch (e) {} }
1313
+ /**
1314
+ * 中断一个**在册**子代理。审计 D1:旧实现 `try{…}catch(e){}` 把"没有这个 child / 宿主拒绝中断"
1315
+ * 都吞成静默成功(调用方拿到 undefined,还以为已经中断)。现在:空/未知 id ⇒ `code` +
1316
+ * `next{tool,hint}`,宿主抛错 ⇒ 保留错误文本并给替代出口;只有真的发出中断才 `ok:true`。
1317
+ */
1318
+ async function interruptChild(childId) {
1319
+ const id = String(childId == null ? '' : childId).trim()
1320
+ if (!id) return { ok: false, code: 'VIBE_MATH_INVALID_ARGUMENT', message: 'interruptChild 失败:childId 为空——无法确定要中断哪个子代理。', next: { kind: 'reason', tool: 'vibe_math_list_agents', hint: '用 vibe_math_list_agents 列出本会话在册子代理的 id,再带 childId 调用。' } }
1321
+ if (!Object.prototype.hasOwnProperty.call(agentRegistry, id)) return { ok: false, code: 'VIBE_MATH_CHILD_NOT_FOUND', message: 'interruptChild 失败:' + id + ' 不在本会话的在册子代理里(在册 ' + Object.keys(agentRegistry).length + ' 个)——可能已经结束,或不是本会话的子代理。', next: { kind: 'reason', tool: 'vibe_math_list_agents', hint: '用 vibe_math_list_agents 取当前在册 id;要停整个会话用 vibe_math_abort。' } }
1322
+ try {
1323
+ await subagents.interrupt(id, { kind: 'ancestor', agent: rootAgent })
1324
+ return { ok: true, childId: id }
1325
+ } catch (e) {
1326
+ return { ok: false, code: 'VIBE_MATH_INTERRUPT_FAILED', message: 'interruptChild 失败:宿主拒绝中断 ' + id + '(' + String((e && e.message) || e) + ')。', next: { kind: 'reason', tool: 'vibe_math_abort', hint: '宿主不接受单点中断时,用 vibe_math_abort 终止整个会话的子代理。' } }
1327
+ }
1328
+ }
1313
1329
 
1314
1330
  // ================= prompts =================
1315
1331
  function personaText(key) { return params[key] ? (String(params[key]) + '\n\n') : '' }
@@ -1625,7 +1641,10 @@ export function apply(ctx) {
1625
1641
  }
1626
1642
  return {}
1627
1643
  }
1628
- async function resolveDecision(id, resolution) { const d = decisionQueue.find(function (x) { return x.id === id }); if (!d) return { ok: false, message: 'decision not found' }; if (d.status !== 'pending') return { ok: false, message: 'decision already resolved' }; d.status = 'resolved'; d.resolution = resolution; if (scheduler.gate && scheduler.gate.decisionId === id) scheduler.gate = null; logActivity('decide', id + ' resolved: ' + resolution.action + (resolution.verdict !== undefined ? ' ' + resolution.verdict : '')); await saveAll(); scheduleTick(); return { ok: true, message: 'decision resolved' } }
1644
+ async function resolveDecision(id, resolution) {
1645
+ const d = decisionQueue.find(function (x) { return x.id === id })
1646
+ if (!d) return { ok: false, code: 'VIBE_MATH_DECISION_NOT_FOUND', message: 'decision not found:' + String(id == null ? '' : id) + ' 不在人工决策队列里(队列现有 ' + decisionQueue.length + ' 个' + (decisionQueue.length ? (':' + decisionQueue.map(function (x) { return x.id }).join(', ')) : '') + ')——可能已被 vibe_math_decide 结清,或被 start/resume/abort/换项目作废(作废是终态,不会回到 pending)。', next: { kind: 'reason', tool: 'vibe_math_list_decisions', hint: '先用 vibe_math_list_decisions 取当前 pending 的 id;若期望它还在,检查是否发生过 start/resume/abort/换项目。' } }
1647
+ if (d.status !== 'pending') return { ok: false, code: 'VIBE_MATH_DECISION_ALREADY_RESOLVED', message: 'decision already resolved:' + String(id) + ' 当前状态是 ' + String(d.status) + '——每次决策只生效一次。', next: { kind: 'reason', tool: 'vibe_math_list_decisions', hint: '用 vibe_math_list_decisions 看还有哪些 pending;要重新决策请等框架发起新的决策项。' } }; d.status = 'resolved'; d.resolution = resolution; if (scheduler.gate && scheduler.gate.decisionId === id) scheduler.gate = null; logActivity('decide', id + ' resolved: ' + resolution.action + (resolution.verdict !== undefined ? ' ' + resolution.verdict : '')); await saveAll(); scheduleTick(); return { ok: true, message: 'decision resolved' } }
1629
1648
 
1630
1649
  // ================= scheduler core =================
1631
1650
  function scheduleTick() { tick().catch(function (e) { console.error('vibe-math-v3 tick error: ' + String((e && e.stack) || e)) }) }
@@ -1877,7 +1896,7 @@ export function apply(ctx) {
1877
1896
  return true
1878
1897
  }
1879
1898
  async function backfillVerifiers(t) {
1880
- while (t.children.length < t.expectedCount) {
1899
+ while (t.children.length < voteCount(t).expected) {
1881
1900
  if (activeCount() >= params.maxParallelThreshold) break
1882
1901
  const index = t.children.length
1883
1902
  const childId = await spawnChild('verifier:' + t.rId + ':' + index, verifierReviewPrompt(t.r), { role: 'verifier', rId: t.rId, round: 1, index: index })
@@ -1891,7 +1910,7 @@ export function apply(ctx) {
1891
1910
  const t = tasks[ids[i]]
1892
1911
  if (t.type !== 'verify') continue
1893
1912
  if (t.status === 'paused') {
1894
- const allReported = t.children.length > 0 && t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === t.round })
1913
+ const allReported = voteCount(t, t.round).quorum
1895
1914
  if (allReported) { t.status = 'debating'; await advanceVerification(t, t.round); continue }
1896
1915
  }
1897
1916
  if (t.status !== 'spawning') continue
@@ -3756,15 +3775,35 @@ export function apply(ctx) {
3756
3775
  * 下限取 max(2, expectedCount):verifierCount=1 不是"允许单票",只是"至少 2 个独立验证器"
3757
3776
  * (与 createVerifyTask 的 `Math.max(2, ...)` 一致)。
3758
3777
  */
3759
- function minVotes(t) { return Math.max(2, Number(t && t.expectedCount) || 0, Number(params.verifierCount) || 0) }
3760
- function haveEnoughVotes(t) { return Object.keys(t.childResults).length >= minVotes(t) }
3778
+ /**
3779
+ * 票数/参与集的**单一来源**(审计 D4)。一个 verify 任务的参与集与"需要几票"只由**创建时的快照**
3780
+ * `t.expectedCount`(createVerifyTask 写入)与当前 round 决定——**绝不**读实时 `params.verifierCount`
3781
+ * 或实时名册:否则同一个计数会在 quorum 判据 / 裁决聚合 / 任务簿记之间给出不同答案,中途调参会让
3782
+ * 已经在进行中的表决永远凑不够票数而卡死(守卫见 formal-verify-v3 §5c)。
3783
+ */
3784
+ function voteCount(t, round) {
3785
+ const tt = t || {}
3786
+ const expected = Math.max(2, Number(tt.expectedCount) || 0)
3787
+ const r = Number((round === undefined || round === null) ? (tt.round || 1) : round) || 1
3788
+ const participants = Array.isArray(tt.children) ? tt.children.slice() : []
3789
+ const results = tt.childResults || {}
3790
+ const reportedIds = Object.keys(results)
3791
+ const thisRoundIds = participants.filter(function (cid) { const x = results[cid]; return x && Number(x.round) === r })
3792
+ return {
3793
+ round: r, expected: expected, participants: participants,
3794
+ dispatched: participants.length, reported: reportedIds.length, reportedIds: reportedIds,
3795
+ reportedThisRound: thisRoundIds.length,
3796
+ quorum: participants.length > 0 && participants.length >= expected && thisRoundIds.length === participants.length,
3797
+ }
3798
+ }
3799
+ function minVotes(t) { return voteCount(t).expected } // 快照值;**故意**不读实时 params.verifierCount
3800
+ function haveEnoughVotes(t) { const v = voteCount(t); return v.reported >= minVotes(t) } // 单一来源:minVotes 也只读快照
3761
3801
  /** 所有已派出的验证器都报了本轮的票,**且**票数达到下限——否则不得推进/裁决。 */
3762
3802
  function allReportedWithQuorum(t, round) {
3763
- if (!t || !Array.isArray(t.children) || t.children.length === 0) return false
3764
- if (t.children.length < Math.max(2, Number(t.expectedCount) || 0)) return false
3765
- return t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === round })
3803
+ const v = voteCount(t, round)
3804
+ return v.dispatched > 0 && v.dispatched >= v.expected && v.quorum
3766
3805
  }
3767
- function consensus(t) { const vs = Object.keys(t.childResults).map(function (cid) { return t.childResults[cid].Result }); if (vs.length === 0) return false; return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
3806
+ function consensus(t) { const vs = voteCount(t).reportedIds.map(function (cid) { return t.childResults[cid].Result }); if (vs.length === 0) return false; return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
3768
3807
  function buildTranscript(t) { const parts = []; const cids = Object.keys(t.childResults); for (let i = 0; i < cids.length; i++) { const r = t.childResults[cids[i]]; parts.push('Reviewer ' + i + ': Result=' + r.Result + ' Reason=' + r.Reason) } return parts.join('\n') }
3769
3808
  async function handleVerifier(childId, meta, output, stopReason) {
3770
3809
  const rId = meta.rId
@@ -3838,7 +3877,7 @@ export function apply(ctx) {
3838
3877
  }
3839
3878
  // 近共识 + forced/flat 裁决(修复 v2 flat 高置信分歧误判:全部同侧且均值≥0.85/≤0.15 取均值)
3840
3879
  function finalVerdict(t) {
3841
- const rs = Object.keys(t.childResults).map(function (cid) { return t.childResults[cid] })
3880
+ const rs = voteCount(t).reportedIds.map(function (cid) { return t.childResults[cid] })
3842
3881
  if (rs.length === 0) return 0.5
3843
3882
  if (rs.every(function (r) { return r.Result === 1 })) return 1
3844
3883
  if (rs.every(function (r) { return r.Result === 0 })) return 0
@@ -4053,6 +4092,12 @@ export function apply(ctx) {
4053
4092
  /** 证据索引(spec §3.9):只列**确实存在**的文件;排除 Paper/ 自己(避免自我污染)。
4054
4093
  * formalVerify=off 必须真的是无操作(formal-verify-v3 的语料守卫断言"off 的提示词里零 Lean 文本"),
4055
4094
  * 所以 off 档不把形式化路径写进材料。 */
4095
+ /**
4096
+ * 成员可见路径说明(跨预设审计 P0)。成员/子代理的**文件工具按会话 cwd 解析**相对路径,而下面列出的
4097
+ * 路径都是**项目根相对**的 ⇒ 必须显式说明"先拼绝对前缀";计算产物用回执里的绝对字段。一处常量、
4098
+ * 五处材料文本各自 append(绝不替换既有行)。
4099
+ */
4100
+ const PAPER_PATH_NOTE = '(路径说明:成员/子代理的文件工具按**会话 cwd** 解析相对路径,因此上面列出的相对路径都必须先拼上**项目根的绝对前缀**再使用;计算产物请用回执里的绝对字段 `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来。)'
4056
4101
  async function paperEvidenceIndex() {
4057
4102
  const formalNow = formalOn()
4058
4103
  const out = []
@@ -4088,6 +4133,7 @@ export function apply(ctx) {
4088
4133
  const ps = allPropos()
4089
4134
  L.push('')
4090
4135
  L.push('[PROPOSITIONS] (Propos/<分类>/<id>.md)')
4136
+ L.push(PAPER_PATH_NOTE)
4091
4137
  if (ps.length === 0) L.push('- (none)')
4092
4138
  for (let i = 0; i < ps.length; i++) {
4093
4139
  const p = ps[i]
@@ -4098,6 +4144,7 @@ export function apply(ctx) {
4098
4144
  }
4099
4145
  L.push('')
4100
4146
  L.push('[METHODS / ARTEFACTS] (Methods/ + 全局 VibeMath/Methods/)')
4147
+ L.push(PAPER_PATH_NOTE)
4101
4148
  const ms = Array.from(methods.values()).concat(Array.from(globalMethods.values()))
4102
4149
  if (ms.length === 0) L.push('- (none)')
4103
4150
  for (let i = 0; i < ms.length; i++) {
@@ -4696,10 +4743,10 @@ export function apply(ctx) {
4696
4743
  registerTool('vibe_math_set_project', TOOL_DESC.vibe_math_set_project, objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, false) })
4697
4744
  registerTool('vibe_math_list_projects', TOOL_DESC.vibe_math_list_projects, objParams({}), async function () { return { ok: true, current: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects') } })
4698
4745
  registerTool('vibe_math_list_decisions', TOOL_DESC.vibe_math_list_decisions, objParams({}), async function () { return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) } })
4699
- registerTool('vibe_math_decide', TOOL_DESC.vibe_math_decide, objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), async function (args) { const d = decisionQueue.find(function (x) { return x.id === args.id }); if (!d) return { ok: false, message: 'decision not found' }; if (d.status !== 'pending') return { ok: false, message: 'decision already resolved' }; const resolution = { action: args.action, verdict: args.verdict }; const applied = await applyDecision(d.node, d.data, resolution); const r = await resolveDecision(args.id, resolution); return Object.assign({ ok: true, applied: applied }, r) })
4746
+ registerTool('vibe_math_decide', TOOL_DESC.vibe_math_decide, objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), async function (args) { const d = decisionQueue.find(function (x) { return x.id === args.id }); const resolution = { action: args.action, verdict: args.verdict }; if (!d || d.status !== 'pending') return await resolveDecision(args.id, resolution); const applied = await applyDecision(d.node, d.data, resolution); const r = await resolveDecision(args.id, resolution); return Object.assign({ ok: true, applied: applied }, r) })
4700
4747
  registerTool('vibe_math_list_agents', TOOL_DESC.vibe_math_list_agents, objParams({}), async function () { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round, rId: m.rId }) } return { ok: true, agents: out, count: out.length } })
4701
4748
  registerTool('vibe_math_message_agent', TOOL_DESC.vibe_math_message_agent, objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), async function (args) { if (!agentRegistry[args.childId]) return { ok: false, message: 'unknown childId' }; await followupChild(args.childId, args.message); return { ok: true, message: 'message delivered' } })
4702
- registerTool('vibe_math_interrupt_agent', TOOL_DESC.vibe_math_interrupt_agent, objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
4749
+ registerTool('vibe_math_interrupt_agent', TOOL_DESC.vibe_math_interrupt_agent, objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { const r = await interruptChild(args.childId); return Object.assign({}, r, { ok: !!(r && r.ok) }) })
4703
4750
  registerTool('vibe_math_plan', TOOL_DESC.vibe_math_plan, objParams({ force: { type: 'boolean' } }), async function (args) { if (args && args.force && scheduler.running && !scheduler.gate) { await callPlanner(); return { ok: true, message: 'planning triggered', queued: planQueue.length } } return { ok: true, queued: planQueue, lastPlan: lastPlanSummary } })
4704
4751
  registerTool('vibe_math_index', TOOL_DESC.vibe_math_index, objParams({}), async function () { await loadKnowledgeBase(); const r = await rebuildIndex(); return { ok: true, index: r, project: currentProject } })
4705
4752
  registerTool('vibe_math_method_add', TOOL_DESC.vibe_math_method_add, objParams({ id: { type: 'string' }, 标题: { type: 'string' }, 类型: { type: 'string' }, 核心内容: { type: 'string' }, 适用场景: { type: 'string' } }, ['id', '标题']), async function (args) { const id = idSafe(args.id); if (methods.has(id)) return { ok: false, message: 'method id already exists' }; const m = { id: id, 标题: args.标题, 类型: args.类型 || '方法', 状态: '经验', 可信断言: [], 上级体系: [], 子方法: [], 相关: [], 适用场景: args.适用场景 || '', 核心内容: args.核心内容 || '', 定义与记号: '', applications: [], improvements: [], 来源: 'user' }; methods.set(m.id, m); await saveMethod(m, false); await rebuildIndex(); return { ok: true, method: m, file: methodRel(m, false) } })
@@ -73,8 +73,8 @@
73
73
  too; they work in every mode.
74
74
  - vibe_v4_formal_report — human-readable Lean formal-verification mirror (mode, Lean-passed
75
75
  - math_computation {op: probe|run|receipt|install, engine, mode: code|file|expr, …} — 先 probe 预检引擎/包/许可,再调引擎计算并把脚本与输出归档成可复核回执;缺引擎/缺包只报告与给安装指引/计划;shell 兜底不算归档。
76
- - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>),你可以用普通文件工具打开并编辑它;编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
77
- - Archive -> edit -> re-run: for mode:'code' the script original is at the receipt's scriptPath (Computation/<id>/script.<ext>) and you may open/edit it with your normal file tools; 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.
76
+ - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
77
+ - 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.
78
78
  - 替代必须声明(诚实性):当替代方案改变了**精确性或结论强度**时(精确符号解 → 数值近似、闭式解 → 采样/求积、改了精度/容差/假设、换了算法类),结论里**必须写明**,不得读起来像得到了原本(精确/所要求的)结果;拿不到精确结果就直说。
79
79
  - 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.
80
80
  objects, recorded blockers, formalization TODO, library paths).
@@ -185,8 +185,8 @@
185
185
  too; they work in every mode.
186
186
  - vibe_v4_formal_report — human-readable Lean formal-verification mirror (mode, Lean-passed
187
187
  - math_computation {op: probe|run|receipt|install, engine, mode: code|file|expr, …} — 先 probe 预检引擎/包/许可,再调引擎计算并把脚本与输出归档成可复核回执;缺引擎/缺包只报告与给安装指引/计划;shell 兜底不算归档。
188
- - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>),你可以用普通文件工具打开并编辑它;编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
189
- - Archive -> edit -> re-run: for mode:'code' the script original is at the receipt's scriptPath (Computation/<id>/script.<ext>) and you may open/edit it with your normal file tools; 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.
188
+ - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
189
+ - 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.
190
190
  - 替代必须声明(诚实性):当替代方案改变了**精确性或结论强度**时(精确符号解 → 数值近似、闭式解 → 采样/求积、改了精度/容差/假设、换了算法类),结论里**必须写明**,不得读起来像得到了原本(精确/所要求的)结果;拿不到精确结果就直说。
191
191
  - 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.
192
192
  objects, recorded blockers, formalization TODO, library paths).