dsh-vibe-math 2.7.2 → 2.8.0

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 (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -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,172 @@ 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').replace(/\r\n/g, '\n')
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').replace(/\r\n/g, '\n')
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').replace(/\r\n/g, '\n')
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
+ // case 4 (task-1): mislabel the bundled-runtime stage -> the named foundVia assertion must redden.
117
+ // NOTE: the self-probe's fs imports are ALIASED at the top of this block (`rf` = readFileSync,
118
+ // `wf` = writeFileSync); using the unaliased names here is a ReferenceError, not a probe failure.
119
+ let ok4 = false
120
+ {
121
+ const dir4 = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe4-'))
122
+ try {
123
+ copyFileSync(join(dirname(modPath), 'math-engines.js'), join(dir4, 'math-engines.js'))
124
+ const src4 = rf(modPath, 'utf8').replace(/\r\n/g, '\n')
125
+ const FROM4 = "via: 'host-runtime'"
126
+ const n4 = src4.split(FROM4).length - 1
127
+ if (n4 !== 1) {
128
+ console.log('SELF-PROBE FAIL: the stage-2 via anchor occurs ' + n4 + ' times (expected exactly 1)')
129
+ } else {
130
+ wf(join(dir4, 'math-computation.js'), src4.split(FROM4).join("via: 'path'"), 'utf8')
131
+ const child4 = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { encoding: 'utf8', maxBuffer: 1 << 26,
132
+ env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir4, 'math-computation.js') }) })
133
+ const out4 = String(child4.stdout || '') + String(child4.stderr || '')
134
+ const failLines4 = out4.split('\n').filter((l) => /^\s*FAIL\b/.test(l))
135
+ const named4 = failLines4.some((l) => l.indexOf('a bundled runtime tree is reported as host-runtime') !== -1)
136
+ const summary4 = out4.split('\n').filter((l) => /passed,/.test(l)).slice(-1)[0] || 'no summary'
137
+ ok4 = child4.status !== 0 && named4
138
+ console.log((ok4 ? 'SELF-PROBE PASS' : 'SELF-PROBE FAIL') + ': mislabelling the bundled-runtime stage reddens the named foundVia assertion (' + summary4 + ')')
139
+ if (!ok4) for (const l of failLines4.slice(0, 4)) console.log(' child FAIL line: ' + l.trim().slice(0, 140))
140
+ }
141
+ } finally { rmSync(dir4, { recursive: true, force: true }) }
142
+ }
143
+ // case 5 (task-2 item 3): restore the old `injected || …` form -> the named empty-injection
144
+ // assertion must redden (a JS empty array is truthy, so the per-OS table was suppressed).
145
+ let ok5 = false
146
+ {
147
+ const dir5 = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe5-'))
148
+ try {
149
+ copyFileSync(join(dirname(modPath), 'math-engines.js'), join(dir5, 'math-engines.js'))
150
+ const src5 = rf(modPath, 'utf8').replace(/\r\n/g, '\n')
151
+ const FIXED5 = 'const roots = (Array.isArray(injected) && injected.length) ? injected : mathInstallRoots(engineName, env, win)'
152
+ const n5 = src5.split(FIXED5).length - 1
153
+ if (n5 !== 1) console.log('SELF-PROBE FAIL: the empty-array guard occurs ' + n5 + ' times (expected exactly 1)')
154
+ else {
155
+ wf(join(dir5, 'math-computation.js'), src5.split(FIXED5).join('const roots = injected || mathInstallRoots(engineName, env, win)'), 'utf8')
156
+ const child5 = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { encoding: 'utf8', maxBuffer: 1 << 26,
157
+ env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir5, 'math-computation.js') }) })
158
+ const out5 = String(child5.stdout || '') + String(child5.stderr || '')
159
+ const failLines5 = out5.split('\n').filter((l) => /^\s*FAIL\b/.test(l))
160
+ ok5 = child5.status !== 0 && failLines5.some((l) => l.indexOf('task-2 empty-injection') !== -1)
161
+ const sum5 = (out5.split('\n').filter((l) => /passed,/.test(l)).slice(-1)[0] || 'no summary').trim()
162
+ console.log((ok5 ? 'SELF-PROBE PASS' : 'SELF-PROBE FAIL') + ': restoring the old injected-truthiness form reddens the named empty-injection assertion (' + sum5 + ')')
163
+ if (!ok5) for (const l of failLines5.slice(0, 4)) console.log(' child FAIL line: ' + l.trim().slice(0, 140))
164
+ }
165
+ } finally { rmSync(dir5, { recursive: true, force: true }) }
166
+ }
167
+ // case 6 (task-2 built-in route): empty the built-in root table -> the named built-in-route assertion
168
+ // must redden (§23c injects NO roots, so the built-in table is the only stage that can find it).
169
+ let ok6 = false
170
+ {
171
+ const dir6 = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe6-'))
172
+ try {
173
+ copyFileSync(join(dirname(modPath), 'math-engines.js'), join(dir6, 'math-engines.js'))
174
+ const src6 = rf(modPath, 'utf8').replace(/\r\n/g, '\n')
175
+ const TABLE6 = 'return (table[engineName] || []).filter((x) => typeof x === \'string\' && x)'
176
+ const n6 = src6.split(TABLE6).length - 1
177
+ if (n6 !== 1) console.log('SELF-PROBE FAIL: the built-in root-table return occurs ' + n6 + ' times (expected exactly 1)')
178
+ else {
179
+ wf(join(dir6, 'math-computation.js'), src6.split(TABLE6).join('return []'), 'utf8')
180
+ const child6 = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { encoding: 'utf8', maxBuffer: 1 << 26,
181
+ env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir6, 'math-computation.js') }) })
182
+ const out6 = String(child6.stdout || '') + String(child6.stderr || '')
183
+ const failLines6 = out6.split('\n').filter((l) => /^\s*FAIL\b/.test(l))
184
+ ok6 = child6.status !== 0 && failLines6.some((l) => l.indexOf('task-2 built-in route') !== -1)
185
+ const sum6 = (out6.split('\n').filter((l) => /passed,/.test(l)).slice(-1)[0] || 'no summary').trim()
186
+ console.log((ok6 ? 'SELF-PROBE PASS' : 'SELF-PROBE FAIL') + ': emptying the built-in root table reddens the named built-in-route assertion (' + sum6 + ')')
187
+ if (!ok6) for (const l of failLines6.slice(0, 4)) console.log(' child FAIL line: ' + l.trim().slice(0, 140))
188
+ }
189
+ } finally { rmSync(dir6, { recursive: true, force: true }) }
190
+ }
191
+ process.exit(child.status !== 0 && named && ok2 && ok3 && ok4 && ok5 && ok6 ? 0 : 1)
192
+ }
193
+
27
194
  let passed = 0, failed = 0
