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.
- package/README.en.md +681 -685
- package/README.md +9 -13
- package/cordis.patch.yml +12 -6
- package/docs/AUDIT-CHECKLIST.md +518 -518
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +588 -588
- package/docs/math-computation.md +187 -121
- package/docs/release-notes/README.en.md +48 -0
- package/docs/release-notes/README.md +47 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.1.en.md +86 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
- package/docs/release-notes/TEMPLATE.md +120 -0
- package/docs/test-timing.md +154 -154
- package/package.json +208 -201
- package/prompt-corpus-persona/persona-corpus.json +8 -8
- package/prompt-corpus-persona/persona-corpus.md +24 -12
- package/prompt-corpus-v2/formal-verify-v2.json +176 -98
- package/prompt-corpus-v2/formal-verify-v2.md +1161 -96
- package/prompt-corpus-v3/formal-verify-v3.json +201 -63
- package/prompt-corpus-v3/formal-verify-v3.md +1401 -57
- package/prompt-corpus-v4/formal-verify-v4.json +10 -10
- package/prompt-corpus-v4/formal-verify-v4.md +40 -20
- package/prompt-corpus-v5/prompt-corpus-v5.json +81 -81
- package/prompt-corpus-v5/prompt-corpus-v5.md +199 -128
- package/tests/audit-math-computation-contract.mjs +56 -1
- package/tests/audit-math-computation-parity.mjs +30 -1
- package/tests/audit-math-computation-sensitivity.mjs +2 -2
- package/tests/audit-v5-sensitivity.mjs +3 -1
- package/tests/e2e-v5-round2.test.mjs +116 -0
- package/tests/formal-verify-v2.test.mjs +108 -2
- package/tests/formal-verify-v3.test.mjs +117 -2
- package/tests/formal-verify-v4.test.mjs +87 -0
- package/tests/helpers/math-computation-fake-seam.mjs +30 -2
- package/tests/math-computation-shared.test.mjs +430 -3
- package/vibe-math-v2/agent.cordis.yml +4 -2
- package/vibe-math-v2/math-computation.js +509 -55
- package/vibe-math-v2/math-engines.js +47 -6
- package/vibe-math-v2/vibe-math-v2.js +92 -12
- package/vibe-math-v3/agent.cordis.yml +4 -2
- package/vibe-math-v3/math-computation.js +509 -55
- package/vibe-math-v3/math-engines.js +47 -6
- package/vibe-math-v3/vibe-math-v3.js +77 -14
- package/vibe-math-v4/agent.cordis.yml +8 -4
- package/vibe-math-v4/math-computation.js +509 -55
- package/vibe-math-v4/math-engines.js +47 -6
- package/vibe-math-v4/vibe-math-v4.js +70 -15
- package/vibe-math-v5/agent.cordis.yml +8 -4
- package/vibe-math-v5/math-computation.js +509 -55
- package/vibe-math-v5/math-engines.js +47 -6
- package/vibe-math-v5/vibe-math-v5.js +152 -31
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1660 -1655
- 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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|