dsh-vibe-math 2.7.3 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +682 -682
- package/README.md +683 -683
- package/docs/AUDIT-CHECKLIST.md +758 -744
- package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -194
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.en.md +49 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.md +49 -0
- package/docs/release-playbook.md +111 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/test-timing.md +158 -157
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +792 -792
- package/package.json +18 -3
- package/prompt-corpus-v2/formal-verify-v2.json +2 -2
- package/prompt-corpus-v2/formal-verify-v2.md +4 -0
- package/prompt-corpus-v3/formal-verify-v3.json +7 -7
- package/prompt-corpus-v3/formal-verify-v3.md +14 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
- package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
- package/scripts/clean-temp.mjs +234 -0
- package/scripts/release-check.mjs +181 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-compat.test.mjs +256 -256
- package/tests/audit-math-computation-sensitivity.mjs +1 -1
- package/tests/audit-persona-sensitivity.mjs +323 -323
- package/tests/audit-persona-surface.test.mjs +431 -431
- package/tests/audit-prompt-duplication.mutants.mjs +3 -2
- package/tests/audit-prompt-invariants.mjs +589 -546
- package/tests/audit-registration.mjs +78 -78
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -172
- package/tests/audit-v5-integrity.mjs +795 -795
- package/tests/audit-v5-sensitivity.mjs +433 -432
- package/tests/e2e-v5-round2.test.mjs +2453 -2426
- package/tests/formal-verify-v2.test.mjs +1806 -1800
- package/tests/formal-verify-v3.test.mjs +1757 -1750
- package/tests/formal-verify-v4.test.mjs +2114 -2106
- package/tests/formal-verify-v5.test.mjs +774 -767
- package/tests/math-computation-discovery.mutants.mjs +3 -3
- package/tests/math-computation-shared.test.mjs +122 -5
- package/tests/math-computation-v2.test.mjs +14 -2
- package/tests/math-computation-v3.test.mjs +14 -2
- package/tests/math-computation-v4.test.mjs +739 -739
- package/tests/prompt-v5-integrity.test.mjs +1291 -1291
- package/tests/release-check.mutants.mjs +78 -0
- package/tests/run-tests.mjs +393 -332
- package/tests/selfdrive-v5.mjs +560 -560
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +4 -0
- package/tests/v2-fix-probes.test.mjs +1034 -1033
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -95
- package/tests/v3-fix-probes.test.mjs +891 -890
- package/tests/v4-final-paper.mutants.mjs +116 -0
- package/tests/v4-final-paper.test.mjs +628 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -190
- package/vibe-math-v2/agent.cordis.yml +376 -376
- package/vibe-math-v2/math-computation.js +17 -15
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5078 -5011
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
- package/vibe-math-v3/agent.cordis.yml +420 -420
- package/vibe-math-v3/math-computation.js +17 -15
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6215 -6149
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -438
- package/vibe-math-v4/math-computation.js +17 -15
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4520 -4422
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
- package/vibe-math-v5/math-computation.js +17 -15
- package/vibe-math-v5/vibe-math-v5.js +93 -11
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
- 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
|
|
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
|
-
|
|
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"。断言直接读**产出文本**
|