dsh-vibe-math 2.7.3 → 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 (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -682
  3. package/README.md +683 -683
  4. package/docs/AUDIT-CHECKLIST.md +755 -744
  5. package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
  6. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  7. package/docs/audit-methodology.md +93 -0
  8. package/docs/final-paper.md +172 -172
  9. package/docs/formal-verification.md +613 -588
  10. package/docs/generate_framework_diagram_v2.py +114 -114
  11. package/docs/generate_framework_diagram_v3.py +127 -127
  12. package/docs/math-computation.md +195 -194
  13. package/docs/release-notes/README.en.md +1 -0
  14. package/docs/release-notes/README.md +1 -0
  15. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  16. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  17. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  18. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  19. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  20. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  21. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  22. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  23. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  24. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  25. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  26. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  27. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  28. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  29. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  30. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  31. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  32. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  33. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  34. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  35. package/docs/release-playbook.md +103 -0
  36. package/docs/slv-playbook.md +117 -0
  37. package/docs/test-timing.md +158 -157
  38. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  39. package/installer.js +792 -792
  40. package/package.json +13 -3
  41. package/prompt-corpus-v2/formal-verify-v2.json +2 -2
  42. package/prompt-corpus-v2/formal-verify-v2.md +4 -0
  43. package/prompt-corpus-v3/formal-verify-v3.json +7 -7
  44. package/prompt-corpus-v3/formal-verify-v3.md +14 -0
  45. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  46. package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
  47. package/scripts/clean-temp.mjs +234 -0
  48. package/tests/audit-engine-faces.mjs +298 -0
  49. package/tests/audit-formal-sensitivity.mjs +345 -342
  50. package/tests/audit-installer-compat.test.mjs +256 -256
  51. package/tests/audit-math-computation-sensitivity.mjs +1 -1
  52. package/tests/audit-persona-sensitivity.mjs +323 -323
  53. package/tests/audit-persona-surface.test.mjs +431 -431
  54. package/tests/audit-prompt-duplication.mutants.mjs +3 -2
  55. package/tests/audit-prompt-invariants.mjs +589 -546
  56. package/tests/audit-registration.mjs +78 -78
  57. package/tests/audit-spec-traceability.mjs +193 -193
  58. package/tests/audit-status-report-fields.mjs +172 -172
  59. package/tests/audit-v5-integrity.mjs +795 -795
  60. package/tests/audit-v5-sensitivity.mjs +433 -432
  61. package/tests/e2e-v5-round2.test.mjs +2453 -2426
  62. package/tests/formal-verify-v2.test.mjs +1806 -1800
  63. package/tests/formal-verify-v3.test.mjs +1757 -1750
  64. package/tests/formal-verify-v4.test.mjs +2114 -2106
  65. package/tests/formal-verify-v5.test.mjs +774 -767
  66. package/tests/math-computation-discovery.mutants.mjs +3 -3
  67. package/tests/math-computation-shared.test.mjs +122 -5
  68. package/tests/math-computation-v2.test.mjs +14 -2
  69. package/tests/math-computation-v3.test.mjs +14 -2
  70. package/tests/math-computation-v4.test.mjs +739 -739
  71. package/tests/prompt-v5-integrity.test.mjs +1291 -1291
  72. package/tests/run-tests.mjs +393 -332
  73. package/tests/selfdrive-v5.mjs +560 -560
  74. package/tests/temp-hygiene.mutants.mjs +80 -0
  75. package/tests/v2-fix-probes.mutants.mjs +4 -0
  76. package/tests/v2-fix-probes.test.mjs +1034 -1033
  77. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  78. package/tests/v3-fix-probes.mutants.mjs +99 -95
  79. package/tests/v3-fix-probes.test.mjs +891 -890
  80. package/tests/v4-final-paper.mutants.mjs +108 -0
  81. package/tests/v5-institute-fixes.mutants.mjs +218 -190
  82. package/vibe-math-v2/agent.cordis.yml +376 -376
  83. package/vibe-math-v2/math-computation.js +17 -15
  84. package/vibe-math-v2/preset.yml +2 -2
  85. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  86. package/vibe-math-v2/vibe-math-v2.js +5078 -5011
  87. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
  88. package/vibe-math-v3/agent.cordis.yml +420 -420
  89. package/vibe-math-v3/math-computation.js +17 -15
  90. package/vibe-math-v3/preset.yml +2 -2
  91. package/vibe-math-v3/vibe-math-v3.js +6215 -6149
  92. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  93. package/vibe-math-v4/agent.cordis.yml +438 -438
  94. package/vibe-math-v4/math-computation.js +17 -15
  95. package/vibe-math-v4/preset.yml +2 -2
  96. package/vibe-math-v4/vibe-math-v4.js +4490 -4422
  97. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
  98. package/vibe-math-v5/math-computation.js +17 -15
  99. package/vibe-math-v5/vibe-math-v5.js +93 -11
  100. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
  101. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -33,14 +33,14 @@ function runSuite(dir) {
33
33
  const o = String(r.stdout || '') + String(r.stderr || '')
34
34
  return { status: r.status, out: o, failLines: o.split('\n').filter((l) => /^\s*FAIL\b/.test(l)), sum: (o.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim() }
35
35
  }
36
- const M3_FROM = " // round-9: then the known per-OS install locations (an engine installed without touching PATH).\n for (const p of await knownInstallCandidates(H, d.name)) {\n if (typeof p === 'string' && p) return p\n }\n"
36
+ const M3_FROM = " // round-9: then the known per-OS install locations (an engine installed without touching PATH).\n for (const p of await knownInstallCandidates(H, d.name)) {\n if (typeof p === 'string' && p) return { exe: p, via: 'known-install' }\n }\n"
37
37
  const CASES = [
38
38
  { name: 'm3: the known-install-location lookup is dropped (PATH + bundled runtime only)',
39
39
  expect: 'a Rscript that exists ONLY in the default install dir is discovered',
40
40
  edit: (t) => t.replace(M3_FROM, '') },
41
41
  { name: 'b1: the host-injected install roots are ignored (back to ambient env)',
42
42
  expect: 'a Rscript that exists ONLY in the default install dir is discovered',
43
- edit: (t) => t.replace('const roots = injected || mathInstallRoots(engineName, env, win)', 'const roots = mathInstallRoots(engineName, env, win)') },
43
+ edit: (t) => t.replace('const roots = (Array.isArray(injected) && injected.length) ? injected : mathInstallRoots(engineName, env, win)', 'const roots = mathInstallRoots(engineName, env, win)') },
44
44
  { name: 'b2: the octave/julia per-OS root entries are removed',
45
45
  expect: 'every engine with a well-defined default install dir declares per-OS roots: octave',
46
46
  edit: (t) => t.split('octave: win').join('octaveDisabled: win').split('julia: win').join('juliaDisabled: win') },
@@ -58,7 +58,7 @@ const CASES = [
58
58
  ok(b.status === 0, 'baseline: the shipped suite is green on the copied module pair (so a red below is the mutation)', b.sum.slice(0, 70))
59
59
  rmSync(dir, { recursive: true, force: true })
60
60
  }
61
- const ORIGIN = readFileSync(join(SRC, 'math-computation.js'), 'utf8')
61
+ const ORIGIN = readFileSync(join(SRC, 'math-computation.js'), 'utf8').replace(/\r\n/g, '\n') // anchors are LF
62
62
  for (const c of CASES) {
63
63
  const mutated = c.edit(ORIGIN)
64
64
  if (mutated === ORIGIN) { ok(false, c.name + ' — the single-site anchor applies', 'ANCHOR MISS'); continue }
@@ -42,7 +42,7 @@ if (process.argv.includes('--self-probe')) {
42
42
  const dir = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe-'))
43
43
  const enginesSrc = join(dirname(modPath), 'math-engines.js')
44
44
  try { copyFileSync(enginesSrc, join(dir, 'math-engines.js')) } catch (e) { /* engines may be resolved elsewhere */ }
45
- const src = rf(modPath, 'utf8')
45
+ const src = rf(modPath, 'utf8').replace(/\r\n/g, '\n')
46
46
  const whitelist = ' if (extra && Array.isArray(extra.absent)) out.absent = extra.absent\n'
47
47
  if (src.indexOf(whitelist) === -1) {
48
48
  console.log('SELF-PROBE FAIL: the fail() whitelist line for absent[] was not found (the probe cannot mutate what it cannot see)')
@@ -62,7 +62,7 @@ if (process.argv.includes('--self-probe')) {
62
62
  // assertion in the child's FAIL line (a label found anywhere in the output is NOT a red).
63
63
  const s45line = ' out.installedSoFar = results.filter((x) => x.exit === 0).map((x) => x.argv)\n'
64
64
  const dir2 = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe2-'))
65
- const src2 = rf(modPath, 'utf8')
65
+ const src2 = rf(modPath, 'utf8').replace(/\r\n/g, '\n')
66
66
  let ok2 = false
67
67
  if (src2.indexOf(s45line) === -1) {
68
68
  console.log('SELF-PROBE FAIL: the installedSoFar assignment was not found (cannot mutate what it cannot see)')
@@ -81,7 +81,7 @@ if (process.argv.includes('--self-probe')) {
81
81
  // case 3 (§30, verify provenance): the SAME predicates the §30 assertions use, fed a broken helper.
82
82
  // The real `verifyFields(engine)` helper is extracted from the module under test and run against a stub
83
83
  // descriptor table; dropping its single reason line (or the no-empty-slot guard) must flip its predicate.
84
- const src3 = rf(modPath, 'utf8')
84
+ const src3 = rf(modPath, 'utf8').replace(/\r\n/g, '\n')
85
85
  const hStart = src3.indexOf('function verifyFields(engine) {')
86
86
  let helper = ''
87
87
  if (hStart !== -1) {
@@ -113,7 +113,82 @@ if (process.argv.includes('--self-probe')) {
113
113
  ok3 = bad3 === 0
114
114
  console.log(' --- §30 verify-provenance predicates: ' + (cases3.length - bad3) + '/' + cases3.length + ' as required')
115
115
  }
116
- process.exit(child.status !== 0 && named && ok2 && ok3 ? 0 : 1)
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)
117
192
  }
118
193
 
119
194
  let passed = 0, failed = 0
@@ -278,7 +353,7 @@ console.log('-- math_computation shared contract --')
278
353
  ok(r.cliScriptAppended === true, 'the return shell flags cliScriptAppended')
279
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')
280
355
  const rj = JSON.parse(state.file(r.receipt.json))
281
- 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')
282
357
  ok(rj.cli.scriptAppended === true && rj.argv[rj.argv.length - 1] === r.argv[3], 'the receipt records scriptAppended and the executed argv')
283
358
  })
284
359
  await withHost({ params: { mathMode: 'typed' } }, async (state) => {
@@ -705,6 +780,7 @@ console.log('-- math_computation shared contract --')
705
780
  const p = await hb.call({ op: 'probe', engine: 'python' })
706
781
  ok(p.ok === true && p.engine === 'python' && p.engineInfo.name === 'python', 'a bundled DSH runtime is found when PATH has nothing')
707
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)')
708
784
  const r = await hb.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
709
785
  ok(r.ok === true && !!r.engineInfo.version, 'a run on the bundled runtime succeeds and reports its version')
710
786
  ok(!r.next, 'a found bundled runtime emits NO install guidance')
@@ -930,6 +1006,7 @@ console.log('-- math_computation shared contract --')
930
1006
  const p = await h.call({ op: 'probe', engine: 'r' })
931
1007
  ok(p.ok === true && p.engine === 'r', '★ a Rscript that exists ONLY in the default install dir is discovered (not on PATH)')
932
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)')
933
1010
  }
934
1011
  // ── 23b. round-B (F3): the SAME walker covers Octave and Julia default dirs (both layout shapes)
935
1012
  {
@@ -956,6 +1033,46 @@ console.log('-- math_computation shared contract --')
956
1033
  '★ every engine with a well-defined default install dir declares per-OS roots: ' + e)
957
1034
  }
958
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)')
1075
+ }
959
1076
  // ── 24. round-9 (real-engine): never suggest an install command that cannot run on this machine
960
1077
  {
961
1078
  const h = makeFakeHost({ installed: [] }) // `winget` is NOT resolvable here
@@ -158,6 +158,7 @@ function makeHost() {
158
158
  const toolRegs = []
159
159
  const cmdRegs = []
160
160
  const spawns = []
161
+ let interruptThrows = false // task-14 (SLV P3): injection seam for the "host refuses to interrupt" branch
161
162
  const ctx = {
162
163
  get(name) {
163
164
  if (name === 'subprocess') return (fake && fake.noSubprocess) ? undefined : subprocess
@@ -180,7 +181,7 @@ function makeHost() {
180
181
  },
181
182
  async sendMessage() { return 'w' },
182
183
  async followup() { return 'w' },
183
- interrupt() {},
184
+ interrupt() { if (interruptThrows) throw new Error('宿主拒绝中断(测试注入)') },
184
185
  },
185
186
  fs: {
186
187
  async resolve(rel, opts) { const base = (opts && opts.cwd) || WS; const p = String(rel).replace(/\\/g, '/'); return /^([A-Za-z]:|\/)/.test(p) ? p : join(base, ...p.split('/')) },
@@ -205,7 +206,7 @@ function makeHost() {
205
206
  return t[p]
206
207
  },
207
208
  })
208
- return { ctx, toolRegs, cmdRegs, spawns, listeners, fs: fsBase }
209
+ return { ctx, toolRegs, cmdRegs, spawns, listeners, fs: fsBase, set interruptThrows(v) { interruptThrows = v } }
209
210
  }
210
211
  function makeRoot(id) { return { id, options: { provider: 'mock', model: 'mock' }, session: { id, header: { cwd: WS, parentSession: undefined } }, followup() {} } }
211
212
 
@@ -703,6 +704,17 @@ section('§19 成员作用域失败诊断:interruptChild 显式失败 + decisi
703
704
  const badDecide = await call('vibe_math_decide', { id: 'no-such-decision', action: 'approve' })
704
705
  assert(badDecide.ok === false && badDecide.code === 'VIBE_MATH_DECISION_NOT_FOUND', '★ [D1] 未知 decision id ⇒ code=VIBE_MATH_DECISION_NOT_FOUND,不是裸 message(实测 ' + JSON.stringify(badDecide).slice(0, 170) + ')')
705
706
  assert(!!badDecide.next && badDecide.next.tool === 'vibe_math_list_decisions', '★ [D1] 决策诊断的 next 指向真实存在的 vibe_math_list_decisions(实测 ' + JSON.stringify(badDecide.next || null) + ')')
707
+ // task-14 (SLV P3): the "host REFUSES to interrupt" branch — the one failure mode §19 did NOT cover.
708
+ // A live child is required FIRST (otherwise the code returns CHILD_NOT_FOUND and the guard would pass for
709
+ // the wrong reason); only then does the fake host start throwing.
710
+ const liveChild = (H.spawns && H.spawns.length) ? H.spawns[H.spawns.length - 1].childId : ''
711
+ const interruptible = await rawInterrupt({ childId: liveChild })
712
+ assert(interruptible.ok === true, '★ [P3/v2] 前置:可中断的 child 返回 ok:true(childId=' + liveChild + ',实测 ' + JSON.stringify(interruptible).slice(0, 140) + ')')
713
+ H.interruptThrows = true
714
+ let refused
715
+ try { refused = await rawInterrupt({ childId: liveChild }) } finally { H.interruptThrows = false }
716
+ assert(refused && refused.ok !== true && refused.code === 'VIBE_MATH_INTERRUPT_FAILED', '★ [P3/v2] 宿主拒绝中断 ⇒ 具名失败 code=VIBE_MATH_INTERRUPT_FAILED(实测 ' + JSON.stringify(refused).slice(0, 170) + ')')
717
+ assert(!!refused.next && refused.next.tool === 'vibe_math_abort' && !!refused.next.hint, '★ [P3/v2] 该失败带 next{tool=vibe_math_abort,hint}(实测 ' + JSON.stringify(refused.next || null) + ')')
706
718
  }
707
719
  // ── §20 审计 P0(**文本层断言**):成员可见材料必须写明"文件工具按会话 cwd 解析相对路径 ⇒ 下面列出的
708
720
  // 相对路径要先拼项目根的绝对前缀;计算产物用回执的绝对字段 receipt.scriptAbs"。断言直接读**产出文本**
@@ -158,6 +158,7 @@ function makeHost() {
158
158
  const toolRegs = []
159
159
  const cmdRegs = []
160
160
  const spawns = []
161
+ let interruptThrows = false // task-14 (SLV P3): injection seam for the "host refuses to interrupt" branch
161
162
  const ctx = {
162
163
  get(name) {
163
164
  if (name === 'subprocess') return (fake && fake.noSubprocess) ? undefined : subprocess
@@ -180,7 +181,7 @@ function makeHost() {
180
181
  },
181
182
  async sendMessage() { return 'w' },
182
183
  async followup() { return 'w' },
183
- interrupt() {},
184
+ interrupt() { if (interruptThrows) throw new Error('宿主拒绝中断(测试注入)') },
184
185
  },
185
186
  fs: {
186
187
  async resolve(rel, opts) { const base = (opts && opts.cwd) || WS; const p = String(rel).replace(/\\/g, '/'); return /^([A-Za-z]:|\/)/.test(p) ? p : join(base, ...p.split('/')) },
@@ -205,7 +206,7 @@ function makeHost() {
205
206
  return t[p]
206
207
  },
207
208
  })
208
- return { ctx, toolRegs, cmdRegs, spawns, listeners, fs: fsBase }
209
+ return { ctx, toolRegs, cmdRegs, spawns, listeners, fs: fsBase, set interruptThrows(v) { interruptThrows = v } }
209
210
  }
210
211
  function makeRoot(id) { return { id, options: { provider: 'mock', model: 'mock' }, session: { id, header: { cwd: WS, parentSession: undefined } }, followup() {} } }
211
212
 
@@ -709,6 +710,17 @@ section('§19 成员作用域失败诊断:interruptChild 显式失败 + decisi
709
710
  const badDecide = await call('vibe_math_decide', { id: 'no-such-decision', action: 'approve' })
710
711
  assert(badDecide.ok === false && badDecide.code === 'VIBE_MATH_DECISION_NOT_FOUND', '★ [D1] 未知 decision id ⇒ code=VIBE_MATH_DECISION_NOT_FOUND,不是裸 message(实测 ' + JSON.stringify(badDecide).slice(0, 170) + ')')
711
712
  assert(!!badDecide.next && badDecide.next.tool === 'vibe_math_list_decisions', '★ [D1] 决策诊断的 next 指向真实存在的 vibe_math_list_decisions(实测 ' + JSON.stringify(badDecide.next || null) + ')')
713
+ // task-14 (SLV P3): the "host REFUSES to interrupt" branch — the one failure mode §19 did NOT cover.
714
+ // A live child is required FIRST (otherwise the code returns CHILD_NOT_FOUND and the guard would pass for
715
+ // the wrong reason); only then does the fake host start throwing.
716
+ const liveChild = (H.spawns && H.spawns.length) ? H.spawns[H.spawns.length - 1].childId : ''
717
+ const interruptible = await rawInterrupt({ childId: liveChild })
718
+ assert(interruptible.ok === true, '★ [P3/v3] 前置:可中断的 child 返回 ok:true(childId=' + liveChild + ',实测 ' + JSON.stringify(interruptible).slice(0, 140) + ')')
719
+ H.interruptThrows = true
720
+ let refused
721
+ try { refused = await rawInterrupt({ childId: liveChild }) } finally { H.interruptThrows = false }
722
+ assert(refused && refused.ok !== true && refused.code === 'VIBE_MATH_INTERRUPT_FAILED', '★ [P3/v3] 宿主拒绝中断 ⇒ 具名失败 code=VIBE_MATH_INTERRUPT_FAILED(实测 ' + JSON.stringify(refused).slice(0, 170) + ')')
723
+ assert(!!refused.next && refused.next.tool === 'vibe_math_abort' && !!refused.next.hint, '★ [P3/v3] 该失败带 next{tool=vibe_math_abort,hint}(实测 ' + JSON.stringify(refused.next || null) + ')')
712
724
  }
713
725
  // ── §20 审计 P0(**文本层断言**):成员可见材料必须写明"文件工具按会话 cwd 解析相对路径 ⇒ 下面列出的
714
726
  // 相对路径要先拼项目根的绝对前缀;计算产物用回执的绝对字段 receipt.scriptAbs"。断言直接读**产出文本**