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.
Files changed (112) hide show
  1. package/README.en.md +11 -10
  2. package/README.md +13 -12
  3. package/cordis.patch.yml +112 -16
  4. package/docs/AUDIT-CHECKLIST.md +229 -3
  5. package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
  6. package/docs/final-paper.md +2 -2
  7. package/docs/math-computation.md +13 -6
  8. package/docs/parameter-schema.md +37 -0
  9. package/docs/release-notes/README.en.md +1 -0
  10. package/docs/release-notes/README.md +1 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  12. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  13. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  15. package/docs/release-notes/TEMPLATE.md +6 -0
  16. package/docs/status-report-fields.md +167 -0
  17. package/docs/test-timing.md +8 -5
  18. package/installer.js +27 -17
  19. package/package.json +250 -203
  20. package/prompt-corpus-persona/persona-corpus.json +9 -9
  21. package/prompt-corpus-persona/persona-corpus.md +225 -33
  22. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  23. package/prompt-corpus-v2/formal-verify-v2.md +2287 -283
  24. package/prompt-corpus-v3/formal-verify-v3.json +105 -105
  25. package/prompt-corpus-v3/formal-verify-v3.md +213 -143
  26. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  27. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  28. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  29. package/prompt-corpus-v5/prompt-corpus-v5.md +451 -207
  30. package/scripts/update-doc-counts.mjs +71 -0
  31. package/tests/audit-artifact-docs.mjs +72 -0
  32. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  33. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  34. package/tests/audit-installer-compat.test.mjs +52 -4
  35. package/tests/audit-installer-policy.test.mjs +41 -8
  36. package/tests/audit-math-computation-contract.mjs +64 -8
  37. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  38. package/tests/audit-math-computation-parity.mjs +164 -3
  39. package/tests/audit-math-computation-sensitivity.mjs +4 -2
  40. package/tests/audit-package-membership.mjs +57 -0
  41. package/tests/audit-package-membership.mutants.mjs +28 -0
  42. package/tests/audit-participant-set-parity.mjs +160 -0
  43. package/tests/audit-path-discipline.mjs +83 -0
  44. package/tests/audit-path-discipline.mutants.mjs +51 -0
  45. package/tests/audit-persona-sensitivity.mjs +14 -0
  46. package/tests/audit-persona-surface.mutants.mjs +69 -0
  47. package/tests/audit-persona-surface.test.mjs +63 -22
  48. package/tests/audit-prompt-duplication.mjs +143 -0
  49. package/tests/audit-prompt-duplication.mutants.mjs +50 -0
  50. package/tests/audit-prompt-invariants.mjs +43 -5
  51. package/tests/audit-readme-bilingual.test.mjs +152 -0
  52. package/tests/audit-readme-counts.mjs +95 -0
  53. package/tests/audit-readme-counts.mutants.mjs +52 -0
  54. package/tests/audit-registration.mjs +78 -0
  55. package/tests/audit-status-report-fields.mjs +172 -0
  56. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  57. package/tests/audit-v3-registration-parity.mjs +156 -0
  58. package/tests/audit-v5-integrity.mjs +300 -13
  59. package/tests/audit-v5-lean-abstention.mjs +392 -0
  60. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  61. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  62. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  63. package/tests/audit-v5-sensitivity.mjs +12 -7
  64. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  65. package/tests/e2e-v5-round2.test.mjs +458 -13
  66. package/tests/formal-verify-v2.test.mjs +91 -43
  67. package/tests/formal-verify-v3.mutants.mjs +117 -0
  68. package/tests/formal-verify-v3.test.mjs +85 -39
  69. package/tests/formal-verify-v4.mutants.mjs +152 -0
  70. package/tests/formal-verify-v4.test.mjs +541 -9
  71. package/tests/formal-verify-v5.test.mjs +68 -9
  72. package/tests/helpers/doc-counts-format.mjs +41 -0
  73. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  74. package/tests/math-computation-a1.mutants.mjs +72 -0
  75. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  76. package/tests/math-computation-discovery.mutants.mjs +76 -0
  77. package/tests/math-computation-shared.test.mjs +274 -5
  78. package/tests/math-computation-v2.test.mjs +767 -0
  79. package/tests/math-computation-v3.test.mjs +759 -0
  80. package/tests/math-computation-v4.mutants.mjs +133 -0
  81. package/tests/math-computation-v4.test.mjs +739 -0
  82. package/tests/prompt-v5-integrity.test.mjs +13 -2
  83. package/tests/run-tests.mjs +139 -9
  84. package/tests/run-tests.mutants.mjs +70 -0
  85. package/tests/selfdrive-v5.mjs +53 -1
  86. package/tests/selfdrive-v5.mutants.mjs +132 -0
  87. package/tests/v2-fix-probes.mutants.mjs +100 -0
  88. package/tests/v2-fix-probes.test.mjs +1039 -0
  89. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  90. package/tests/v3-fix-probes.mutants.mjs +95 -0
  91. package/tests/v3-fix-probes.test.mjs +891 -0
  92. package/tests/v5-institute-fixes.mutants.mjs +190 -0
  93. package/vibe-math-v2/agent.cordis.yml +62 -6
  94. package/vibe-math-v2/math-computation.js +130 -41
  95. package/vibe-math-v2/math-engines.js +12 -12
  96. package/vibe-math-v2/vibe-math-v2.js +388 -113
  97. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
  98. package/vibe-math-v3/agent.cordis.yml +72 -6
  99. package/vibe-math-v3/math-computation.js +130 -41
  100. package/vibe-math-v3/math-engines.js +12 -12
  101. package/vibe-math-v3/vibe-math-v3.js +422 -126
  102. package/vibe-math-v4/agent.cordis.yml +44 -8
  103. package/vibe-math-v4/math-computation.js +130 -41
  104. package/vibe-math-v4/math-engines.js +12 -12
  105. package/vibe-math-v4/vibe-math-v4.js +196 -43
  106. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
  107. package/vibe-math-v5/agent.cordis.yml +46 -12
  108. package/vibe-math-v5/math-computation.js +130 -41
  109. package/vibe-math-v5/math-engines.js +12 -12
  110. package/vibe-math-v5/vibe-math-v5.js +1173 -218
  111. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +80 -12
  112. 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
- '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' }],
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 && /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) + ')')
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)