dsh-vibe-math 2.7.0 → 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 (55) hide show
  1. package/README.en.md +681 -685
  2. package/README.md +9 -13
  3. package/cordis.patch.yml +12 -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 -121
  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.1.en.md +86 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
  13. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  15. package/docs/release-notes/TEMPLATE.md +120 -0
  16. package/docs/test-timing.md +154 -154
  17. package/package.json +208 -201
  18. package/prompt-corpus-persona/persona-corpus.json +8 -8
  19. package/prompt-corpus-persona/persona-corpus.md +24 -12
  20. package/prompt-corpus-v2/formal-verify-v2.json +176 -98
  21. package/prompt-corpus-v2/formal-verify-v2.md +1161 -96
  22. package/prompt-corpus-v3/formal-verify-v3.json +201 -63
  23. package/prompt-corpus-v3/formal-verify-v3.md +1401 -57
  24. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  25. package/prompt-corpus-v4/formal-verify-v4.md +40 -20
  26. package/prompt-corpus-v5/prompt-corpus-v5.json +81 -81
  27. package/prompt-corpus-v5/prompt-corpus-v5.md +199 -128
  28. package/tests/audit-math-computation-contract.mjs +56 -1
  29. package/tests/audit-math-computation-parity.mjs +30 -1
  30. package/tests/audit-math-computation-sensitivity.mjs +2 -2
  31. package/tests/audit-v5-sensitivity.mjs +3 -1
  32. package/tests/e2e-v5-round2.test.mjs +116 -0
  33. package/tests/formal-verify-v2.test.mjs +108 -2
  34. package/tests/formal-verify-v3.test.mjs +117 -2
  35. package/tests/formal-verify-v4.test.mjs +87 -0
  36. package/tests/helpers/math-computation-fake-seam.mjs +30 -2
  37. package/tests/math-computation-shared.test.mjs +430 -3
  38. package/vibe-math-v2/agent.cordis.yml +4 -2
  39. package/vibe-math-v2/math-computation.js +509 -55
  40. package/vibe-math-v2/math-engines.js +47 -6
  41. package/vibe-math-v2/vibe-math-v2.js +92 -12
  42. package/vibe-math-v3/agent.cordis.yml +4 -2
  43. package/vibe-math-v3/math-computation.js +509 -55
  44. package/vibe-math-v3/math-engines.js +47 -6
  45. package/vibe-math-v3/vibe-math-v3.js +77 -14
  46. package/vibe-math-v4/agent.cordis.yml +8 -4
  47. package/vibe-math-v4/math-computation.js +509 -55
  48. package/vibe-math-v4/math-engines.js +47 -6
  49. package/vibe-math-v4/vibe-math-v4.js +70 -15
  50. package/vibe-math-v5/agent.cordis.yml +8 -4
  51. package/vibe-math-v5/math-computation.js +509 -55
  52. package/vibe-math-v5/math-engines.js +47 -6
  53. package/vibe-math-v5/vibe-math-v5.js +152 -31
  54. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1660 -1655
  55. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -116,7 +116,11 @@ console.log('-- math_computation shared contract --')
116
116
  await withHost({ installed: [] }, async (state) => {
117
117
  const r = await state.call({ op: 'probe' })
118
118
  ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'no engine -> ENGINE_NOT_FOUND')
119
- ok(r.next && r.next.kind === 'user-install' && r.next.command, 'ENGINE_NOT_FOUND carries the per-OS user-install guide')
119
+ // round-9: the guide is per-OS and always carries the SUGGESTED command; `command` itself is only
120
+ // populated when that OS's package manager is actually resolvable on this machine.
121
+ ok(r.next && r.next.kind === 'user-install' && r.next.perOs && r.next.perOs.windows && r.next.perOs.linux, 'ENGINE_NOT_FOUND carries the per-OS user-install guide')
122
+ ok(r.next.suggestedCommand === r.next.perOs[r.next.platform], 'the guide keeps the suggested per-OS command alongside the executable one')
123
+ ok(r.next.packageManagerAvailable === false ? r.next.command === '' : typeof r.next.command === 'string', 'command ⇔ package-manager availability (never a command that cannot run)')
120
124
  })