28
195
  const failures = []
29
196
  function ok(cond, label, detail) {
@@ -186,7 +353,7 @@ console.log('-- math_computation shared contract --')
186
353
  ok(r.cliScriptAppended === true, 'the return shell flags cliScriptAppended')
187
354
  ok(r.engine === 'cli:/fake/bin/mytool' && r.engineInfo.name === 'cli:/fake/bin/mytool', 'cli is labelled cli:<command> in the shell and engineInfo')
188
355
  const rj = JSON.parse(state.file(r.receipt.json))
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')
356
+ ok(rj.engine.name === 'cli:/fake/bin/mytool' && rj.engine.source === 'cli' && rj.engine.foundVia === 'cli' && rj.cli && rj.cli.argv.length === 2, 'cli receipt records cli:<command>, source=cli, foundVia=cli and the USER argv')
190
357
  ok(rj.cli.scriptAppended === true && rj.argv[rj.argv.length - 1] === r.argv[3], 'the receipt records scriptAppended and the executed argv')
191
358
  })
192
359
  await withHost({ params: { mathMode: 'typed' } }, async (state) => {
@@ -613,6 +780,7 @@ console.log('-- math_computation shared contract --')
613
780
  const p = await hb.call({ op: 'probe', engine: 'python' })
614
781
  ok(p.ok === true && p.engine === 'python' && p.engineInfo.name === 'python', 'a bundled DSH runtime is found when PATH has nothing')
615
782
  ok(/dsh-runtimes\/dsh-primary-runtime\/dependencies\/python\/python/.test(p.engineInfo.path), 'the discovered path comes from the generically globbed runtime tree')
783
+ ok(p.engineInfo.foundVia === 'host-runtime', '★ a bundled runtime tree is reported as host-runtime (foundVia, not the coarse source)')
616
784
  const r = await hb.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
617
785
  ok(r.ok === true && !!r.engineInfo.version, 'a run on the bundled runtime succeeds and reports its version')
618
786
  ok(!r.next, 'a found bundled runtime emits NO install guidance')
@@ -826,15 +994,84 @@ console.log('-- math_computation shared contract --')
826
994
  // 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
995
  // was missing even though it was installed.
828
996
  {
997
+ // round-B (F4): the candidate roots are INJECTED through the host seam, so this fixture cannot depend
998
+ // on the ambient machine's ProgramFiles/LOCALAPPDATA (which made it a false red on other layouts).
999
+ const RROOT = 'X:/installs/R'
829
1000
  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' }],
1001
+ [RROOT]: [{ name: 'R-4.6.1', type: 'directory' }],
1002
+ [RROOT + '/R-4.6.1/bin']: [{ name: 'Rscript.exe', type: 'file' }, { name: 'R.exe', type: 'file' }],
832
1003
  }
833
- const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [] })
1004
+ const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [], installRoots: { r: [RROOT] } })
834
1005
  M.registerMathComputation(h.host)
