dsh-vibe-math 2.7.2 → 2.7.3
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 +11 -10
- package/README.md +13 -12
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +229 -3
- package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
- package/docs/final-paper.md +2 -2
- package/docs/math-computation.md +13 -6
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +1 -0
- package/docs/release-notes/README.md +1 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/TEMPLATE.md +6 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +8 -5
- package/installer.js +27 -17
- package/package.json +250 -203
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +295 -115
- package/prompt-corpus-v2/formal-verify-v2.md +2287 -283
- package/prompt-corpus-v3/formal-verify-v3.json +105 -105
- package/prompt-corpus-v3/formal-verify-v3.md +213 -143
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +451 -207
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +52 -4
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +64 -8
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +4 -2
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +14 -0
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +63 -22
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +50 -0
- package/tests/audit-prompt-invariants.mjs +43 -5
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +300 -13
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +12 -7
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +458 -13
- package/tests/formal-verify-v2.test.mjs +91 -43
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +85 -39
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +541 -9
- package/tests/formal-verify-v5.test.mjs +68 -9
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +274 -5
- package/tests/math-computation-v2.test.mjs +767 -0
- package/tests/math-computation-v3.test.mjs +759 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +13 -2
- package/tests/run-tests.mjs +139 -9
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +53 -1
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/v2-fix-probes.mutants.mjs +100 -0
- package/tests/v2-fix-probes.test.mjs +1039 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v3-fix-probes.mutants.mjs +95 -0
- package/tests/v3-fix-probes.test.mjs +891 -0
- package/tests/v5-institute-fixes.mutants.mjs +190 -0
- package/vibe-math-v2/agent.cordis.yml +62 -6
- package/vibe-math-v2/math-computation.js +130 -41
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/vibe-math-v2.js +388 -113
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
- package/vibe-math-v3/agent.cordis.yml +72 -6
- package/vibe-math-v3/math-computation.js +130 -41
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/vibe-math-v3.js +422 -126
- package/vibe-math-v4/agent.cordis.yml +44 -8
- package/vibe-math-v4/math-computation.js +130 -41
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/vibe-math-v4.js +196 -43
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +130 -41
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1173 -218
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +80 -12
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +2 -2
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// Usage: node tests/math-computation-shared.test.mjs
|
|
16
16
|
// ============================================================================================
|
|
17
17
|
import { makeFakeHost } from './helpers/math-computation-fake-seam.mjs'
|
|
18
|
-
import { pathToFileURL } from 'node:url'
|
|
18
|
+
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
19
20
|
|
|
20
21
|
// The sensitivity probe (tests/audit-math-computation-sensitivity.mjs) points this at a MUTATED copy
|
|
21
22
|
// of the module pair; the default is the shipped copy in vibe-math-v2/.
|
|
@@ -24,6 +25,97 @@ const MODULE = process.env.MATH_COMPUTATION_MODULE
|
|
|
24
25
|
: new URL('../vibe-math-v2/math-computation.js', import.meta.url).href
|
|
25
26
|
const M = await import(MODULE)
|
|
26
27
|
|
|
28
|
+
// §30 predicates — used by the §30 assertions AND by `--self-probe` case 3, so the shipped proof and the
|
|
29
|
+
// audit cannot drift ("same predicate, broken input"). Unit: descriptor entries carrying the reason.
|
|
30
|
+
const carriesVerifyProvenance = (v) => !!v && v.verify === true && typeof v.verifyReason === 'string' && v.verifyReason.length > 0
|
|
31
|
+
const hasNoVerifySlot = (v) => !!v && !('verify' in v) && !('verifyReason' in v)
|
|
32
|
+
|
|
33
|
+
// ── --self-probe (shipped): prove the §32 evidence assertion can REDDEN, from the tarball alone.
|
|
34
|
+
// It copies the module, removes ONE `fail()` whitelist line, and requires the named §32 assertion
|
|
35
|
+
// to fail in the child. Usage: node tests/math-computation-shared.test.mjs --self-probe
|
|
36
|
+
if (process.argv.includes('--self-probe')) {
|
|
37
|
+
const { mkdtempSync, copyFileSync, readFileSync: rf, writeFileSync: wf, rmSync } = await import('node:fs')
|
|
38
|
+
const { tmpdir } = await import('node:os')
|
|
39
|
+
const { join, dirname } = await import('node:path')
|
|
40
|
+
const { spawnSync } = await import('node:child_process')
|
|
41
|
+
const modPath = process.env.MATH_COMPUTATION_MODULE || fileURLToPath(new URL('../vibe-math-v2/math-computation.js', import.meta.url))
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe-'))
|
|
43
|
+
const enginesSrc = join(dirname(modPath), 'math-engines.js')
|
|
44
|
+
try { copyFileSync(enginesSrc, join(dir, 'math-engines.js')) } catch (e) { /* engines may be resolved elsewhere */ }
|
|
45
|
+
const src = rf(modPath, 'utf8')
|
|
46
|
+
const whitelist = ' if (extra && Array.isArray(extra.absent)) out.absent = extra.absent\n'
|
|
47
|
+
if (src.indexOf(whitelist) === -1) {
|
|
48
|
+
console.log('SELF-PROBE FAIL: the fail() whitelist line for absent[] was not found (the probe cannot mutate what it cannot see)')
|
|
49
|
+
rmSync(dir, { recursive: true, force: true })
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
wf(join(dir, 'math-computation.js'), src.replace(whitelist, ''))
|
|
53
|
+
const child = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir, 'math-computation.js') }) })
|
|
54
|
+
const out = String(child.stdout || '') + String(child.stderr || '')
|
|
55
|
+
const failLines = out.split('\n').filter((l) => /^\s*FAIL\b/.test(l))
|
|
56
|
+
const named = failLines.some((l) => l.indexOf('S2 absence evidence: the failure carries an absent[] list') !== -1)
|
|
57
|
+
const sum = (out.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim()
|
|
58
|
+
console.log((child.status !== 0 && named ? 'SELF-PROBE PASS' : 'SELF-PROBE FAIL') + ': removing ONE fail() whitelist line reddens the named §32 assertion (' + sum + ')')
|
|
59
|
+
if (!named) for (const l of failLines.slice(0, 4)) console.log(' child FAIL line: ' + l.trim().slice(0, 140))
|
|
60
|
+
rmSync(dir, { recursive: true, force: true })
|
|
61
|
+
// case 2 (S4/S5): remove the single `out.installedSoFar = …` assignment and require the NAMED §33
|
|
62
|
+
// assertion in the child's FAIL line (a label found anywhere in the output is NOT a red).
|
|
63
|
+
const s45line = ' out.installedSoFar = results.filter((x) => x.exit === 0).map((x) => x.argv)\n'
|
|
64
|
+
const dir2 = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe2-'))
|
|
65
|
+
const src2 = rf(modPath, 'utf8')
|
|
66
|
+
let ok2 = false
|
|
67
|
+
if (src2.indexOf(s45line) === -1) {
|
|
68
|
+
console.log('SELF-PROBE FAIL: the installedSoFar assignment was not found (cannot mutate what it cannot see)')
|
|
69
|
+
} else {
|
|
70
|
+
try { copyFileSync(join(dirname(modPath), 'math-engines.js'), join(dir2, 'math-engines.js')) } catch (e) { /* optional */ }
|
|
71
|
+
wf(join(dir2, 'math-computation.js'), src2.replace(s45line, ''))
|
|
72
|
+
const child2 = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir2, 'math-computation.js') }) })
|
|
73
|
+
const out2 = String(child2.stdout || '') + String(child2.stderr || '')
|
|
74
|
+
const failLines2 = out2.split('\n').filter((l) => /^\s*FAIL\b/.test(l))
|
|
75
|
+
ok2 = child2.status !== 0 && failLines2.some((l) => l.indexOf('reports what already succeeded (installedSoFar)') !== -1)
|
|
76
|
+
const sum2 = (out2.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim()
|
|
77
|
+
console.log((ok2 ? 'SELF-PROBE PASS' : 'SELF-PROBE FAIL') + ': removing ONE installedSoFar assignment reddens the named §33 assertion (' + sum2 + ')')
|
|
78
|
+
if (!ok2) for (const l of failLines2.slice(0, 4)) console.log(' child FAIL line: ' + l.trim().slice(0, 140))
|
|
79
|
+
}
|
|
80
|
+
rmSync(dir2, { recursive: true, force: true })
|
|
81
|
+
// case 3 (§30, verify provenance): the SAME predicates the §30 assertions use, fed a broken helper.
|
|
82
|
+
// The real `verifyFields(engine)` helper is extracted from the module under test and run against a stub
|
|
83
|
+
// descriptor table; dropping its single reason line (or the no-empty-slot guard) must flip its predicate.
|
|
84
|
+
const src3 = rf(modPath, 'utf8')
|
|
85
|
+
const hStart = src3.indexOf('function verifyFields(engine) {')
|
|
86
|
+
let helper = ''
|
|
87
|
+
if (hStart !== -1) {
|
|
88
|
+
let d = 0, end = hStart
|
|
89
|
+
for (let i = hStart; i < src3.length; i++) { if (src3[i] === '{') d++; else if (src3[i] === '}') { d--; if (d === 0) { end = i + 1; break } } }
|
|
90
|
+
helper = src3.slice(hStart, end)
|
|
91
|
+
}
|
|
92
|
+
const REASON_LINE = " if (typeof d.verifyReason === 'string' && d.verifyReason) out.verifyReason = d.verifyReason\n"
|
|
93
|
+
const GUARD_LINE = ' if (!d.verify) return {}\n'
|
|
94
|
+
let ok3 = false
|
|
95
|
+
if (helper.indexOf(REASON_LINE) === -1 || helper.indexOf(GUARD_LINE) === -1) {
|
|
96
|
+
console.log('SELF-PROBE FAIL: the §30 helper anchors were not found (the probe cannot mutate what it cannot see)')
|
|
97
|
+
} else {
|
|
98
|
+
const stub = { maple: { verify: true, verifyReason: 'CLI spelling varies by Maple version - confirm on a licensed machine' }, python: { license: 'free' } }
|
|
99
|
+
const make = (s) => new Function('MATH_ENGINES', s + '\nreturn verifyFields;')(stub)
|
|
100
|
+
const good = make(helper)
|
|
101
|
+
const broken = make(helper.replace(REASON_LINE, ''))
|
|
102
|
+
const brokenGuard = make(helper.replace(GUARD_LINE, ''))
|
|
103
|
+
const cases3 = [
|
|
104
|
+
['§30 probe carries WHY (shared predicate carriesVerifyProvenance)', carriesVerifyProvenance(good('maple')), carriesVerifyProvenance(broken('maple'))],
|
|
105
|
+
['§30 no empty slot without a declared reason (hasNoVerifySlot)', hasNoVerifySlot(good('python')), hasNoVerifySlot(brokenGuard('python'))],
|
|
106
|
+
]
|
|
107
|
+
let bad3 = 0
|
|
108
|
+
for (const [name, greenNow, redWhenBroken] of cases3) {
|
|
109
|
+
const okc = greenNow === true && redWhenBroken === false
|
|
110
|
+
if (!okc) bad3++
|
|
111
|
+
console.log((okc ? ' ok ' : ' FAIL ') + name + ' :: green-now=' + greenNow + ' broken-goes-red=' + !redWhenBroken)
|
|
112
|
+
}
|
|
113
|
+
ok3 = bad3 === 0
|
|
114
|
+
console.log(' --- §30 verify-provenance predicates: ' + (cases3.length - bad3) + '/' + cases3.length + ' as required')
|
|
115
|
+
}
|
|
116
|
+
process.exit(child.status !== 0 && named && ok2 && ok3 ? 0 : 1)
|
|
117
|
+
}
|
|
118
|
+
|
|
27
119
|
let passed = 0, failed = 0
|
|
28
120
|
const failures = []
|
|
29
121
|
function ok(cond, label, detail) {
|
|
@@ -826,15 +918,43 @@ console.log('-- math_computation shared contract --')
|
|
|
826
918
|
// 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
919
|
// was missing even though it was installed.
|
|
828
920
|
{
|
|
921
|
+
// round-B (F4): the candidate roots are INJECTED through the host seam, so this fixture cannot depend
|
|
922
|
+
// on the ambient machine's ProgramFiles/LOCALAPPDATA (which made it a false red on other layouts).
|
|
923
|
+
const RROOT = 'X:/installs/R'
|
|
829
924
|
const absTree = {
|
|
830
|
-
|
|
831
|
-
'
|
|
925
|
+
[RROOT]: [{ name: 'R-4.6.1', type: 'directory' }],
|
|
926
|
+
[RROOT + '/R-4.6.1/bin']: [{ name: 'Rscript.exe', type: 'file' }, { name: 'R.exe', type: 'file' }],
|
|
832
927
|
}
|
|
833
|
-
const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [] })
|
|
928
|
+
const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [], installRoots: { r: [RROOT] } })
|
|
834
929
|
M.registerMathComputation(h.host)
|
|
835
930
|
const p = await h.call({ op: 'probe', engine: 'r' })
|
|
836
931
|
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 && /
|
|
932
|
+
ok(!!p.engineInfo && /installs\/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) + ')')
|
|
933
|
+
}
|
|
934
|
+
// ── 23b. round-B (F3): the SAME walker covers Octave and Julia default dirs (both layout shapes)
|
|
935
|
+
{
|
|
936
|
+
const OROOT = 'X:/installs/GNU Octave'
|
|
937
|
+
const JROOT = 'X:/home/.juliaup/bin'
|
|
938
|
+
const absTree = {
|
|
939
|
+
[OROOT]: [{ name: 'Octave-9.2.0', type: 'directory' }],
|
|
940
|
+
[OROOT + '/Octave-9.2.0']: [{ name: 'mingw64', type: 'directory' }],
|
|
941
|
+
[OROOT + '/Octave-9.2.0/mingw64/bin']: [{ name: 'octave-cli.exe', type: 'file' }],
|
|
942
|
+
[JROOT]: [{ name: 'julia.exe', type: 'file' }],
|
|
943
|
+
}
|
|
944
|
+
const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [], installRoots: { octave: [OROOT], julia: [JROOT] } })
|
|
945
|
+
M.registerMathComputation(h.host)
|
|
946
|
+
const po = await h.call({ op: 'probe', engine: 'octave' })
|
|
947
|
+
ok(po.ok === true && /Octave-9\.2\.0\/mingw64\/bin\/octave-cli/.test(String(po.engineInfo && po.engineInfo.path)), '★ an Octave installed in its default dir (no PATH) is discovered (versioned + mingw64/bin layout)')
|
|
948
|
+
const pj = await h.call({ op: 'probe', engine: 'julia' })
|
|
949
|
+
ok(pj.ok === true && /\.juliaup\/bin\/julia/.test(String(pj.engineInfo && pj.engineInfo.path)), '★ a juliaup-managed Julia (flat bin dir, no PATH) is discovered by the same walker')
|
|
950
|
+
const pr = await h.call({ op: 'probe' })
|
|
951
|
+
ok((pr.absent || []).some((a) => a.engine === 'r' && a.why === 'not-found-on-this-machine'), 'an engine with no injected roots and no PATH entry is still honestly listed as absent')
|
|
952
|
+
// F3: the per-engine default roots are part of the contract (the release notes claim "installed but not
|
|
953
|
+
// on PATH is found"), so their coverage is asserted directly - not only via an injected fixture.
|
|
954
|
+
for (const e of ['r', 'python', 'octave', 'julia', 'matlab']) {
|
|
955
|
+
ok(typeof M.mathInstallRoots === 'function' && M.mathInstallRoots(e, {}, true).length > 0 && M.mathInstallRoots(e, {}, false).length > 0,
|
|
956
|
+
'★ every engine with a well-defined default install dir declares per-OS roots: ' + e)
|
|
957
|
+
}
|
|
838
958
|
}
|
|
839
959
|
// ── 24. round-9 (real-engine): never suggest an install command that cannot run on this machine
|
|
840
960
|
{
|
|
@@ -894,6 +1014,9 @@ console.log('-- math_computation shared contract --')
|
|
|
894
1014
|
ok(r1.ok === true && r1.attempt === 1 && r1.scriptChanged === false, 'first run of a source file: attempt 1, no change')
|
|
895
1015
|
const rj1 = JSON.parse(h.files.get(String(r1.receipt.json).replace(/\\/g, '/')))
|
|
896
1016
|
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"')
|
|
1017
|
+
// round-B (F6): the DURABLE receipt must record the constraint policy too (not just the response).
|
|
1018
|
+
ok(typeof rj1.versionPolicy === 'string' && /existence-only/.test(rj1.versionPolicy) && Array.isArray(rj1.constraintsNotEnforced),
|
|
1019
|
+
'★ the durable receipt records versionPolicy + constraintsNotEnforced (the promise is auditable evidence)')
|
|
897
1020
|
h.files.set('Problems/x.py', 'print(2)\n')
|
|
898
1021
|
const r2 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/x.py' })
|
|
899
1022
|
ok(r2.ok === true && r2.runId === r1.runId, '★ editing the ORIGINAL source and re-running lands on the SAME archive id')
|
|
@@ -913,6 +1036,18 @@ console.log('-- math_computation shared contract --')
|
|
|
913
1036
|
ok(r3.scriptChanged === false, '★ …and reports scriptChanged:false (no history FOR THIS id)')
|
|
914
1037
|
ok(r3.previousReceipt === null || r3.previousReceipt === undefined, '★ …and has no previousReceipt (expected, not broken change detection)')
|
|
915
1038
|
ok(h.files.get(String(r1.scriptPath).replace(/\\/g, '/')) === 'print(1)\n', 'the earlier attempt is still untouched after that run')
|
|
1039
|
+
// Review round A (F1/F2): the ARCHIVED copy is REACHABLE (projectRel() is purely lexical - it rejects
|
|
1040
|
+
// absolute paths and escaping `..` only). The shipped guidance must therefore say the truth: pointing
|
|
1041
|
+
// mode:'file' at the archived copy yields a NEW archive (new id, attempt 1, no history), flagged by
|
|
1042
|
+
// fileIsArchivedScript + ARCHIVED_SCRIPT_RERUN - it is NOT "a new attempt of the same archive".
|
|
1043
|
+
const archivedPath = String(r1.scriptPath).replace(/\\/g, '/')
|
|
1044
|
+
const r4 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: archivedPath })
|
|
1045
|
+
ok(r4.ok === true, '★ pointing mode:file at the ARCHIVED copy succeeds (the guard is lexical, it does not refuse Computation/)')
|
|
1046
|
+
ok(r4.fileIsArchivedScript === true, '★ the response flags fileIsArchivedScript for an archived source')
|
|
1047
|
+
ok((r4.warnings || []).some((w) => w.code === 'ARCHIVED_SCRIPT_RERUN'), '★ the response warns ARCHIVED_SCRIPT_RERUN (so it cannot read as broken change detection)')
|
|
1048
|
+
ok(r4.runId !== r1.runId && r4.attempt === 1 && r4.scriptChanged === false && (r4.previousReceipt === null || r4.previousReceipt === undefined),
|
|
1049
|
+
'★ …and it really is a NEW archive (new id, attempt 1, no history) - NOT a new attempt of the same archive')
|
|
1050
|
+
ok(h.files.get(archivedPath) === 'print(1)\n', 'the archived original is still byte-identical after that run')
|
|
916
1051
|
}
|
|
917
1052
|
// ── 27. round-9 (N2): a TIMEOUT carries its partial output in BOTH the response and the receipt
|
|
918
1053
|
{
|
|
@@ -925,7 +1060,141 @@ console.log('-- math_computation shared contract --')
|
|
|
925
1060
|
ok(typeof rj.partialStdout === 'string' && typeof rj.partialStderr === 'string', '★ the TIMEOUT receipt JSON carries the capped partial output (as strings)')
|
|
926
1061
|
}
|
|
927
1062
|
|
|
1063
|
+
// ── 28. descriptor sweep: NO spawn may ever carry an unsubstituted argv placeholder
|
|
1064
|
+
// Sweep finding (HIGH, fixed): matlab's scriptArgv EMBEDS the placeholder (`run('<script>')`) while the
|
|
1065
|
+
// module substituted by EXACT element equality, so the literal `run('<script>')` reached the engine and
|
|
1066
|
+
// the archived script never ran. This section walks the whole descriptor table.
|
|
1067
|
+
{
|
|
1068
|
+
const PLACEHOLDER = /<script>|<expr>|<pkgs>|<pkgs\.\.\.>|<probeCode>|<exe>|<pkg>|<cliArgv/
|
|
1069
|
+
for (const name of (M.MATH_PARAM_DEFAULTS.mathEngines || [])) {
|
|
1070
|
+
if (name === 'cli') continue
|
|
1071
|
+
const h = makeFakeHost({ installed: [name], files: {}, licence: true })
|
|
1072
|
+
M.registerMathComputation(h.host)
|
|
1073
|
+
const r = await h.call({ op: 'run', engine: name, mode: 'code', code: 'x=1\n' })
|
|
1074
|
+
const bad = (h.spawns || []).map((s) => (s && s.argv) || []).flat().filter((a) => typeof a === 'string' && PLACEHOLDER.test(a))
|
|
1075
|
+
ok(bad.length === 0, '★ ' + name + ': no spawn carries a literal argv placeholder', JSON.stringify(bad.slice(0, 2)))
|
|
1076
|
+
if (name === 'matlab' && r.ok === true) {
|
|
1077
|
+
const runs = (h.spawns || []).map((s) => s.argv).filter((a) => a.join(' ').indexOf('-batch') !== -1 && a.join(' ').indexOf('disp(version)') === -1)
|
|
1078
|
+
const run = runs.pop() || []
|
|
1079
|
+
ok(run.some((a) => /run\('.*script\.m'\)/.test(a)), '★ matlab: the EMBEDDED placeholder is substituted (run(\'<abs script.m>\'))', JSON.stringify(run))
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ── 29. cli policy is SINGLE-SOURCED (descriptor), not hard-coded in the module
|
|
1085
|
+
// Sweep ruling: `cli.policy.requiresMathMode` / `requiresEngineInList` were declared but never consumed
|
|
1086
|
+
// (the same rule was hard-coded twice), so descriptor and behaviour could drift. The refusals now carry
|
|
1087
|
+
// the DESCRIPTOR value, which is exactly what makes a hard-coded revert observable.
|
|
1088
|
+
{
|
|
1089
|
+
const h = makeFakeHost({ installed: ['python3'], params: { mathMode: 'typed' }, cliCommands: ['python3'] })
|
|
1090
|
+
M.registerMathComputation(h.host)
|
|
1091
|
+
const r = await h.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: 'python3' } })
|
|
1092
|
+
ok(r.ok === false && r.code === 'MATH_REFUSED', 'cli under mathMode=typed is refused')
|
|
1093
|
+
ok(r.next && r.next.reason === 'policy' && r.next.requiresMathMode === 'typed+shell',
|
|
1094
|
+
'★ the refusal carries the DESCRIPTOR policy value (next.requiresMathMode === typed+shell)', JSON.stringify(r.next))
|
|
1095
|
+
const h2 = makeFakeHost({ installed: ['python3'], params: { mathMode: 'typed+shell', mathEngines: ['python', 'r'] }, cliCommands: ['python3'] })
|
|
1096
|
+
M.registerMathComputation(h2.host)
|
|
1097
|
+
const r2 = await h2.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: 'python3' } })
|
|
1098
|
+
ok(r2.ok === false && r2.next && r2.next.reason === 'engine-not-allowed' && r2.next.requiresEngineInList === true,
|
|
1099
|
+
'★ when cli is not in mathEngines the refusal surfaces requiresEngineInList from the descriptor', JSON.stringify(r2.next))
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// ── 30. a commercial template's VERIFY provenance REACHES the user (item 2)
|
|
1103
|
+
// `maple.verify`/`verifyReason` were declared but never reached any response, so a user told "commercial
|
|
1104
|
+
// template needs confirmation" could not see WHY. It is surfaced on the probe result (per-engine entry +
|
|
1105
|
+
// engineInfo) and on the vendor-guidance payloads; engines without a declared reason print no empty slot.
|
|
1106
|
+
{
|
|
1107
|
+
const h = makeFakeHost({ installed: ['maple'], licence: true, files: {} })
|
|
1108
|
+
M.registerMathComputation(h.host)
|
|
1109
|
+
const p = await h.call({ op: 'probe', engine: 'maple' })
|
|
1110
|
+
ok(p.ok === true && !!p.engineInfo && p.engineInfo.verify === true, 'probe reports the verify flag for a commercial template')
|
|
1111
|
+
ok(!!p.engineInfo && typeof p.engineInfo.verifyReason === 'string' && p.engineInfo.verifyReason.length > 0 && /version/i.test(p.engineInfo.verifyReason),
|
|
1112
|
+
'★ probe carries WHY the template needs confirmation (engineInfo.verifyReason)', JSON.stringify(p.engineInfo && p.engineInfo.verifyReason))
|
|
1113
|
+
const entry = (p.engines || []).find((e) => e.name === 'maple')
|
|
1114
|
+
ok(carriesVerifyProvenance(entry), 'the per-engine probe entry carries the same provenance')
|
|
1115
|
+
const hp = makeFakeHost({ installed: ['python3'], files: {} })
|
|
1116
|
+
M.registerMathComputation(hp.host)
|
|
1117
|
+
const pp = await hp.call({ op: 'probe', engine: 'python' })
|
|
1118
|
+
ok(hasNoVerifySlot(pp.engineInfo),
|
|
1119
|
+
'★ an engine without a declared reason prints NO empty slot (assemble-only-when-present)')
|
|
1120
|
+
const hm = makeFakeHost({ installed: [], files: {} })
|
|
1121
|
+
M.registerMathComputation(hm.host)
|
|
1122
|
+
const pm = await hm.call({ op: 'probe', engine: 'maple' })
|
|
1123
|
+
ok(pm.ok === false && !!pm.next && typeof pm.next.verifyReason === 'string' && /version/i.test(pm.next.verifyReason),
|
|
1124
|
+
'★ the not-found guidance also says why the commercial template needs confirmation', JSON.stringify(pm.next && pm.next.verifyReason))
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ── 31. S3 symmetry: EVERY failure kind surfaces partial stdout AND stderr (like the timeout)
|
|
1128
|
+
{
|
|
1129
|
+
const h1 = makeFakeHost({ installed: ['python3'], exit: 2, stdoutBytes: 16 })
|
|
1130
|
+
M.registerMathComputation(h1.host)
|
|
1131
|
+
const r1 = await h1.call({ op: 'run', engine: 'python', mode: 'code', code: 'x=1\n' })
|
|
1132
|
+
ok(r1.ok === false && r1.code === 'MATH_NONZERO_EXIT', 'a non-zero exit reports MATH_NONZERO_EXIT')
|
|
1133
|
+
ok('stdout' in r1 && 'stderr' in r1, '★ S3 symmetry: MATH_NONZERO_EXIT carries BOTH partial stdout and stderr', JSON.stringify({ so: 'stdout' in r1, se: 'stderr' in r1 }))
|
|
1134
|
+
const h2 = makeFakeHost({ installed: ['python3'], argError: 'unknown option', stdoutBytes: 16 })
|
|
1135
|
+
M.registerMathComputation(h2.host)
|
|
1136
|
+
const r2 = await h2.call({ op: 'run', engine: 'python', mode: 'code', code: 'x=1\n' })
|
|
1137
|
+
ok(r2.ok === false && r2.code === 'MATH_ENGINE_BAD_ARGV', 'a usage error reports MATH_ENGINE_BAD_ARGV')
|
|
1138
|
+
ok('stdout' in r2 && 'stderr' in r2, '★ S3 symmetry: MATH_ENGINE_BAD_ARGV carries BOTH partial stdout and stderr')
|
|
1139
|
+
}
|
|
1140
|
+
// ── 32. S2: the failure a user actually hits carries the SAME absence evidence as the success shape
|
|
1141
|
+
{
|
|
1142
|
+
const h = makeFakeHost({ installed: [] })
|
|
1143
|
+
M.registerMathComputation(h.host)
|
|
1144
|
+
const r = await h.call({ op: 'probe' })
|
|
1145
|
+
ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'no resolvable engine reports MATH_ENGINE_NOT_FOUND')
|
|
1146
|
+
ok(Array.isArray(r.configured) && r.configured.length > 0, '★ S2 absence evidence: the failure names the CONFIGURED engines', JSON.stringify(r.configured ?? null))
|
|
1147
|
+
const absent = Array.isArray(r.absent) ? r.absent : []
|
|
1148
|
+
ok(absent.length > 0, '★ S2 absence evidence: the failure carries an absent[] list (not a silent partial)', JSON.stringify(r.absent ?? null))
|
|
1149
|
+
ok(absent.every((a) => a && typeof a.why === 'string' && a.why.length > 0), 'every absent[] entry carries a why', JSON.stringify(absent))
|
|
1150
|
+
}
|
|
928
1151
|
console.log('')
|
|
1152
|
+
// ── 33. S4/S5: install failures are discriminated WITHOUT a new code (frozen interface). Reads the
|
|
1153
|
+
// ENV-RESOLVED module (MATH_COMPUTATION_MODULE), so the --self-probe case 2 above can reach exactly
|
|
1154
|
+
// what these assertions read (the parity-located version could not be reddened by any mutant).
|
|
1155
|
+
{
|
|
1156
|
+
const src = readFileSync(fileURLToPath(new URL(MODULE)), 'utf8')
|
|
1157
|
+
ok(src.indexOf("out.op = 'install'") !== -1, '★ S4/S5 install discrimination: a failed install sets op=install')
|
|
1158
|
+
ok(/out\.timedOut = !!/.test(src), '★ S4/S5 install discrimination: a failed install carries timedOut')
|
|
1159
|
+
ok(/out\.installedSoFar = results\.filter/.test(src), '★ S4/S5 install discrimination: a failed install reports what already succeeded (installedSoFar)')
|
|
1160
|
+
ok(/installedSoFar: commands\.map/.test(src), 'the success path reports installedSoFar too (no special-casing by the caller)')
|
|
1161
|
+
ok(src.indexOf('MATH_INSTALL_FAILED') === -1, 'no 12th failure code was added (MATH_FAILURE_CODES stays frozen)')
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ── 34. S6: the install plan, the run path and the missing-package failure state ONE policy the
|
|
1165
|
+
// same way (behavioural: read from the responses, not from the source text).
|
|
1166
|
+
{
|
|
1167
|
+
const PREFIX = 'version-constraints-are-existence-only: '
|
|
1168
|
+
const h = makeFakeHost({ installed: ['python3'] })
|
|
1169
|
+
M.registerMathComputation(h.host)
|
|
1170
|
+
const plan = await h.call({ op: 'install', packages: ['numpy==1.2.3'] })
|
|
1171
|
+
const planPolicy = String((plan && plan.plan && plan.plan.versionPolicy) || '')
|
|
1172
|
+
ok(plan.ok === true && !!plan.plan, 'S6: an install dry-run returns a plan', JSON.stringify(plan && plan.code))
|
|
1173
|
+
ok(planPolicy.indexOf(PREFIX) === 0, '★ S6: the install plan uses the SAME policy wording as the run path', planPolicy.slice(0, 70))
|
|
1174
|
+
ok(planPolicy.indexOf('numpy==1.2.3') !== -1, '★ S6: the plan names the pinned spec it does not enforce', planPolicy.slice(0, 90))
|
|
1175
|
+
const miss = await h.call({ op: 'run', engine: 'python', mode: 'code', packages: ['numpy==1.2.3'], code: 'x=1\n' })
|
|
1176
|
+
ok(miss.ok === false && miss.code === 'MATH_MISSING_PACKAGES', 'S6: a pinned-but-missing package fails as MATH_MISSING_PACKAGES', JSON.stringify(miss.code))
|
|
1177
|
+
ok(String(miss.versionPolicy || '').indexOf(PREFIX) === 0, '★ S6: the failure path uses the same wording', String(miss.versionPolicy || '').slice(0, 70))
|
|
1178
|
+
ok(String(miss.versionPolicy || '') === planPolicy, '★ S6: plan and failure wording are byte-identical for the same spec', JSON.stringify([planPolicy.slice(0, 40), String(miss.versionPolicy || '').slice(0, 40)]))
|
|
1179
|
+
ok(String(miss.message || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the MATH_MISSING_PACKAGES detail derives the same clause', String(miss.message || '').slice(0, 70))
|
|
1180
|
+
const badSpec = M.parsePackageSpec('numpy@1.2')
|
|
1181
|
+
ok(badSpec && badSpec.ok === false && String(badSpec.problem || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the unsupported-version-syntax message derives the same clause', String((badSpec && badSpec.problem) || '').slice(0, 80))
|
|
1182
|
+
ok(String(planPolicy || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the policy FIELD itself contains that clause (one literal, three surfaces)', planPolicy.slice(0, 80))
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// ── 35. S6 derivation is LOAD-BEARING (found by mutant case 2): "the message contains the clause" also
|
|
1186
|
+
// passes when the message RE-INLINES the same words, so the real checks are (a) the clause exists as
|
|
1187
|
+
// exactly ONE literal in the module and (b) the two human messages are built from the constant.
|
|
1188
|
+
{
|
|
1189
|
+
const src = readFileSync(fileURLToPath(new URL(MODULE)), 'utf8')
|
|
1190
|
+
const clause = '只检查是否存在;版本求解交给包管理器'
|
|
1191
|
+
const occurrences = src.split(clause).length - 1
|
|
1192
|
+
ok(occurrences === 1, '★ S6: the policy clause exists as exactly ONE literal in the module (a re-inlined message is caught)', 'occurrences=' + occurrences)
|
|
1193
|
+
ok(/MATH_MISSING_PACKAGES[\s\S]{0,160}MATH_VERSION_POLICY_CLAUSE/.test(src), '★ S6: the missing-package detail is built from the clause constant', 'MATH_MISSING_PACKAGES … MATH_VERSION_POLICY_CLAUSE')
|
|
1194
|
+
ok(/unsupported version syntax[\s\S]{0,160}MATH_VERSION_POLICY_CLAUSE/.test(src), '★ S6: the unsupported-version-syntax message is built from the clause constant')
|
|
1195
|
+
ok(/const MATH_VERSION_POLICY = .*MATH_VERSION_POLICY_CLAUSE/.test(src), '★ S6: the long policy sentence is derived from the clause')
|
|
1196
|
+
}
|
|
1197
|
+
|
|
929
1198
|
console.log('=== MATH COMPUTATION SHARED: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
930
1199
|
if (failures.length) for (const f of failures) console.error(' - ' + f)
|
|
931
1200
|
process.exit(failed === 0 ? 0 : 1)
|