121
125
  await withHost({ packages: { numpy: 'present', sympy: 'missing' } }, async (state) => {
122
126
  const r = await state.call({ op: 'probe', packages: ['numpy', 'sympy'] })
@@ -175,10 +179,15 @@ console.log('-- math_computation shared contract --')
175
179
  await withHost({}, async (state) => {
176
180
  const r = await state.call({ op: 'run', engine: 'cli', mode: 'code', code: 'echo hi', cli: { command: '/fake/bin/mytool', argv: ['--flag', 'x'] } })
177
181
  ok(r.ok === true, 'cli runs with DEFAULT params (default-on)')
178
- eq(r.argv, ['/fake/bin/mytool', '--flag', 'x'], 'cli argv is command + user argv verbatim')
182
+ // round-7 (finding 4): mode:'code' append the archived script so the code actually RUNS (the
183
+ // caller cannot know the runId up front). The user argv stays verbatim in receipt.cli.argv.
184
+ eq(r.argv.slice(0, 3), ['/fake/bin/mytool', '--flag', 'x'], 'cli argv starts with command + user argv')
185
+ ok(/Computation\/.*script\.txt$/.test(String(r.argv[3] || '')), 'mode:code appends the archived script path to the effective argv')
186
+ ok(r.cliScriptAppended === true, 'the return shell flags cliScriptAppended')
179
187
  ok(r.engine === 'cli:/fake/bin/mytool' && r.engineInfo.name === 'cli:/fake/bin/mytool', 'cli is labelled cli:<command> in the shell and engineInfo')
180
188
  const rj = JSON.parse(state.file(r.receipt.json))
181
- ok(rj.engine.name === 'cli:/fake/bin/mytool' && rj.engine.source === 'cli' && rj.cli && rj.cli.argv.length === 2, 'cli receipt records cli:<command>, source=cli and argv')
189
+ ok(rj.engine.name === 'cli:/fake/bin/mytool' && rj.engine.source === 'cli' && rj.cli && rj.cli.argv.length === 2, 'cli receipt records cli:<command>, source=cli and the USER argv')
190
+ ok(rj.cli.scriptAppended === true && rj.argv[rj.argv.length - 1] === r.argv[3], 'the receipt records scriptAppended and the executed argv')
182
191
  })
183
192
  await withHost({ params: { mathMode: 'typed' } }, async (state) => {
184
193
  const r = await state.call({ op: 'run', engine: 'cli', mode: 'code', code: 'x', cli: { command: '/fake/bin/mytool' } })
@@ -498,6 +507,424 @@ console.log('-- math_computation shared contract --')
498
507
  }
499
508
  }
500
509
 
510
+ // ── 18. round-6: substitution honesty (A), manager dispatch (B), scope policy (C), version syntax (D)
511
+ {
512
+ // 18a (A): the rule is part of the frozen rule block and reaches both language lines.
513
+ ok(M.MATH_RULE_LINES.join('\n').indexOf(M.MATH_SUBSTITUTION_RULE_LINE) !== -1, 'MATH_RULE_LINES carries the substitution-honesty rule (zh)')
514
+ ok(M.MATH_RULE_LINES_EN.join('\n').indexOf(M.MATH_SUBSTITUTION_RULE_LINE_EN) !== -1, 'MATH_RULE_LINES_EN carries the substitution-honesty rule (en)')
515
+ ok(/替代方案若改变精确性/.test(String(M.MATH_TOOL_DESCRIPTION)), 'the tool description states the substitution duty (paraphrase)')
516
+ {
517
+ const p = await M.probeMathEngines(makeFakeHost({ installed: ['python3'] }).host)
518
+ const zh = M.mathAvailabilityLine(p, 'zh', 'typed+shell')
519
+ const en = M.mathAvailabilityLine(p, 'en', 'typed+shell')
520
+ ok(zh.indexOf('替代必须声明') !== -1, 'the injected (zh) availability line carries the substitution rule')
521
+ ok(en.indexOf('Declare substitutions') !== -1, 'the injected (en) availability line carries the substitution rule')
522
+ }
523
+
524
+ // 18b (B): python dispatch follows the DETECTED environment, never a guess.
525
+ {
526
+ const condaHost = makeFakeHost({ resolveMap: { python3: 'C:/miniconda3/envs/math/python.exe', conda: 'C:/miniconda3/Scripts/conda.exe' } })
527
+ M.registerMathComputation(condaHost.host)
528
+ const c = await condaHost.call({ op: 'install', engine: 'python', packages: ['numpy'] })
529
+ ok(c.ok === true && c.plan.manager === 'conda' && c.plan.managerAssumed === false, 'a conda-style interpreter plans with conda (not guessed)')
530
+ eq(c.plan.commands[0].argv, ['conda', 'install', '-y', '-c', 'conda-forge', 'numpy'], 'conda install comes from conda-forge')
531
+ ok(/conda/.test(String(c.message)), 'the plan message names the manager')
532
+
533
+ const uvHost = makeFakeHost({ resolveMap: { python3: '/venv/bin/python3', uv: '/venv/bin/uv' } })
534
+ M.registerMathComputation(uvHost.host)
535
+ const u = await uvHost.call({ op: 'install', engine: 'python', packages: ['numpy'] })
536
+ ok(u.ok === true && u.plan.manager === 'uv', 'a uv next to the interpreter plans with uv')
537
+ eq(u.plan.commands[0].argv, ['uv', 'pip', 'install', 'numpy'], 'uv install uses `uv pip install`')
538
+
539
+ const pipHost = makeFakeHost({ resolveMap: { python3: '/usr/bin/python3' } })
540
+ M.registerMathComputation(pipHost.host)
541
+ const d = await pipHost.call({ op: 'install', engine: 'python', packages: ['numpy'] })
542
+ ok(d.ok === true && d.plan.manager === 'pip' && d.plan.managerAssumed === true, 'no marker -> pip fallback, flagged as an ASSUMPTION')
543
+ eq(d.plan.commands[0].argv, ['/usr/bin/python3', '-m', 'pip', 'install', '--user', 'numpy'], 'the pip fallback keeps the user-scope form')
544
+ ok(/假设|assum/i.test(String(d.message)), 'the message says the manager is an assumption')
545
+ }
546
+
547
+ // 18c (C): R/Octave/Julia refuse system scope WITH a per-engine reason; conda too.
548
+ {
549
+ const h = makeFakeHost({ resolveMap: { Rscript: '/usr/bin/Rscript', octave: '/usr/bin/octave', julia: '/usr/bin/julia' } })
550
+ M.registerMathComputation(h.host)
551
+ for (const [engine, marker] of [['r', 'R_LIBS_USER'], ['octave', 'share/packages'], ['julia', 'JULIA_DEPOT_PATH']]) {
552
+ const r = await h.call({ op: 'install', engine: engine, packages: ['x'], scope: 'system' })
553
+ ok(r.ok === false && r.code === 'MATH_REFUSED' && r.next && r.next.reason === 'system-scope-unsupported', engine + ': system scope -> MATH_REFUSED(system-scope-unsupported)')
554
+ ok(String(r.message).indexOf(marker) !== -1, engine + ': the refusal states WHY (mentions ' + marker + ')')
555
+ }
556
+ const condaHost = makeFakeHost({ resolveMap: { python3: 'C:/miniconda3/envs/math/python.exe', conda: 'C:/miniconda3/Scripts/conda.exe' } })
557
+ M.registerMathComputation(condaHost.host)
558
+ const cs = await condaHost.call({ op: 'install', engine: 'python', packages: ['numpy'], scope: 'system' })
559
+ ok(cs.ok === false && cs.next.reason === 'system-scope-unsupported' && /conda/.test(String(cs.message)), 'conda has no system scope and says so')
560
+ }
561
+
562
+ // 18d (D): version syntax is passed through where the manager understands it, refused otherwise.
563
+ {
564
+ ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy==1.2'] }, {}).ok === true, 'pkg==1.2 is accepted (pass-through)')
565
+ ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy=1.2'] }, {}).ok === true, 'pkg=1.2 (conda form) is accepted')
566
+ ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy[extra]>=1.2'] }, {}).ok === true, 'extras + comparator are accepted')
567
+ const badSpec = M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy; rm -rf /'] }, {})
568
+ ok(badSpec.ok === false && badSpec.code === 'MATH_INVALID_ARGUMENT' && badSpec.next && badSpec.next.reason === 'unsupported-version-syntax', 'a shell-ish spec is refused with unsupported-version-syntax')
569
+
570
+ const ph = makeFakeHost({ packages: { sympy: 'present' } })
571
+ M.registerMathComputation(ph.host)
572
+ await ph.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['sympy==1.12'] })
573
+ const pkgSpawns = ph.spawns.filter((s) => s.argv.join(' ').indexOf('sympy') !== -1)
574
+ ok(pkgSpawns.length > 0 && pkgSpawns.every((s) => s.argv.join(' ').indexOf('sympy==1.12') === -1), 'presence is probed by BASE NAME (the constraint never reaches the probe)')
575
+ const plan = await ph.call({ op: 'install', engine: 'python', packages: ['sympy==1.12'] })
576
+ ok(plan.ok === true && plan.plan.commands[0].argv[plan.plan.commands[0].argv.length - 1] === 'sympy==1.12', 'the install plan passes the spec through verbatim')
577
+ ok(!!plan.plan.versionPolicy, 'the plan states the version policy (solving belongs to the manager)')
578
+ }
579
+ }
580
+
581
+ // ── 19. round-7: probe reports the REQUESTED engine (fix 1) + DSH bundled-runtime discovery (fix 2)
582
+ {
583
+ // 19a (fix 1): `probe {engine:'r'}` must describe r - not the first allowed engine (python).
584
+ {
585
+ const hr = makeFakeHost({ installed: ['python3', 'python', 'Rscript'] })
586
+ M.registerMathComputation(hr.host)
587
+ const p = await hr.call({ op: 'probe', engine: 'r' })
588
+ ok(p.ok === true && p.engine === 'r', 'probe with engine:r reports engine:r (not python)')
589
+ ok(!!p.engineInfo && p.engineInfo.name === 'r' && !!p.engineInfo.version, "probe reports the requested engine's version/path")
590
+ ok(!p.next, 'a usable requested engine yields no install guidance')
591
+ const pp = await hr.call({ op: 'probe', engine: 'python' })
592
+ ok(pp.ok === true && pp.engine === 'python' && pp.engineInfo.name === 'python', 'probe with engine:python reports python (each request is honoured)')
593
+ }
594
+ // 19b (fix 1): a MISSING requested engine must not blame python in the guidance.
595
+ {
596
+ const hm = makeFakeHost({ installed: ['python3', 'python'] })
597
+ M.registerMathComputation(hm.host)
598
+ const p = await hm.call({ op: 'probe', engine: 'r' })
599
+ ok(p.ok === false && p.code === 'MATH_ENGINE_NOT_FOUND' && p.engine === 'r', 'a missing requested engine is reported as THAT engine (r, not python)')
600
+ ok(p.next && p.next.kind === 'user-install' && p.next.engine === 'r' && !!p.next.perOs, 'the install guidance is for the REQUESTED engine (r), with per-OS commands')
601
+ const msg = String(p.message)
602
+ ok(/试过:r|请求:r/.test(msg) && !/python/i.test(msg), 'the message names the requested engine (r), never a python fallback')
603
+ }
604
+ // 19c (fix 2): a DSH-bundled runtime is discovered as a LAST resort and used like any engine.
605
+ {
606
+ const absTree = {
607
+ 'X:/home/.dsh/dsh-runtimes': [{ name: 'dsh-primary-runtime', type: 'directory' }, { name: 'dsh-other-runtime', type: 'directory' }],
608
+ 'X:/home/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/python': [{ name: 'python.exe', type: 'file' }, { name: 'python3.exe', type: 'file' }],
609
+ 'X:/home/.dsh/dsh-runtimes/dsh-other-runtime/dependencies/python': [],
610
+ }
611
+ const hb = makeFakeHost({ installed: [], runtimeRoots: ['X:/home/.dsh'], absTree: absTree })
612
+ M.registerMathComputation(hb.host)
613
+ const p = await hb.call({ op: 'probe', engine: 'python' })
614
+ ok(p.ok === true && p.engine === 'python' && p.engineInfo.name === 'python', 'a bundled DSH runtime is found when PATH has nothing')
615
+ ok(/dsh-runtimes\/dsh-primary-runtime\/dependencies\/python\/python/.test(p.engineInfo.path), 'the discovered path comes from the generically globbed runtime tree')
616
+ const r = await hb.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
617
+ ok(r.ok === true && !!r.engineInfo.version, 'a run on the bundled runtime succeeds and reports its version')
618
+ ok(!r.next, 'a found bundled runtime emits NO install guidance')
619
+ // The bundled runtime must not be invented when the tree is absent.
620
+ const hn = makeFakeHost({ installed: [] })
621
+ M.registerMathComputation(hn.host)
622
+ const n = await hn.call({ op: 'probe', engine: 'python' })
623
+ ok(n.ok === false && n.code === 'MATH_ENGINE_NOT_FOUND' && n.next && n.next.kind === 'user-install', 'with no PATH engine and no runtime tree the per-OS guidance still fires')
624
+ }
625
+ // 19d (finding 3): the cli package precheck runs against the FAMILY of `cli.command`, not the
626
+ // generic cli descriptor (which has no packageProbe and therefore reported a false "missing").
627
+ {
628
+ const hc = makeFakeHost({ installed: [], packages: { numpy: 'present' } })
629
+ M.registerMathComputation(hc.host)
630
+ const r = await hc.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3' }, packages: ['numpy'] })
631
+ ok(r.ok === true, 'cli + packages: a python-family command prechecks with the python probe (numpy present => the run proceeds)')
632
+ const probes = hc.spawns.filter((s) => s.argv.join(' ').indexOf('numpy') !== -1)
633
+ ok(probes.length > 0 && probes.every((s) => String(s.argv[0]).indexOf('python3') !== -1), 'the package precheck ran against cli.command itself')
634
+ // An unrecognisable command must SKIP the precheck (never a false missing) and say so.
635
+ const hu = makeFakeHost({ installed: [] })
636
+ M.registerMathComputation(hu.host)
637
+ const ru = await hu.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/mytool' }, packages: ['numpy'] })
638
+ ok(ru.ok === true && (ru.warnings || []).some((w) => w.code === 'PACKAGE_PRECHECK_SKIPPED'), 'an unknown cli family skips the precheck with an explicit warning (no false missing-packages)')
639
+ }
640
+ // 19e (finding 5): cli reports the REAL version of the resolved command when its family is known.
641
+ {
642
+ const hv = makeFakeHost({ installed: [] })
643
+ M.registerMathComputation(hv.host)
644
+ const p = await hv.call({ op: 'probe', engine: 'cli', cli: { command: '/fake/bin/python3' } })
645
+ ok(p.ok === true && p.engineInfo && p.engineInfo.version === '1.2.3', 'probe(engine:cli) reports the real version of the resolved command')
646
+ const r = await hv.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3' } })
647
+ ok(r.engineInfo.version === '1.2.3', 'run(engine:cli) reports the real command version (not "unknown")')
648
+ }
649
+ // 19f (finding 6, found on the real bundled python): the probe code iterates `sys.argv[1:]`, so the
650
+ // names must arrive as SEPARATE argv items - a comma-joined single item makes python look up one
651
+ // package literally named "numpy,pandas" and report everything missing.
652
+ {
653
+ const hp = makeFakeHost({ installed: ['python3'], packages: { numpy: 'present', pandas: 'present' } })
654
+ M.registerMathComputation(hp.host)
655
+ await hp.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy', 'pandas'] })
656
+ const probe = hp.spawns.find((s) => s.argv.join(' ').indexOf('numpy') !== -1)
657
+ ok(!!probe && probe.argv.indexOf('numpy') !== -1 && probe.argv.indexOf('pandas') !== -1, 'the python package probe passes ONE argv item per package (sys.argv-based probe)')
658
+ ok(!!probe && probe.argv.join(' ').indexOf('numpy,pandas') === -1, 'the probe never passes the comma-joined list as a single argv item')
659
+ // round-7 (live): a real host rejected probe argv containing shell metacharacters (| %), so no
660
+ // probe argv may carry them - the parsers accept both `name:state` and `name state` shapes.
661
+ ok(!!probe && !/[|%]/.test(probe.argv.join(' ')), 'no probe argv contains a shell metacharacter (| or %)')
662
+ for (const name of ['python', 'r', 'octave', 'julia']) {
663
+ const d = (await import('../vibe-math-v2/math-engines.js')).MATH_ENGINES[name]
664
+ for (const a of (d.packageProbe.argv || [])) ok(!/[|%]/.test(String(a)), name + ' probe template has no | or %: ' + String(a).slice(0, 40))
665
+ ok(!/[|%]/.test(String(d.probeCode || 'numpy ok line')), name + ' probeCode has no | or %')
666
+ }
667
+ }
668
+ // 19f-bis: cli + mode:'code' actually PASSES the script (finding 4) - verified through argv shape.
669
+ {
670
+ const hs = makeFakeHost({ installed: [] })
671
+ M.registerMathComputation(hs.host)
672
+ const r = await hs.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3' } })
673
+ const last = String(r.argv[r.argv.length - 1] || '')
674
+ ok(r.ok === true && /script\.txt$/.test(last), 'cli + code: the archived script is last in argv (it is really executed, not a REPL)')
675
+ const runSpawn = hs.spawns.filter((s) => String(s.argv[s.argv.length - 1] || '').indexOf('script.txt') !== -1)
676
+ ok(runSpawn.length === 1, 'the executed argv carried the script path to the process')
677
+ // …but when the CALLER's argv already runs code (-c/-m/-e/--eval), the caller's argv wins: the
678
+ // archived script must NOT be appended after it (python would read it as sys.argv[1]).
679
+ const rc = await hs.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3', argv: ['-c', 'print(1)'] } })
680
+ ok(rc.ok === true && rc.cliScriptAppended === false, 'cli + code with a caller program slot (-c) does NOT append the archived script')
681
+ ok(rc.argv.join(' ').indexOf('script.txt') === -1, 'the executed argv is exactly the caller argv (no script path)')
682
+ ok(!!rc.receipt && !!rc.receipt.json, 'the skipped-append run still writes a receipt')
683
+ }
684
+ // 19g (live-session fix): an engine that RESOLVED but whose version probe failed must NOT come with
685
+ // install guidance (the user does not need to install anything) and must carry the diagnostic.
686
+ {
687
+ const hh = makeFakeHost({ installed: ['python3', 'python'], versionProbeFails: true })
688
+ M.registerMathComputation(hh.host)
689
+ const p = await hh.call({ op: 'probe', engine: 'python' })
690
+ ok(p.ok === false && p.code === 'MATH_ENGINE_UNUSABLE', 'a resolved engine whose probe fails => MATH_ENGINE_UNUSABLE')
691
+ ok(!p.next || p.next.kind !== 'user-install', 'unusable (but FOUND) engine carries NO install guidance')
692
+ ok(!!p.probe && Array.isArray(p.probe.argv) && p.probe.argv[0].indexOf('python3') !== -1, 'the failure carries the probe diagnostic (argv)')
693
+ ok(!!p.probe && p.probe.exit === 1 && p.probe.stderrTail === 'probe boom', 'the diagnostic carries exit + stderr tail')
694
+ ok(/MATH_ENGINE_UNUSABLE/.test(p.code) && /exit=1/.test(String(p.message)), 'the message states the probe exit code (never an opaque "unusable")')
695
+ // A deterministic failure is NOT retried (no retryDiag, no retry storm), only a cold/null/timeout
696
+ // signature is - see 19h.
697
+ ok(!(p.probe && p.probe.retried), 'a deterministic probe failure is NOT retried (no retryDiag)')
698
+ }
699
+ // 19h (live-session fix): the real host intermittently returns a NULL spawn early in a session.
700
+ // The retry must cover spawned:false / exit:null (not just timeouts) so the probe still succeeds.
701
+ {
702
+ const hN = makeFakeHost({ installed: ['python3', 'python'], nullSpawnTimes: 2 })
703
+ M.registerMathComputation(hN.host)
704
+ const p = await hN.call({ op: 'probe', engine: 'python' })
705
+ ok(p.ok === true && p.engine === 'python' && p.engineInfo.version === '1.2.3', 'a transient NULL spawn is retried and the probe still succeeds')
706
+ ok(hN.spawns.filter((s) => s.nullSpawn).length === 2, 'the null spawns are visible in the seam (2 consumed)')
707
+ }
708
+ // 19i (live-session fix): a package precheck that could not RUN must never be reported as
709
+ // "missing package", and the skip must be explicit (fail-open + named warning).
710
+ {
711
+ const hP = makeFakeHost({ installed: ['python3'], packageProbeNullTimes: 9 })
712
+ M.registerMathComputation(hP.host)
713
+ const r = await hP.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy'] })
714
+ ok(r.ok !== false || r.code !== 'MATH_MISSING_PACKAGES', 'a precheck that could not run is NEVER reported as a missing package')
715
+ ok((r.warnings || []).some((w) => w.code === 'PACKAGE_PRECHECK_UNKNOWN'), 'the un-runnable precheck is reported explicitly (PACKAGE_PRECHECK_UNKNOWN)')
716
+ }
717
+ }
718
+
719
+ // ── 20. round-7 cross-cutting invariant: EVERY response must be JSON-serialisable
720
+ // (a real session got `Converting circular structure to JSON ... property 'retryDiag' closes the
721
+ // circle` from a probe/run response, which meant the caller saw no result at all). This applies to
722
+ // all four presets because they all ship this module.
723
+ {
724
+ const cases = []
725
+ const push = (label, r) => cases.push({ label, r })
726
+ {
727
+ const h = makeFakeHost({ installed: ['python3', 'python', 'Rscript'], packages: { numpy: 'present' } })
728
+ M.registerMathComputation(h.host)
729
+ push('probe ok', await h.call({ op: 'probe', engine: 'python' }))
730
+ push('probe missing engine', await h.call({ op: 'probe', engine: 'octave' }))
731
+ push('run ok', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' }))
732
+ push('run bad mode', await h.call({ op: 'run', engine: 'python', mode: 'nope' }))
733
+ push('run missing pkg', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', packages: ['pandas'] }))
734
+ push('receipt missing', await h.call({ op: 'receipt', id: 'nope' }))
735
+ push('install plan', await h.call({ op: 'install', engine: 'python', packages: ['numpy'] }))
736
+ push('install commercial', await h.call({ op: 'install', engine: 'matlab', packages: ['MATLAB'] }))
737
+ push('install system scope r', await h.call({ op: 'install', engine: 'r', packages: ['x'], scope: 'system' }))
738
+ push('unsupported spec', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', packages: ['numpy; rm -rf /'] }))
739
+ push('unknown engine', await h.call({ op: 'run', engine: 'nope', mode: 'code', code: 'x\n' }))
740
+ }
741
+ {
742
+ const h = makeFakeHost({ installed: ['python3'], versionProbeFails: true })
743
+ M.registerMathComputation(h.host)
744
+ push('probe unusable (probe diagnostics)', await h.call({ op: 'probe', engine: 'python' }))
745
+ }
746
+ {
747
+ // A RETRIED probe carries retryDiag: that is exactly where the circular payload came from.
748
+ const h = makeFakeHost({ installed: ['python3'], nullSpawnTimes: 40 })
749
+ M.registerMathComputation(h.host)
750
+ push('probe unusable after retries (retryDiag present)', await h.call({ op: 'probe', engine: 'python' }))
751
+ }
752
+ {
753
+ const h = makeFakeHost({ installed: ['python3'], packageProbeNullTimes: 9 })
754
+ M.registerMathComputation(h.host)
755
+ push('run with un-runnable precheck', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', packages: ['numpy'] }))
756
+ }
757
+ {
758
+ const h = makeFakeHost({ installed: [] })
759
+ M.registerMathComputation(h.host)
760
+ push('probe nothing', await h.call({ op: 'probe', engine: 'python' }))
761
+ }
762
+ {
763
+ const h = makeFakeHost({ resolveThrows: true })
764
+ M.registerMathComputation(h.host)
765
+ push('probe resolve throws', await h.call({ op: 'probe', engine: 'python' }))
766
+ }
767
+ for (const c of cases) {
768
+ let json = null
769
+ let err = null
770
+ try { json = JSON.stringify(c.r) } catch (e) { err = String((e && e.message) || e) }
771
+ ok(json !== null, '★ response is JSON-serialisable: ' + c.label + (err ? ' (' + err + ')' : ''))
772
+ }
773
+ // The diagnostics themselves must never carry a back-reference.
774
+ {
775
+ const h = makeFakeHost({ installed: ['python3'], nullSpawnTimes: 2 })
776
+ M.registerMathComputation(h.host)
777
+ const r = await h.call({ op: 'probe', engine: 'python' })
778
+ ok(r.ok === true, 'the null-spawn retry recovers (JSON invariant context)')
779
+ const h2 = makeFakeHost({ installed: ['python3'], versionProbeFails: true })
780
+ M.registerMathComputation(h2.host)
781
+ const u = await h2.call({ op: 'probe', engine: 'python' })
782
+ ok(!!u.probe && JSON.stringify(u.probe).indexOf('retryDiag') === -1 ? true : !!u.probe, 'a deterministic failure still exposes its probe diagnostics')
783
+ }
784
+ }
785
+
786
+ // ── 21. round-7 (live root cause): probes must not depend on the project root EXISTING
787
+ // The real host answers a spawn whose cwd does not exist with `spawned:true, exit:null` (retries
788
+ // cannot help). On a brand-new session the project root does not exist yet, so the version probe and
789
+ // the package precheck must run from a cwd that is guaranteed to exist.
790
+ {
791
+ const FRESH = '/fresh/nonexistent/workspace'
792
+ const h = makeFakeHost({ installed: ['python3', 'python'], projectRoot: FRESH, packages: { numpy: 'present' } })
793
+ M.registerMathComputation(h.host)
794
+ const p = await h.call({ op: 'probe', engine: 'python' })
795
+ ok(p.ok === true && p.engine === 'python' && p.engineInfo.version === '1.2.3', '★ a probe on a FRESH (non-existent) project root still succeeds with a real version')
796
+ const versionSpawns = h.spawns.filter((s) => /--version/.test(s.argv.join(' ')))
797
+ ok(versionSpawns.length > 0, 'the version probe actually spawned')
798
+ ok(versionSpawns.every((s) => String(s.cwd) !== FRESH), '★ no version probe spawn uses the non-existent project root as cwd')
799
+ ok(versionSpawns.every((s) => !s.nullSpawn), 'no version probe spawn reported exit:null (fresh-root signature)')
800
+ const r = await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy'] })
801
+ const pkgSpawns = h.spawns.filter((s) => s.argv.join(' ').indexOf('numpy') !== -1)
802
+ ok(r.ok === true, 'a run with packages on a FRESH root passes the precheck')
803
+ ok(pkgSpawns.length > 0 && pkgSpawns.every((s) => String(s.cwd) !== FRESH), '★ the package precheck also avoids the non-existent project root')
804
+ ok(!(r.warnings || []).some((w) => w.code === 'PACKAGE_PRECHECK_UNKNOWN'), 'the precheck RAN (no PACKAGE_PRECHECK_UNKNOWN on a fresh root)')
805
+ }
806
+
807
+ // ── 22. round-8 (P0/D3): member-facing artifact paths must be OPENABLE by members
808
+ // The receipt's scriptPath is PROJECT-ROOT relative, but members' file tools resolve against the
809
+ // SESSION CWD. The receipt/return therefore carry the absolute `scriptAbs` and the `cwd` to join with.
810
+ {
811
+ const h = makeFakeHost({ installed: ['python3'] })
812
+ M.registerMathComputation(h.host)
813
+ const r = await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
814
+ ok(r.ok === true, 'a run succeeds (P0 context)')
815
+ ok(typeof r.scriptAbs === 'string' && r.scriptAbs.length > 0, '★ the return carries an ABSOLUTE scriptAbs (members can open it directly)')
816
+ ok(typeof r.cwd === 'string' && r.cwd.length > 0, '★ the return carries cwd (the project root to join with)')
817
+ ok(String(r.scriptAbs).replace(/\\/g, '/').endsWith(String(r.scriptPath).replace(/\\/g, '/')), '★ scriptAbs ends with the receipt-relative scriptPath (join consistency)')
818
+ ok(!!h.files && h.files.has(String(r.scriptPath).replace(/\\/g, '/')), '★ the archived script really exists at scriptPath (the path handed to members is real)')
819
+ const rj = JSON.parse(h.files.get(String(r.receipt.json).replace(/\\/g, '/')))
820
+ ok(rj.scriptAbs === r.scriptAbs && rj.cwd === r.cwd, 'the RECEIPT carries scriptAbs + cwd too (a later reader can reconstruct the location)')
821
+ ok(String(M.MATH_ARCHIVE_WORKFLOW_LINE).indexOf('receipt.scriptAbs') !== -1 && String(M.MATH_ARCHIVE_WORKFLOW_LINE_EN).indexOf('receipt.scriptAbs') !== -1, 'the injected archive rule tells members HOW to open the script (zh+en)')
822
+ ok(/会话 cwd/.test(String(M.MATH_ARCHIVE_WORKFLOW_LINE)) && /SESSION CWD/.test(String(M.MATH_ARCHIVE_WORKFLOW_LINE_EN)), 'the rule states the session-cwd resolution fact (zh+en)')
823
+ }
824
+
825
+ // ── 23. round-9 (real-engine): an engine INSTALLED but not on PATH must still be discovered
826
+ // R 4.6.1 went to `C:\Program Files\R\R-4.6.1\bin` without touching PATH, so `probe` used to say R
827
+ // was missing even though it was installed.
828
+ {
829
+ const absTree = {
830
+ 'C:/Program Files/R': [{ name: 'R-4.6.1', type: 'directory' }],
831
+ 'C:/Program Files/R/R-4.6.1/bin': [{ name: 'Rscript.exe', type: 'file' }, { name: 'R.exe', type: 'file' }],
832
+ }
833
+ const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [] })
834
+ M.registerMathComputation(h.host)
835
+ const p = await h.call({ op: 'probe', engine: 'r' })
836
+ ok(p.ok === true && p.engine === 'r', '★ a Rscript that exists ONLY in the default install dir is discovered (not on PATH)')
837
+ ok(!!p.engineInfo && /Program Files\/R\/R-4\.6\.1\/bin\/Rscript/.test(String(p.engineInfo.path)), 'the discovered path is the versioned install dir (' + String(p.engineInfo && p.engineInfo.path).slice(0, 60) + ')')
838
+ }
839
+ // ── 24. round-9 (real-engine): never suggest an install command that cannot run on this machine
840
+ {
841
+ const h = makeFakeHost({ installed: [] }) // `winget` is NOT resolvable here
842
+ M.registerMathComputation(h.host)
843
+ const p = await h.call({ op: 'probe', engine: 'octave' })
844
+ ok(p.ok === false && p.code === 'MATH_ENGINE_NOT_FOUND', 'a missing engine still fails with ENGINE_NOT_FOUND')
845
+ ok(!!p.next && p.next.kind === 'user-install', 'the guidance is still user-install shaped')
846
+ ok(String(p.next.command) === '', '★ no runnable-looking command when its package manager is absent (command is empty)')
847
+ ok(!/winget|brew|apt|choco|scoop/i.test(String(p.next.command) + ' ' + String(p.message)), '★ no winget/brew/apt suggestion anywhere when that manager is absent')
848
+ ok(p.next.packageManager === 'winget' && p.next.packageManagerAvailable === false, 'the response SAYS which manager was assumed and that it is unavailable')
849
+ ok(typeof p.next.note === 'string' && p.next.note.length > 0, 'an explicit note explains that the suggested command cannot run')
850
+ const h2 = makeFakeHost({ installed: [], resolveMap: { winget: 'C:/fake/winget.exe' } })
851
+ M.registerMathComputation(h2.host)
852
+ const p2 = await h2.call({ op: 'probe', engine: 'octave' })
853
+ ok(/winget/i.test(String(p2.next.command)) && p2.next.packageManagerAvailable === true, 'when the manager IS present the command is offered as before')
854
+ }
855
+
856
+ // ── 25. round-9 F3/F5/F7: probe completeness, uniform failure evidence, explicit constraint policy
857
+ {
858
+ // F3: a partial availability line must NAME the configured-but-absent engines.
859
+ const h = makeFakeHost({ installed: ['python3'] })
860
+ M.registerMathComputation(h.host)
861
+ const p = await h.call({ op: 'probe', engine: 'python' })
862
+ ok(p.ok === true && Array.isArray(p.absent) && p.absent.length > 0, '★ probe names the configured-but-absent engines instead of being silently partial')
863
+ ok(p.absent.some((a) => a.engine === 'r' && a.why === 'not-found-on-this-machine'), 'the absent list carries a reason (key `why`) per engine')
864
+ ok(p.absent.some((a) => a.engine === 'cli' && /caller-supplied/.test(a.why)), 'cli is explained as needing a caller-supplied command')
865
+ ok(/未发现/.test(String(p.message)), 'the message states which configured engines were not found')
866
+ // F5: MATH_TIMEOUT carries the SAME evidence fields a non-zero exit does.
867
+ const h2 = makeFakeHost({ installed: ['python3'], hang: true })
868
+ M.registerMathComputation(h2.host)
869
+ const t = await h2.call({ op: 'run', engine: 'python', mode: 'code', code: 'while True: pass\n', timeoutMs: 1000 })
870
+ ok(t.ok === false && t.code === 'MATH_TIMEOUT', 'a hanging run reports MATH_TIMEOUT')
871
+ ok(t.timedOut === true && 'exit' in t && t.exit === null, '★ MATH_TIMEOUT carries timedOut:true and an explicit exit field (uniform with NONZERO_EXIT)')
872
+ ok('stderr' in t && typeof t.stderr === 'string', '★ MATH_TIMEOUT carries the captured stderr like NONZERO_EXIT does')
873
+ // F7: the response says the constraints are existence-only, and lists the ones that were NOT checked.
874
+ const h3 = makeFakeHost({ installed: ['python3'], packages: { numpy: 'present' } })
875
+ M.registerMathComputation(h3.host)
876
+ const c = await h3.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy>=1'] })
877
+ ok(c.ok === true && /existence-only/.test(String(c.versionPolicy)), '★ the run response states the version constraints are existence-only')
878
+ ok(Array.isArray(c.constraintsNotEnforced) && c.constraintsNotEnforced.indexOf('numpy>=1') !== -1, '★ the response names the constraint it did NOT check (no silent drop)')
879
+ const c2 = await h3.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy'] })
880
+ ok(Array.isArray(c2.constraintsNotEnforced) && c2.constraintsNotEnforced.length === 0, 'an unconstrained request reports an empty not-enforced list')
881
+ const m = await h3.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['pandas>=2'] })
882
+ ok(m.ok === false && /existence-only/.test(String(m.versionPolicy)), 'the missing-package failure also states the constraint policy')
883
+ }
884
+
885
+ // ── 26. round-9 (N1): edited-archived-script evidence integrity
886
+ // The documented P2a flow: edit the ORIGINAL source, re-run mode:'file' ⇒ SAME archive id, attempt>=2,
887
+ // scriptChanged:true, previousReceipt at the prior attempt, prior attempt untouched. Pointing mode:'file'
888
+ // at an ARCHIVED script is a NEW archive id by design (the id is keyed by the SOURCE PATH) - which must
889
+ // be SAID (fileIsArchivedScript + a warning), not look like broken change detection.
890
+ {
891
+ const h = makeFakeHost({ installed: ['python3'], files: { 'Problems/x.py': 'print(1)\n' } })
892
+ M.registerMathComputation(h.host)
893
+ const r1 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/x.py' })
894
+ ok(r1.ok === true && r1.attempt === 1 && r1.scriptChanged === false, 'first run of a source file: attempt 1, no change')
895
+ const rj1 = JSON.parse(h.files.get(String(r1.receipt.json).replace(/\\/g, '/')))
896
+ ok(typeof r1.runId === 'string' && rj1.runId === r1.runId, '★ the run response exposes its archive id (matches the durable receipt.runId) - needed to check "same archive id"')
897
+ h.files.set('Problems/x.py', 'print(2)\n')
898
+ const r2 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/x.py' })
899
+ ok(r2.ok === true && r2.runId === r1.runId, '★ editing the ORIGINAL source and re-running lands on the SAME archive id')
900
+ ok(r2.attempt >= 2 && /\/attempts\/\d+/.test(String(r2.attemptDir || '')), '★ the re-run is attempt>=2 under attempts/<n>/')
901
+ ok(r2.scriptChanged === true, '★ the re-run reports scriptChanged:true')
902
+ ok(!!r2.previousReceipt && r2.previousReceipt.attempt === r1.attempt && r2.previousReceipt.scriptHash === r1.scriptHash, '★ previousReceipt points at the prior attempt (attempt + scriptHash)')
903
+ ok(h.files.get(String(r1.scriptPath).replace(/\\/g, '/')) === 'print(1)\n', '★ the prior attempt\'s script is NOT overwritten (append-only)')
904
+ // A DIFFERENT source path (e.g. a copy of the old script, or a brand-new file) is a NEW archive id BY
905
+ // DESIGN: the id is keyed by the source path, so there is no history for that id and
906
+ // scriptChanged/previousReceipt are - correctly - empty. This is the case a finder can mistake for
907
+ // "broken change detection"; it is not, and the docs now say so.
908
+ h.files.set('Problems/copy_of_old.py', 'print(1)\n')
909
+ const r3 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/copy_of_old.py' })
910
+ ok(r3.ok === true, 'a run of a different source path succeeds (' + JSON.stringify({ ok: r3.ok, code: r3.code }) + ')')
911
+ ok(r3.runId !== r1.runId, '★ a DIFFERENT source path gets a DIFFERENT archive id')
912
+ ok(r3.attempt === 1, '★ …and therefore starts at attempt 1 (fresh archive, by design)')
913
+ ok(r3.scriptChanged === false, '★ …and reports scriptChanged:false (no history FOR THIS id)')
914
+ ok(r3.previousReceipt === null || r3.previousReceipt === undefined, '★ …and has no previousReceipt (expected, not broken change detection)')
915
+ ok(h.files.get(String(r1.scriptPath).replace(/\\/g, '/')) === 'print(1)\n', 'the earlier attempt is still untouched after that run')
916
+ }
917
+ // ── 27. round-9 (N2): a TIMEOUT carries its partial output in BOTH the response and the receipt
918
+ {
919
+ const h = makeFakeHost({ installed: ['python3'], hang: true, stdoutBytes: 16 })
920
+ M.registerMathComputation(h.host)
921
+ const t = await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print("before-timeout")\n', timeoutMs: 1000 })
922
+ ok(t.ok === false && t.code === 'MATH_TIMEOUT', 'the hanging run reports MATH_TIMEOUT')
923
+ ok('stdout' in t && 'stderr' in t, '★ the TIMEOUT response carries stdout AND stderr (not just the archived file)')
924
+ const rj = JSON.parse(h.files.get(String(t.receipt.json).replace(/\\/g, '/')))
925
+ ok(typeof rj.partialStdout === 'string' && typeof rj.partialStderr === 'string', '★ the TIMEOUT receipt JSON carries the capped partial output (as strings)')
926
+ }
927
+
501
928
  console.log('')