835
1006
  const p = await h.call({ op: 'probe', engine: 'r' })
836
1007
  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) + ')')
1008
+ 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) + ')')
1009
+ ok(p.engineInfo.foundVia === 'known-install', '★ a known install dir is reported as known-install (foundVia)')
1010
+ }
1011
+ // ── 23b. round-B (F3): the SAME walker covers Octave and Julia default dirs (both layout shapes)
1012
+ {
1013
+ const OROOT = 'X:/installs/GNU Octave'
1014
+ const JROOT = 'X:/home/.juliaup/bin'
1015
+ const absTree = {
1016
+ [OROOT]: [{ name: 'Octave-9.2.0', type: 'directory' }],
1017
+ [OROOT + '/Octave-9.2.0']: [{ name: 'mingw64', type: 'directory' }],
1018
+ [OROOT + '/Octave-9.2.0/mingw64/bin']: [{ name: 'octave-cli.exe', type: 'file' }],
1019
+ [JROOT]: [{ name: 'julia.exe', type: 'file' }],
1020
+ }
1021
+ const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [], installRoots: { octave: [OROOT], julia: [JROOT] } })
1022
+ M.registerMathComputation(h.host)
1023
+ const po = await h.call({ op: 'probe', engine: 'octave' })
1024
+ 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)')
1025
+ const pj = await h.call({ op: 'probe', engine: 'julia' })
1026
+ 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')
1027
+ const pr = await h.call({ op: 'probe' })
1028
+ 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')
1029
+ // F3: the per-engine default roots are part of the contract (the release notes claim "installed but not
1030
+ // on PATH is found"), so their coverage is asserted directly - not only via an injected fixture.
1031
+ for (const e of ['r', 'python', 'octave', 'julia', 'matlab']) {
1032
+ ok(typeof M.mathInstallRoots === 'function' && M.mathInstallRoots(e, {}, true).length > 0 && M.mathInstallRoots(e, {}, false).length > 0,
1033
+ '★ every engine with a well-defined default install dir declares per-OS roots: ' + e)
1034
+ }
1035
+ }
1036
+ // ── 23c (task-2): the route a REAL machine takes — the BUILT-IN per-OS table + a REAL filesystem
1037
+ // listing. §23/§23b above both INJECT the candidate roots; this section has NO `installRoots` at all
1038
+ // and uses a real absolute temp dir laid out like <tmp>/R/<version>/bin. `knownInstallCandidates`
1039
+ // reads process.env at CALL time, so pointing `ProgramFiles` at the temp dir is enough (no seam).
1040
+ async function task2ProbeBuiltInRoots(opts) {
1041
+ const { mkdtempSync, mkdirSync, writeFileSync, readdirSync, rmSync } = await import('node:fs')
1042
+ const { tmpdir } = await import('node:os')
1043
+ const { join } = await import('node:path')
1044
+ const tmp = mkdtempSync(join(tmpdir(), 'mc-known-install-'))
1045
+ const prevPF = process.env.ProgramFiles
1046
+ try {
1047
+ mkdirSync(join(tmp, 'R', 'R-4.6.1', 'bin'), { recursive: true })
1048
+ writeFileSync(join(tmp, 'R', 'R-4.6.1', 'bin', 'Rscript.exe'), '')
1049
+ process.env.ProgramFiles = tmp
1050
+ const h = makeFakeHost(Object.assign({ installed: [], runtimeRoots: [] }, opts || {}))
1051
+ h.host.listDirAbs = async (abs) => {
1052
+ try { return readdirSync(abs, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) } catch (e) { return [] }
1053
+ }
1054
+ M.registerMathComputation(h.host)
1055
+ const p = await h.call({ op: 'probe', engine: 'r' })
1056
+ return { p, tmp }
1057
+ } finally {
1058
+ if (prevPF === undefined) delete process.env.ProgramFiles; else process.env.ProgramFiles = prevPF
1059
+ rmSync(tmp, { recursive: true, force: true })
1060
+ }
1061
+ }
1062
+ {
1063
+ const { p, tmp } = await task2ProbeBuiltInRoots(null)
1064
+ ok(p.ok === true && p.engine === 'r', '★ task-2 built-in route: an engine under the BUILT-IN per-OS root is discovered with a REAL fs listing (no installRoots injection)')
1065
+ ok(!!p.engineInfo && /R-4\.6\.1\/bin\/Rscript/.test(String(p.engineInfo.path)) && String(p.engineInfo.path).replace(/\\/g, '/').indexOf(tmp.replace(/\\/g, '/')) !== -1,
1066
+ '★ task-2 built-in route: the discovered path is the versioned install dir inside the temp root (' + String(p.engineInfo && p.engineInfo.path) + ')')
1067
+ ok(!!p.engineInfo && p.engineInfo.foundVia === 'known-install', '★ task-2 built-in route: foundVia names the known-install stage (' + String(p.engineInfo && p.engineInfo.foundVia) + ')')
1068
+ }
1069
+ // ── 23d (task-2 item 3): an EMPTY injected array must not suppress the built-in table. A JS empty array
1070
+ // is TRUTHY, so `injected || mathInstallRoots(...)` silently disabled the whole per-OS stage.
1071
+ {
1072
+ const { p } = await task2ProbeBuiltInRoots({ installRoots: { r: [] } })
1073
+ ok(p.ok === true && !!p.engineInfo && p.engineInfo.foundVia === 'known-install',
1074
+ '★ task-2 empty-injection: a host that answers [] still falls back to the BUILT-IN roots (a JS empty array is truthy)')
838
1075
  }