502
929
  console.log('=== MATH COMPUTATION SHARED: ' + passed + ' passed, ' + failed + ' failed ===')
503
930
  if (failures.length) for (const f of failures) console.error(' - ' + f)
@@ -41,7 +41,8 @@
41
41
  mode.
42
42
 
43
43
  - math_computation {op: probe|run|receipt|install, engine, mode: code|file|expr, …} — 先 probe 预检引擎/包/许可,再调引擎计算并把脚本与输出归档成可复核回执;缺引擎/缺包只报告与给安装指引/计划;shell 兜底不算归档。
44
- - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>),你可以用普通文件工具打开并编辑它;编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
44
+ - 替代必须声明(诚实性):当替代方案改变了**精确性或结论强度**时(精确符号解 → 数值近似、闭式解 → 采样/求积、改了精度/容差/假设、换了算法类),结论里**必须写明**,不得读起来像得到了原本(精确/所要求的)结果;拿不到精确结果就直说。
45
+ - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
45
46
 
46
47
  Final paper: strict termination writes Paper/<project>/{paper.md,paper.tex,paper.pdf,paper.meta.json,paper.log.md}
47
48
  automatically — a dedicated paper-writing child assembles the solved material (finalPaper=false disables only that
@@ -118,7 +119,8 @@
118
119
  mode.
119
120
 
120
121
  - math_computation {op: probe|run|receipt|install, engine, mode: code|file|expr, …} — 先 probe 预检引擎/包/许可,再调引擎计算并把脚本与输出归档成可复核回执;缺引擎/缺包只报告与给安装指引/计划;shell 兜底不算归档。
121
- - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>),你可以用普通文件工具打开并编辑它;编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
122
+ - 替代必须声明(诚实性):当替代方案改变了**精确性或结论强度**时(精确符号解 → 数值近似、闭式解 → 采样/求积、改了精度/容差/假设、换了算法类),结论里**必须写明**,不得读起来像得到了原本(精确/所要求的)结果;拿不到精确结果就直说。
123
+ - 归档→编辑→重跑:mode:'code' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);编辑后用 mode:'file' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。
122
124
 
123
125
  Final paper: strict termination writes Paper/<project>/{paper.md,paper.tex,paper.pdf,paper.meta.json,paper.log.md}
124
126
  automatically — a dedicated paper-writing child assembles the solved material (finalPaper=false disables only that