839
1076
  // ── 24. round-9 (real-engine): never suggest an install command that cannot run on this machine
840
1077
  {
@@ -894,6 +1131,9 @@ console.log('-- math_computation shared contract --')
894
1131
  ok(r1.ok === true && r1.attempt === 1 && r1.scriptChanged === false, 'first run of a source file: attempt 1, no change')
895
1132
  const rj1 = JSON.parse(h.files.get(String(r1.receipt.json).replace(/\\/g, '/')))
896
1133
  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"')
1134
+ // round-B (F6): the DURABLE receipt must record the constraint policy too (not just the response).
1135
+ ok(typeof rj1.versionPolicy === 'string' && /existence-only/.test(rj1.versionPolicy) && Array.isArray(rj1.constraintsNotEnforced),
1136
+ '★ the durable receipt records versionPolicy + constraintsNotEnforced (the promise is auditable evidence)')
897
1137
  h.files.set('Problems/x.py', 'print(2)\n')
898
1138
  const r2 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/x.py' })
899
1139
  ok(r2.ok === true && r2.runId === r1.runId, '★ editing the ORIGINAL source and re-running lands on the SAME archive id')
@@ -913,6 +1153,18 @@ console.log('-- math_computation shared contract --')
913
1153
  ok(r3.scriptChanged === false, '★ …and reports scriptChanged:false (no history FOR THIS id)')
914
1154
  ok(r3.previousReceipt === null || r3.previousReceipt === undefined, '★ …and has no previousReceipt (expected, not broken change detection)')
915
1155
  ok(h.files.get(String(r1.scriptPath).replace(/\\/g, '/')) === 'print(1)\n', 'the earlier attempt is still untouched after that run')
1156
+ // Review round A (F1/F2): the ARCHIVED copy is REACHABLE (projectRel() is purely lexical - it rejects
1157
+ // absolute paths and escaping `..` only). The shipped guidance must therefore say the truth: pointing
1158
+ // mode:'file' at the archived copy yields a NEW archive (new id, attempt 1, no history), flagged by
1159
+ // fileIsArchivedScript + ARCHIVED_SCRIPT_RERUN - it is NOT "a new attempt of the same archive".
1160
+ const archivedPath = String(r1.scriptPath).replace(/\\/g, '/')
1161
+ const r4 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: archivedPath })
1162
+ ok(r4.ok === true, '★ pointing mode:file at the ARCHIVED copy succeeds (the guard is lexical, it does not refuse Computation/)')
1163
+ ok(r4.fileIsArchivedScript === true, '★ the response flags fileIsArchivedScript for an archived source')
1164
+ ok((r4.warnings || []).some((w) => w.code === 'ARCHIVED_SCRIPT_RERUN'), '★ the response warns ARCHIVED_SCRIPT_RERUN (so it cannot read as broken change detection)')
1165
+ ok(r4.runId !== r1.runId && r4.attempt === 1 && r4.scriptChanged === false && (r4.previousReceipt === null || r4.previousReceipt === undefined),
1166
+ '★ …and it really is a NEW archive (new id, attempt 1, no history) - NOT a new attempt of the same archive')
1167
+ ok(h.files.get(archivedPath) === 'print(1)\n', 'the archived original is still byte-identical after that run')
916
1168
  }
917
1169
  // ── 27. round-9 (N2): a TIMEOUT carries its partial output in BOTH the response and the receipt
918
1170
  {
@@ -925,7 +1177,141 @@ console.log('-- math_computation shared contract --')
925
1177
  ok(typeof rj.partialStdout === 'string' && typeof rj.partialStderr === 'string', '★ the TIMEOUT receipt JSON carries the capped partial output (as strings)')
926
1178
  }
927
1179
 
1180
+ // ── 28. descriptor sweep: NO spawn may ever carry an unsubstituted argv placeholder
1181
+ // Sweep finding (HIGH, fixed): matlab's scriptArgv EMBEDS the placeholder (`run('<script>')`) while the
1182
+ // module substituted by EXACT element equality, so the literal `run('<script>')` reached the engine and
1183
+ // the archived script never ran. This section walks the whole descriptor table.
1184
+ {
1185
+ const PLACEHOLDER = /<script>|<expr>|<pkgs>|<pkgs\.\.\.>|<probeCode>|<exe>|<pkg>|<cliArgv/
1186
+ for (const name of (M.MATH_PARAM_DEFAULTS.mathEngines || [])) {
1187
+ if (name === 'cli') continue
1188
+ const h = makeFakeHost({ installed: [name], files: {}, licence: true })
1189
+ M.registerMathComputation(h.host)
1190
+ const r = await h.call({ op: 'run', engine: name, mode: 'code', code: 'x=1\n' })
1191
+ const bad = (h.spawns || []).map((s) => (s && s.argv) || []).flat().filter((a) => typeof a === 'string' && PLACEHOLDER.test(a))
1192
+ ok(bad.length === 0, '★ ' + name + ': no spawn carries a literal argv placeholder', JSON.stringify(bad.slice(0, 2)))
1193
+ if (name === 'matlab' && r.ok === true) {
1194
+ const runs = (h.spawns || []).map((s) => s.argv).filter((a) => a.join(' ').indexOf('-batch') !== -1 && a.join(' ').indexOf('disp(version)') === -1)
1195
+ const run = runs.pop() || []
1196
+ ok(run.some((a) => /run\('.*script\.m'\)/.test(a)), '★ matlab: the EMBEDDED placeholder is substituted (run(\'<abs script.m>\'))', JSON.stringify(run))
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ // ── 29. cli policy is SINGLE-SOURCED (descriptor), not hard-coded in the module
1202
+ // Sweep ruling: `cli.policy.requiresMathMode` / `requiresEngineInList` were declared but never consumed
1203
+ // (the same rule was hard-coded twice), so descriptor and behaviour could drift. The refusals now carry
1204
+ // the DESCRIPTOR value, which is exactly what makes a hard-coded revert observable.
1205
+ {
1206
+ const h = makeFakeHost({ installed: ['python3'], params: { mathMode: 'typed' }, cliCommands: ['python3'] })
1207
+ M.registerMathComputation(h.host)
1208
+ const r = await h.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: 'python3' } })
1209
+ ok(r.ok === false && r.code === 'MATH_REFUSED', 'cli under mathMode=typed is refused')
1210
+ ok(r.next && r.next.reason === 'policy' && r.next.requiresMathMode === 'typed+shell',
1211
+ '★ the refusal carries the DESCRIPTOR policy value (next.requiresMathMode === typed+shell)', JSON.stringify(r.next))
1212
+ const h2 = makeFakeHost({ installed: ['python3'], params: { mathMode: 'typed+shell', mathEngines: ['python', 'r'] }, cliCommands: ['python3'] })
1213
+ M.registerMathComputation(h2.host)
1214
+ const r2 = await h2.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: 'python3' } })
1215
+ ok(r2.ok === false && r2.next && r2.next.reason === 'engine-not-allowed' && r2.next.requiresEngineInList === true,
1216
+ '★ when cli is not in mathEngines the refusal surfaces requiresEngineInList from the descriptor', JSON.stringify(r2.next))
1217
+ }
1218
+
1219
+ // ── 30. a commercial template's VERIFY provenance REACHES the user (item 2)
1220
+ // `maple.verify`/`verifyReason` were declared but never reached any response, so a user told "commercial
1221
+ // template needs confirmation" could not see WHY. It is surfaced on the probe result (per-engine entry +
1222
+ // engineInfo) and on the vendor-guidance payloads; engines without a declared reason print no empty slot.
1223
+ {
1224
+ const h = makeFakeHost({ installed: ['maple'], licence: true, files: {} })
1225
+ M.registerMathComputation(h.host)
1226
+ const p = await h.call({ op: 'probe', engine: 'maple' })
1227
+ ok(p.ok === true && !!p.engineInfo && p.engineInfo.verify === true, 'probe reports the verify flag for a commercial template')
1228
+ ok(!!p.engineInfo && typeof p.engineInfo.verifyReason === 'string' && p.engineInfo.verifyReason.length > 0 && /version/i.test(p.engineInfo.verifyReason),
1229
+ '★ probe carries WHY the template needs confirmation (engineInfo.verifyReason)', JSON.stringify(p.engineInfo && p.engineInfo.verifyReason))
1230
+ const entry = (p.engines || []).find((e) => e.name === 'maple')
1231
+ ok(carriesVerifyProvenance(entry), 'the per-engine probe entry carries the same provenance')
1232
+ const hp = makeFakeHost({ installed: ['python3'], files: {} })
1233
+ M.registerMathComputation(hp.host)
1234
+ const pp = await hp.call({ op: 'probe', engine: 'python' })
1235
+ ok(hasNoVerifySlot(pp.engineInfo),
1236
+ '★ an engine without a declared reason prints NO empty slot (assemble-only-when-present)')
1237
+ const hm = makeFakeHost({ installed: [], files: {} })
1238
+ M.registerMathComputation(hm.host)
1239
+ const pm = await hm.call({ op: 'probe', engine: 'maple' })
1240
+ ok(pm.ok === false && !!pm.next && typeof pm.next.verifyReason === 'string' && /version/i.test(pm.next.verifyReason),
1241
+ '★ the not-found guidance also says why the commercial template needs confirmation', JSON.stringify(pm.next && pm.next.verifyReason))
1242
+ }
1243
+
1244
+ // ── 31. S3 symmetry: EVERY failure kind surfaces partial stdout AND stderr (like the timeout)
1245
+ {
1246
+ const h1 = makeFakeHost({ installed: ['python3'], exit: 2, stdoutBytes: 16 })
1247
+ M.registerMathComputation(h1.host)
1248
+ const r1 = await h1.call({ op: 'run', engine: 'python', mode: 'code', code: 'x=1\n' })
1249
+ ok(r1.ok === false && r1.code === 'MATH_NONZERO_EXIT', 'a non-zero exit reports MATH_NONZERO_EXIT')
1250
+ 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 }))
1251
+ const h2 = makeFakeHost({ installed: ['python3'], argError: 'unknown option', stdoutBytes: 16 })
1252
+ M.registerMathComputation(h2.host)
1253
+ const r2 = await h2.call({ op: 'run', engine: 'python', mode: 'code', code: 'x=1\n' })
1254
+ ok(r2.ok === false && r2.code === 'MATH_ENGINE_BAD_ARGV', 'a usage error reports MATH_ENGINE_BAD_ARGV')
1255
+ ok('stdout' in r2 && 'stderr' in r2, '★ S3 symmetry: MATH_ENGINE_BAD_ARGV carries BOTH partial stdout and stderr')
1256
+ }
1257
+ // ── 32. S2: the failure a user actually hits carries the SAME absence evidence as the success shape
1258
+ {
1259
+ const h = makeFakeHost({ installed: [] })
1260
+ M.registerMathComputation(h.host)
1261
+ const r = await h.call({ op: 'probe' })
1262
+ ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'no resolvable engine reports MATH_ENGINE_NOT_FOUND')
1263
+ ok(Array.isArray(r.configured) && r.configured.length > 0, '★ S2 absence evidence: the failure names the CONFIGURED engines', JSON.stringify(r.configured ?? null))
1264
+ const absent = Array.isArray(r.absent) ? r.absent : []
1265
+ ok(absent.length > 0, '★ S2 absence evidence: the failure carries an absent[] list (not a silent partial)', JSON.stringify(r.absent ?? null))
1266
+ ok(absent.every((a) => a && typeof a.why === 'string' && a.why.length > 0), 'every absent[] entry carries a why', JSON.stringify(absent))
1267
+ }
928
1268
  console.log('')
1269
+ // ── 33. S4/S5: install failures are discriminated WITHOUT a new code (frozen interface). Reads the
1270
+ // ENV-RESOLVED module (MATH_COMPUTATION_MODULE), so the --self-probe case 2 above can reach exactly
1271
+ // what these assertions read (the parity-located version could not be reddened by any mutant).
1272
+ {
1273
+ const src = readFileSync(fileURLToPath(new URL(MODULE)), 'utf8')
1274
+ ok(src.indexOf("out.op = 'install'") !== -1, '★ S4/S5 install discrimination: a failed install sets op=install')
1275
+ ok(/out\.timedOut = !!/.test(src), '★ S4/S5 install discrimination: a failed install carries timedOut')
1276
+ ok(/out\.installedSoFar = results\.filter/.test(src), '★ S4/S5 install discrimination: a failed install reports what already succeeded (installedSoFar)')
1277
+ ok(/installedSoFar: commands\.map/.test(src), 'the success path reports installedSoFar too (no special-casing by the caller)')
1278
+ ok(src.indexOf('MATH_INSTALL_FAILED') === -1, 'no 12th failure code was added (MATH_FAILURE_CODES stays frozen)')
1279
+ }
1280
+
1281
+ // ── 34. S6: the install plan, the run path and the missing-package failure state ONE policy the
1282
+ // same way (behavioural: read from the responses, not from the source text).
1283
+ {
1284
+ const PREFIX = 'version-constraints-are-existence-only: '
1285
+ const h = makeFakeHost({ installed: ['python3'] })
1286
+ M.registerMathComputation(h.host)
1287
+ const plan = await h.call({ op: 'install', packages: ['numpy==1.2.3'] })
1288
+ const planPolicy = String((plan && plan.plan && plan.plan.versionPolicy) || '')
1289
+ ok(plan.ok === true && !!plan.plan, 'S6: an install dry-run returns a plan', JSON.stringify(plan && plan.code))
1290
+ ok(planPolicy.indexOf(PREFIX) === 0, '★ S6: the install plan uses the SAME policy wording as the run path', planPolicy.slice(0, 70))
1291
+ ok(planPolicy.indexOf('numpy==1.2.3') !== -1, '★ S6: the plan names the pinned spec it does not enforce', planPolicy.slice(0, 90))
1292
+ const miss = await h.call({ op: 'run', engine: 'python', mode: 'code', packages: ['numpy==1.2.3'], code: 'x=1\n' })
1293
+ ok(miss.ok === false && miss.code === 'MATH_MISSING_PACKAGES', 'S6: a pinned-but-missing package fails as MATH_MISSING_PACKAGES', JSON.stringify(miss.code))
1294
+ ok(String(miss.versionPolicy || '').indexOf(PREFIX) === 0, '★ S6: the failure path uses the same wording', String(miss.versionPolicy || '').slice(0, 70))
1295
+ 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)]))
1296
+ ok(String(miss.message || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the MATH_MISSING_PACKAGES detail derives the same clause', String(miss.message || '').slice(0, 70))
1297
+ const badSpec = M.parsePackageSpec('numpy@1.2')
1298
+ 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))
1299
+ ok(String(planPolicy || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the policy FIELD itself contains that clause (one literal, three surfaces)', planPolicy.slice(0, 80))
1300
+ }
1301
+
1302
+ // ── 35. S6 derivation is LOAD-BEARING (found by mutant case 2): "the message contains the clause" also
1303
+ // passes when the message RE-INLINES the same words, so the real checks are (a) the clause exists as
1304
+ // exactly ONE literal in the module and (b) the two human messages are built from the constant.
1305
+ {
1306
+ const src = readFileSync(fileURLToPath(new URL(MODULE)), 'utf8')
1307
+ const clause = '只检查是否存在;版本求解交给包管理器'
1308
+ const occurrences = src.split(clause).length - 1
1309
+ ok(occurrences === 1, '★ S6: the policy clause exists as exactly ONE literal in the module (a re-inlined message is caught)', 'occurrences=' + occurrences)
1310
+ 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')
1311
+ 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')
1312
+ ok(/const MATH_VERSION_POLICY = .*MATH_VERSION_POLICY_CLAUSE/.test(src), '★ S6: the long policy sentence is derived from the clause')
1313
+ }
1314
+
929
1315
  console.log('=== MATH COMPUTATION SHARED: ' + passed + ' passed, ' + failed + ' failed ===')
930
1316
  if (failures.length) for (const f of failures) console.error(' - ' + f)
931
1317
  process.exit(failed === 0 ? 0 : 1)