dsh-vibe-math 2.5.0 → 2.7.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 (59) hide show
  1. package/README.en.md +175 -369
  2. package/README.md +175 -370
  3. package/cordis.patch.yml +78 -22
  4. package/docs/AUDIT-CHECKLIST.md +3 -3
  5. package/docs/final-paper.md +172 -0
  6. package/docs/formal-verification.md +184 -29
  7. package/docs/math-computation.md +121 -0
  8. package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
  9. package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
  10. package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
  11. package/docs/test-timing.md +25 -9
  12. package/installer.js +7 -4
  13. package/package.json +21 -3
  14. package/prompt-corpus-persona/persona-corpus.json +16 -16
  15. package/prompt-corpus-persona/persona-corpus.md +164 -52
  16. package/prompt-corpus-v2/formal-verify-v2.json +128 -92
  17. package/prompt-corpus-v2/formal-verify-v2.md +1574 -108
  18. package/prompt-corpus-v3/formal-verify-v3.json +137 -53
  19. package/prompt-corpus-v3/formal-verify-v3.md +1406 -100
  20. package/prompt-corpus-v4/formal-verify-v4.json +14 -14
  21. package/prompt-corpus-v4/formal-verify-v4.md +247 -14
  22. package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
  23. package/prompt-corpus-v5/prompt-corpus-v5.md +570 -3
  24. package/tests/audit-math-computation-contract.mjs +191 -0
  25. package/tests/audit-math-computation-parity.mjs +169 -0
  26. package/tests/audit-math-computation-sensitivity.mjs +231 -0
  27. package/tests/audit-persona-sensitivity.mjs +64 -11
  28. package/tests/audit-persona-surface.test.mjs +53 -16
  29. package/tests/audit-v5-integrity.mjs +14 -0
  30. package/tests/e2e-v5-round2.test.mjs +923 -3
  31. package/tests/formal-verify-v2.test.mjs +295 -6
  32. package/tests/formal-verify-v3.test.mjs +275 -4
  33. package/tests/formal-verify-v4.test.mjs +295 -6
  34. package/tests/formal-verify-v5.test.mjs +9 -1
  35. package/tests/helpers/math-computation-fake-seam.mjs +135 -0
  36. package/tests/math-computation-shared.test.mjs +504 -0
  37. package/tests/prompt-v5-integrity.test.mjs +14 -8
  38. package/tests/selfdrive-v5.mjs +55 -4
  39. package/vibe-math-v2/agent.cordis.yml +36 -14
  40. package/vibe-math-v2/math-computation.js +936 -0
  41. package/vibe-math-v2/math-engines.js +156 -0
  42. package/vibe-math-v2/vibe-math-v2.js +1653 -48
  43. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  44. package/vibe-math-v3/agent.cordis.yml +30 -12
  45. package/vibe-math-v3/math-computation.js +936 -0
  46. package/vibe-math-v3/math-engines.js +156 -0
  47. package/vibe-math-v3/vibe-math-v3.js +1623 -53
  48. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  49. package/vibe-math-v4/agent.cordis.yml +44 -8
  50. package/vibe-math-v4/math-computation.js +936 -0
  51. package/vibe-math-v4/math-engines.js +156 -0
  52. package/vibe-math-v4/vibe-math-v4.js +1592 -37
  53. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  54. package/vibe-math-v5/agent.cordis.yml +46 -10
  55. package/vibe-math-v5/math-computation.js +936 -0
  56. package/vibe-math-v5/math-engines.js +156 -0
  57. package/vibe-math-v5/vibe-math-v5.js +2233 -42
  58. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +153 -4
  59. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
@@ -0,0 +1,191 @@
1
+ // ============================================================================================
2
+ // math_computation — CROSS-PRESET CONTRACT guard (integration owner).
3
+ //
4
+ // Proves that all four presets wired the frozen shared interface the same way:
5
+ // - each imports ./math-computation.js and registers the tool through its own registration path(s);
6
+ // - each carries the six frozen parameter names in DEFAULT_PARAMS, its normaliser call, its
7
+ // parameter schema(s) and (v5) its CLOSED set schema + visibleParams;
8
+ // - each references the frozen prompt text constants (description / persona line / rule lines /
9
+ // availability line), so the shell-fallback marking and the criteria cannot silently disappear;
10
+ // - both persona blocks (prefix + text) list the tool;
11
+ // - the bilingual README documents the six parameters.
12
+ //
13
+ // Falsifiable: a preset that drops a param, forgets the second registration layer, or stops using
14
+ // the frozen prompt text turns this suite RED. It is intentionally stricter than any single preset
15
+ // suite - it is the cross-preset part no owner can weaken alone.
16
+ //
17
+ // Usage: node tests/audit-math-computation-contract.mjs
18
+ // ============================================================================================
19
+ import { existsSync, readFileSync } from 'node:fs'
20
+ import { join, resolve } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
24
+ const REPO = resolve(HERE, '..')
25
+ // MC_CONTRACT_ROOT points the guard at a MUTATED copy of the tree (used by the round-4 item-3
26
+ // mutant, which must delete 'Formal/Jobs' from one preset without touching the repo).
27
+ const ROOT = process.env.MC_CONTRACT_ROOT ? resolve(String(process.env.MC_CONTRACT_ROOT)) : REPO
28
+ // The shared defaults are the reference for AUDIT-B #6 (no preset may hold a drifting literal).
29
+ const M = await import(new URL('../vibe-math-v2/math-computation.js', import.meta.url).href)
30
+ const PRESETS = [
31
+ { dir: 'vibe-math-v2', designator: 'vibe-math-v2', dualRegistration: true },
32
+ { dir: 'vibe-math-v3', designator: 'vibe-math-v3', dualRegistration: true },
33
+ { dir: 'vibe-math-v4', designator: 'vibe-math-v4', dualRegistration: false },
34
+ { dir: 'vibe-math-v5', designator: 'vibe-math-v5', dualRegistration: false },
35
+ ]
36
+ const SIX = ['mathComputation', 'mathMode', 'mathEngines', 'mathTimeoutMs', 'mathPackages', 'mathInstallScope']
37
+
38
+ let passed = 0, failed = 0
39
+ const failures = []
40
+ function ok(cond, label, detail) {
41
+ if (cond) { passed++; return true }
42
+ failed++; failures.push(label + (detail ? ' — ' + detail : ''))
43
+ return false
44
+ }
45
+ const read = (rel) => readFileSync(join(ROOT, rel), 'utf8')
46
+
47
+ function block(src, startMarker, endMarker) {
48
+ const i = src.indexOf(startMarker)
49
+ if (i === -1) return ''
50
+ const j = endMarker ? src.indexOf(endMarker, i) : -1
51
+ return j === -1 ? src.slice(i, i + 4000) : src.slice(i, j)
52
+ }
53
+ // A literal array assignment (e.g. `const PARAM_SCHEMA = [ … ]`) must be sliced to the CLOSING
54
+ // bracket of the assignment, not to the first `]` inside it (that is an inner `options: [...]`).
55
+ // The presets indent the closing bracket on its own line at two spaces, which is stable and does
56
+ // not need a brace-aware parser.
57
+ // A literal array assignment (e.g. `const PARAM_SCHEMA = [ … ]`) must be sliced to the CLOSING
58
+ // bracket of the assignment, not to the first `]` inside it (that is an inner `options: [...]`).
59
+ function arrayBlock(src, startMarker) {
60
+ const i = src.indexOf(startMarker)
61
+ if (i === -1) return ''
62
+ const j = src.indexOf('\n ]', i)
63
+ return j === -1 ? src.slice(i, i + 4000) : src.slice(i, j)
64
+ }
65
+ // A registration call (`registerTool('vibe_v5_set', '<long description>', objParams({…}))`) can
66
+ // contain `})` inside its description string, so slice to the closing bracket on its own line
67
+ // (two-space indent) instead of the first `})`.
68
+ function callBlock(src, startMarker) {
69
+ const i = src.indexOf(startMarker)
70
+ if (i === -1) return ''
71
+ const j = src.indexOf('\n })', i)
72
+ if (j !== -1) return src.slice(i, j)
73
+ const k = src.indexOf('})', i)
74
+ return k === -1 ? src.slice(i, i + 4000) : src.slice(i, k)
75
+ }
76
+
77
+ console.log('-- math_computation cross-preset contract --')
78
+
79
+ for (const P of PRESETS) {
80
+ const js = read(P.dir + '/' + P.dir + '.js')
81
+ const tag = P.dir + ': '
82
+
83
+ // 1. import + registration
84
+ ok(js.indexOf("'./math-computation.js'") !== -1, tag + 'imports the shared module')
85
+ ok(js.indexOf('registerMathComputation(') !== -1, tag + 'calls registerMathComputation(host)')
86
+ if (P.dualRegistration) {
87
+ // v2/v3 register every tool twice: the session handlers layer and the apply-level layer.
88
+ const defs = (js.match(/function registerTool\s*\(/g) || []).length
89
+ ok(defs >= 2, tag + 'has both registerTool definition layers', 'found ' + defs)
90
+ }
91
+
92
+ // 2. six params everywhere
93
+ const defaults = block(js, 'const DEFAULT_PARAMS', '\n }')
94
+ for (const k of SIX) ok(defaults.indexOf(k) !== -1, tag + 'DEFAULT_PARAMS holds ' + k)
95
+ ok(js.indexOf('normalizeMathParams(') !== -1, tag + 'normalises through normalizeMathParams()')
96
+
97
+ // parameter schemas: v2/v3 PARAM_SCHEMA + two set_params schemas; v4 vibe_v4_set; v5 closed set + visibleParams
98
+ // AUDIT-B #7: check the PROPERTY, not the prose. The set tool's description string naturally
99
+ // contains every parameter name, so an indexOf over the whole registration call stays green even
100
+ // when a property is renamed. Only the `objParams({...})` argument counts.
101
+ const SET_TOOL = P.dir === 'vibe-math-v2' || P.dir === 'vibe-math-v3'
102
+ ? "registerTool('vibe_math_set_params'"
103
+ : (P.dir === 'vibe-math-v4' ? "registerTool('vibe_v4_set'" : "registerTool('vibe_v5_set'")
104
+ {
105
+ const i = js.indexOf(SET_TOOL)
106
+ const callText = i === -1 ? '' : js.slice(i, js.indexOf('registerTool(', i + 1) === -1 ? i + 4000 : js.indexOf('registerTool(', i + 1))
107
+ const oi = callText.lastIndexOf('objParams(')
108
+ const setSchema = oi === -1 ? '' : callText.slice(oi)
109
+ ok(setSchema.length > 0, tag + 'set tool takes an objParams({...}) schema argument')
110
+ for (const k of SIX) {
111
+ ok(new RegExp('(^|[{,\\s])' + k + '\\s*:').test(setSchema), tag + 'set schema has the property ' + k + ' (property-level, not prose)', 'schema len ' + setSchema.length)
112
+ }
113
+ }
114
+ if (P.dir === 'vibe-math-v2' || P.dir === 'vibe-math-v3') {
115
+ const schema = arrayBlock(js, 'const PARAM_SCHEMA')
116
+ for (const k of SIX) ok(schema.indexOf(k) !== -1, tag + 'PARAM_SCHEMA documents ' + k)
117
+ } else if (P.dir === 'vibe-math-v5') {
118
+ const visible = block(js, 'function visibleParams()', '\n }')
119
+ for (const k of SIX) ok(visible.indexOf(k) !== -1, tag + 'visibleParams exposes ' + k)
120
+ }
121
+
122
+ // AUDIT-B #6: the presets' exposed DEFAULTS must come from the shared MATH_PARAM_DEFAULTS, so the
123
+ // four copies cannot drift (v4 had a literal `mathPackages: []`). Arrays must be referenced and
124
+ // sliced; scalar literals must equal the shared value.
125
+ {
126
+ const defs = block(js, 'const DEFAULT_PARAMS', '\n }')
127
+ for (const k of SIX) {
128
+ if (new RegExp('MATH_PARAM_DEFAULTS\\.' + k + '\\b').test(defs)) continue
129
+ if (Array.isArray(M.MATH_PARAM_DEFAULTS[k])) {
130
+ ok(false, tag + 'DEFAULT_PARAMS.' + k + ' must reference MATH_PARAM_DEFAULTS (array literals drift)')
131
+ continue
132
+ }
133
+ const m = new RegExp(k + ":\\s*(?:'([^']*)'|(\\d+))").exec(defs)
134
+ const val = m ? (m[1] !== undefined ? m[1] : Number(m[2])) : undefined
135
+ ok(val === M.MATH_PARAM_DEFAULTS[k], tag + 'DEFAULT_PARAMS.' + k + ' literal must equal the shared default', 'got ' + JSON.stringify(val) + ' want ' + JSON.stringify(M.MATH_PARAM_DEFAULTS[k]))
136
+ }
137
+ }
138
+
139
+ // round-4 item 3: `Formal/Jobs` (the async Lean job mirror) must be in EVERY preset's ensureDirs.
140
+ // The host creates missing parents, so this is a consistency/discoverability pin, not a
141
+ // correctness one - but deleting it from v4 also leaves the Lean suites green, so nothing else
142
+ // would notice the four presets drifting apart.
143
+ {
144
+ // Match the ensureDirs/dirs-creation FUNCTION body (not a comment, and not the unrelated
145
+ // verification-only dirs list that legitimately carries 'Formal' without 'Formal/Jobs').
146
+ const lines = js.split(/\r?\n/)
147
+ const start = lines.findIndex((l) => /function\s+(ensureDirs|mkdirs)\s*\(/.test(l))
148
+ const window = start === -1 ? '' : lines.slice(start, start + 15).join('\n')
149
+ ok(start !== -1, tag + 'has an ensureDirs()/mkdirs() function')
150
+ ok(window.indexOf("'Formal/Jobs'") !== -1, tag + "ensureDirs creates 'Formal/Jobs' (async Lean job mirror)")
151
+ }
152
+
153
+ // 3. frozen prompt text constants are actually used
154
+ ok(js.indexOf('MATH_TOOL_DESCRIPTION') !== -1, tag + 'uses MATH_TOOL_DESCRIPTION')
155
+ ok(js.indexOf('MATH_PERSONA_TOOL_LINE') !== -1, tag + 'uses MATH_PERSONA_TOOL_LINE')
156
+ ok(js.indexOf('MATH_RULE_LINES') !== -1, tag + 'uses MATH_RULE_LINES')
157
+ ok(js.indexOf('mathAvailabilityLine(') !== -1, tag + 'builds the availability line (carries the shell marking)')
158
+ if (P.dir === 'vibe-math-v4' || P.dir === 'vibe-math-v5') ok(js.indexOf('MATH_RULE_LINES_EN') !== -1, tag + 'bilingual rule lines referenced')
159
+
160
+ // 4. persona: both literal blocks list the tool
161
+ const yml = read(P.dir + '/agent.cordis.yml')
162
+ const blocks = yml.split(/\n\s*(?:prefix|text):\s*\|/)
163
+ const hits = blocks.filter((b) => b.indexOf('math_computation') !== -1).length
164
+ ok(hits >= 2, tag + 'both persona blocks (prefix + text) list math_computation', 'blocks with the tool: ' + hits)
165
+
166
+ // 4b. P2a: both blocks must carry the archive -> edit -> re-run rule (scriptChanged + mode:'file').
167
+ const wf = blocks.filter((b) => b.indexOf('scriptChanged') !== -1 && b.indexOf("mode:'file'") !== -1).length
168
+ ok(wf >= 2, tag + 'both persona blocks carry the archive->edit->re-run rule (scriptChanged + mode:file)', 'blocks with the rule: ' + wf)
169
+ ok(js.indexOf('MATH_ARCHIVE_WORKFLOW_LINE') !== -1, tag + 'references MATH_ARCHIVE_WORKFLOW_LINE (zh persona text source)')
170
+ if (P.dir === 'vibe-math-v4' || P.dir === 'vibe-math-v5') ok(js.indexOf('MATH_ARCHIVE_WORKFLOW_LINE_EN') !== -1, tag + 'references MATH_ARCHIVE_WORKFLOW_LINE_EN (en persona text source)')
171
+ }
172
+
173
+ // 5. README (both languages) document the six parameters
174
+ for (const f of ['README.md', 'README.en.md']) {
175
+ const md = existsSync(join(ROOT, f)) ? read(f) : ''
176
+ for (const k of SIX) ok(md.indexOf(k) !== -1, f + ' documents ' + k)
177
+ ok(/math-computation\.md|math_computation/.test(md), f + ' links the math_computation feature')
178
+ }
179
+
180
+ // 6. AUDIT-A/C honesty disclosures must not silently disappear from the contract doc.
181
+ {
182
+ const doc = existsSync(join(ROOT, 'docs/math-computation.md')) ? read('docs/math-computation.md') : ''
183
+ ok(doc.indexOf('字符串级') !== -1 && doc.indexOf('不解析符号链接') !== -1, 'docs §6 states the path guard is string-level and does not resolve links/junctions')
184
+ ok(doc.indexOf('只通过假 subprocess seam 验证') !== -1, 'docs §6 states engine execution is verified only through the fake subprocess seam')
185
+ ok(doc.indexOf('串行') !== -1 && doc.indexOf('跨进程') !== -1, 'docs §6 states the in-process serialisation and the cross-process limitation')
186
+ }
187
+
188
+ console.log('')
189
+ console.log('=== MATH COMPUTATION CONTRACT: ' + passed + ' passed, ' + failed + ' failed ===')
190
+ if (failures.length) { for (const f of failures.slice(0, 25)) console.error(' - ' + f); if (failures.length > 25) console.error(' … ' + (failures.length - 25) + ' more') }
191
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,169 @@
1
+ // ============================================================================================
2
+ // math_computation — PARITY guard (integration owner).
3
+ //
4
+ // The shared module is installed as FOUR byte-identical copies (one per preset dir) because the
5
+ // <=0.1.6 install line copies only the files listed in installer.js PRESETS[].files - a repo-root
6
+ // shared module would be missing from an installed preset. That design is only safe if drift is
7
+ // impossible to miss, so this guard fails when:
8
+ // 1. any of the four copies differs from the others (byte level);
9
+ // 2. the copies differ from the canonical source in _oneoff/mc-P1-ready/canonical/ (dev checkout);
10
+ // 3. installer.js / package.json stop shipping the module (either install line);
11
+ // 4. the exported freeze drifts from _oneoff/mc-P1-ready/tool-schema.json (codes, param names);
12
+ // 5. the descriptor table loses a P1 invariant (cli default-on + policy, Maple VERIFY flag,
13
+ // arg-error hints on the commercial three, engine order).
14
+ //
15
+ // Usage: node tests/audit-math-computation-parity.mjs
16
+ // ============================================================================================
17
+ import { createHash } from 'node:crypto'
18
+ import { existsSync, readFileSync } from 'node:fs'
19
+ import { join, resolve } from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
23
+ const REPO = resolve(HERE, '..')
24
+ const PRESETS = ['vibe-math-v2', 'vibe-math-v3', 'vibe-math-v4', 'vibe-math-v5']
25
+ const MODULES = ['math-engines.js', 'math-computation.js']
26
+ const CANON = join(REPO, '_oneoff', 'mc-P1-ready', 'canonical')
27
+ const SCHEMA = join(REPO, '_oneoff', 'mc-P1-ready', 'tool-schema.json')
28
+
29
+ let passed = 0, failed = 0
30
+ const failures = []
31
+ function ok(cond, label, detail) {
32
+ if (cond) { passed++; console.log(' ok ' + label); return true }
33
+ failed++; failures.push(label + (detail ? ' — ' + detail : ''))
34
+ console.error(' FAIL ' + label + (detail ? ' — ' + detail : ''))
35
+ return false
36
+ }
37
+ const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
38
+ const read = (rel) => readFileSync(join(REPO, rel), 'utf8')
39
+
40
+ console.log('-- math_computation parity --')
41
+
42
+ // ---- 1/2. the four copies are identical, and match the canonical source ----
43
+ for (const f of MODULES) {
44
+ const hashes = {}
45
+ for (const p of PRESETS) {
46
+ const file = join(REPO, p, f)
47
+ if (!existsSync(file)) { ok(false, p + '/' + f + ' exists'); continue }
48
+ const h = sha(file)
49
+ hashes[h] = (hashes[h] || []).concat(p)
50
+ }
51
+ const distinct = Object.keys(hashes)
52
+ ok(distinct.length === 1 && hashes[distinct[0]].length === 4, 'four copies of ' + f + ' are byte-identical', distinct.length + ' distinct hash(es)')
53
+ const canon = join(CANON, f)
54
+ if (existsSync(canon)) ok(sha(canon) === distinct[0], f + ' matches the canonical source', sha(canon).slice(0, 16) + ' vs ' + String(distinct[0]).slice(0, 16))
55
+ else console.log(' note canonical source absent (published tree) - four-way comparison only')
56
+ }
57
+
58
+ // ---- 3. both install lines ship the module ----
59
+ {
60
+ const installer = read('installer.js')
61
+ // Parse each preset block: src + dst + (optional comment lines) + files[...]. Windows are not
62
+ // good enough here (the explanatory comment between dst and files pushes the module names past a
63
+ // fixed slice), so the files list is captured explicitly.
64
+ const blocks = {}
65
+ const re = /src:\s*'([^']+)',\s*dst:\s*'[^']+',\s*(?:\/\/[^\n]*\n\s*)*files:\s*\[([^\]]*)\]/g
66
+ let m
67
+ while ((m = re.exec(installer)) !== null) blocks[m[1]] = m[2]
68
+ for (const p of PRESETS) {
69
+ const list = blocks[p]
70
+ ok(!!list, 'installer.js has a PRESETS entry for ' + p)
71
+ for (const f of MODULES) ok(!!list && list.indexOf("'" + f + "'") !== -1, 'installer.js ' + p + ' files[] ships ' + f)
72
+ }
73
+ const pkg = JSON.parse(read('package.json'))
74
+ const files = pkg.files || []
75
+ for (const p of PRESETS) for (const f of MODULES) ok(files.indexOf(p + '/' + f) !== -1, 'package.json files[] ships ' + p + '/' + f)
76
+ ok(files.indexOf('docs/math-computation.md') !== -1, 'package.json files[] ships docs/math-computation.md')
77
+ ok(files.indexOf('tests/audit-math-computation-parity.mjs') !== -1 && files.indexOf('tests/audit-math-computation-contract.mjs') !== -1, 'package.json ships the cross-preset guards')
78
+ ok(files.indexOf('tests/math-computation-shared.test.mjs') !== -1, 'package.json ships the shared contract suite')
79
+ }
80
+
81
+ // ---- 4. the freeze matches tool-schema.json ----
82
+ {
83
+ const mod = await import(new URL('../vibe-math-v2/math-computation.js', import.meta.url).href)
84
+ if (existsSync(SCHEMA)) {
85
+ const s = JSON.parse(readFileSync(SCHEMA, 'utf8'))
86
+ const codes = s.failureCodes.map((c) => c.code)
87
+ ok(JSON.stringify(mod.MATH_FAILURE_CODES) === JSON.stringify(codes), 'failure codes match tool-schema.json', JSON.stringify(mod.MATH_FAILURE_CODES))
88
+ ok(JSON.stringify(mod.MATH_PARAM_NAMES) === JSON.stringify(s.paramNamesFrozen.names), 'param names match tool-schema.json#paramNamesFrozen')
89
+ ok(s.paramNamesFrozen.value === true, 'param names are frozen in the schema')
90
+ ok(!!s.returnShell.argv && s.returnShell.argv.length > 0, 'return shell declares argv (ruling 4: echo)')
91
+ ok(s.failureCodes.some((c) => c.code === 'MATH_ENGINE_BAD_ARGV' && /engine-override/.test(c.next)), 'MATH_ENGINE_BAD_ARGV carries the engine-override next')
92
+ } else {
93
+ console.log(' note tool-schema.json absent (published tree) - freeze compared against the module only')
94
+ }
95
+ ok(mod.MATH_FAILURE_CODES.length === 11, 'eleven failure codes exported')
96
+ ok(mod.MATH_PARAM_NAMES.length === 6, 'six param names exported')
97
+ ok(JSON.stringify(mod.MATH_PARAM_DEFAULTS.mathEngines) === JSON.stringify(['python', 'r', 'octave', 'julia', 'matlab', 'maple', 'wolfram', 'cli']), 'default engine order is the frozen P1 set')
98
+ }
99
+
100
+ // ---- 5. descriptor invariants ----
101
+ {
102
+ const E = await import(new URL('../vibe-math-v2/math-engines.js', import.meta.url).href)
103
+ const order = E.MATH_ENGINE_ORDER
104
+ ok(JSON.stringify(order) === JSON.stringify(['python', 'r', 'octave', 'julia', 'matlab', 'maple', 'wolfram', 'cli']), 'MATH_ENGINE_ORDER is exactly the P1 set')
105
+ ok(E.MATH_ENGINES.cli.defaultOn === true, 'cli is flagged defaultOn')
106
+ ok(E.MATH_ENGINES.cli.policy && E.MATH_ENGINES.cli.policy.requiresMathMode === 'typed+shell' && E.MATH_ENGINES.cli.policy.requiresEngineInList === true, 'cli policy gate declared')
107
+ ok(E.MATH_ENGINES.maple.verify === true && !!E.MATH_ENGINES.maple.verifyReason, 'Maple template is flagged VERIFY with a reason')
108
+ for (const name of ['matlab', 'maple', 'wolfram']) {
109
+ ok(Array.isArray(E.MATH_ENGINES[name].argErrorHints) && E.MATH_ENGINES[name].argErrorHints.length > 0, name + ' ships argErrorHints (bad-argv diagnosis)')
110
+ ok(E.MATH_ENGINES[name].license === 'commercial' && E.MATH_ENGINES[name].install === null, name + ' is commercial and never installable')
111
+ }
112
+ for (const name of ['python', 'r', 'octave', 'julia']) {
113
+ ok(E.MATH_ENGINES[name].license === 'free' && !!E.MATH_ENGINES[name].install, name + ' is free with a user-level install template')
114
+ }
115
+ ok(!!E.MATH_P2_ENGINES.sage && E.MATH_P2_ENGINES.sage.phase === 'P2', 'sage is kept as the P2 descriptor')
116
+ ok(E.mathEngineCandidates('cli') === null, 'cli has no detection candidates')
117
+ }
118
+
119
+ // ── 6. round-3 item 3: the machine-readable refusal vocabulary cannot drift ──────────────────────
120
+ // The docs used to list 5 reasons while the code emitted 12, and nothing pinned the names. This
121
+ // section derives the literals FROM the shipped module and requires the documented lists (docs +
122
+ // tool-schema.json) to be exactly that set - adding/renaming one in the module without updating the
123
+ // docs turns this red.
124
+ {
125
+ const modHref = process.env.MATH_COMPUTATION_MODULE
126
+ ? new URL('file:///' + String(process.env.MATH_COMPUTATION_MODULE).replace(/\\/g, '/'))
127
+ : new URL('../vibe-math-v2/math-computation.js', import.meta.url)
128
+ const { readFileSync: rf } = await import('node:fs')
129
+ const src = rf(fileURLToPath(modHref), 'utf8')
130
+ const kinds = new Set(); for (const m of src.matchAll(/next\('([a-z-]+)'/g)) kinds.add(m[1])
131
+ const reasons = new Set(); for (const m of src.matchAll(/reason:\s*'([a-z-]+)'/g)) reasons.add(m[1])
132
+ const codes = new Set(); for (const m of src.matchAll(/fail\('(MATH_[A-Z_]+)'/g)) codes.add(m[1])
133
+ // round-4 item 1: the "exhaustive" check above is only sound while the vocabulary is written as
134
+ // STRING LITERALS. `next('reason', { reason: SOME_CONST })` would emit a value the extractor cannot
135
+ // see while the exhaustive assertion stayed green. Pin the machine-extractability itself:
136
+ // - every `next(...)` first argument must be a string literal;
137
+ // - every `reason:` field must be a string literal.
138
+ // A non-literal form makes this red (and then the vocabulary lists must be updated to match).
139
+ // Exclude helper DEFINITION lines (`function next(kind, extra)`, `bad(reason, …)`, and any
140
+ // parameterised helper such as `function reasonNext(r) { return next('reason', {reason: r}) }`):
141
+ // those are plumbing whose call sites still pass literals. The literal pattern accepts ANY quoted
142
+ // text (digits/underscores included) - it only asks "can the value be extracted statically?".
143
+ const callSites = src.split(/\r?\n/).filter((l) => !/^\s*(async\s+)?function\s+\w+\s*\(/.test(l)).join('\n')
144
+ const nextFirst = [...callSites.matchAll(/\bnext\(\s*([^,)]*)/g)].map((m) => m[1].trim())
145
+ const nonLiteralKinds = nextFirst.filter((a) => !/^'[^']*'$/.test(a))
146
+ ok(nonLiteralKinds.length === 0, 'every next(kind, …) kind is a STRING LITERAL (machine-extractable vocabulary)', 'non-literal: ' + JSON.stringify(nonLiteralKinds.slice(0, 3)))
147
+ const reasonFields = [...callSites.matchAll(/\breason:\s*([^,}\n]*)/g)].map((m) => m[1].trim())
148
+ const nonLiteralReasons = reasonFields.filter((v) => !/^'[^']*'$/.test(v))
149
+ ok(nonLiteralReasons.length === 0, 'every reason: value is a STRING LITERAL (the exhaustive list cannot be evaded)', 'non-literal: ' + JSON.stringify(nonLiteralReasons.slice(0, 3)))
150
+ const schemaPath = [join(REPO, '_oneoff', 'mc-P1-ready', 'tool-schema.json'), resolve(REPO, '..', '_oneoff', 'mc-P1-ready', 'tool-schema.json')].find((p) => existsSync(p))
151
+ const schema = schemaPath ? JSON.parse(rf(schemaPath, 'utf8')) : null
152
+ const RV = schema && schema.refusalVocabulary
153
+ const same = (a, b) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort())
154
+ if (!RV) {
155
+ console.log(' note refusalVocabulary source absent (published tree) - deriving from the module only')
156
+ } else {
157
+ ok(same(kinds, RV.nextKinds), 'documented next.kind list is EXHAUSTIVE', 'code=' + [...kinds].sort().join(',') + ' doc=' + RV.nextKinds.join(','))
158
+ ok(same(reasons, RV.reasons), 'documented next.reason list is EXHAUSTIVE', 'code=' + [...reasons].sort().join(',') + ' doc=' + RV.reasons.join(','))
159
+ ok(codes.size === 11 && schema.failureCodes.length === 11, 'the emitted failure-code set is the frozen 11')
160
+ }
161
+ const doc = rf(join(REPO, 'docs', 'math-computation.md'), 'utf8')
162
+ for (const k of kinds) ok(doc.indexOf('`' + k + '`') !== -1, 'docs/math-computation.md names next.kind ' + k)
163
+ for (const r of reasons) ok(doc.indexOf('`' + r + '`') !== -1, 'docs/math-computation.md names next.reason ' + r)
164
+ }
165
+
166
+ console.log('')
167
+ console.log('=== MATH COMPUTATION PARITY: ' + passed + ' passed, ' + failed + ' failed ===')
168
+ if (failures.length) for (const f of failures) console.error(' - ' + f)
169
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,231 @@
1
+ // ============================================================================================
2
+ // math_computation — SENSITIVITY probes (integration owner).
3
+ //
4
+ // Proves the shared contract suite is FALSIFIABLE: for each mutation of the module, the suite must
5
+ // go RED for the RIGHT assertion (the probe checks both the exit code and the failing label, so a
6
+ // suite that merely crashes does not count as evidence).
7
+ //
8
+ // Mechanism: copy the canonical module pair into a temp dir, apply ONE textual mutation, then run
9
+ // `tests/math-computation-shared.test.mjs` with MATH_COMPUTATION_MODULE pointed at the mutated copy.
10
+ // The shipped copies are never touched.
11
+ //
12
+ // Usage: node tests/audit-math-computation-sensitivity.mjs
13
+ // A probe PASSES when the mutated run fails AND the expected label is among the failures.
14
+ // ============================================================================================
15
+ import { spawnSync } from 'node:child_process'
16
+ import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { join, resolve } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
22
+ const REPO = resolve(HERE, '..')
23
+ const SRC = join(REPO, 'vibe-math-v2')
24
+ const SUITE = join(HERE, 'math-computation-shared.test.mjs')
25
+
26
+ const PROBES = [
27
+ {
28
+ name: 'cli-default-off',
29
+ file: 'math-computation.js',
30
+ from: 'mathEngines: MATH_ENGINE_ORDER.slice(),',
31
+ to: "mathEngines: MATH_ENGINE_ORDER.filter(function (n) { return n !== 'cli' }),",
32
+ expect: 'cli runs with DEFAULT params (default-on)',
33
+ },
34
+ {
35
+ name: 'cli-policy-ignored',
36
+ file: 'math-computation.js',
37
+ from: "if (p.mathMode !== 'typed+shell') return bad('engine=cli is disabled while mathMode=' + p.mathMode, 'MATH_REFUSED', next('reason', { reason: 'policy' }))",
38
+ to: 'if (false) return bad(\'noop\')',
39
+ expect: 'cli refused while mathMode=typed',
40
+ },
41
+ {
42
+ name: 'argv-echo-removed',
43
+ file: 'math-computation.js',
44
+ from: ' argv: assembled.argv.slice(),\n scriptPath: scriptRel, scriptHash: scriptHash,',
45
+ to: ' scriptPath: scriptRel, scriptHash: scriptHash,',
46
+ expect: 'receipt argv === returned argv',
47
+ },
48
+ {
49
+ name: 'bad-argv-collapsed',
50
+ file: 'math-computation.js',
51
+ from: " const out = fail('MATH_ENGINE_BAD_ARGV', engineLabel,",
52
+ to: " const out = fail('MATH_NONZERO_EXIT', engineLabel,",
53
+ expect: 'usage/option error -> MATH_ENGINE_BAD_ARGV (not collapsed into NONZERO_EXIT)',
54
+ },
55
+ {
56
+ name: 'timeout-not-reported',
57
+ file: 'math-computation.js',
58
+ from: ' if (receipt.timedOut) {',
59
+ to: ' if (false) {',
60
+ expect: 'hang -> MATH_TIMEOUT',
61
+ },
62
+ {
63
+ name: 'param-renamed',
64
+ file: 'math-computation.js',
65
+ from: " 'mathComputation', 'mathMode', 'mathEngines', 'mathTimeoutMs', 'mathPackages', 'mathInstallScope',",
66
+ to: " 'mathComputation', 'mathMode', 'mathEngines', 'mathTimeoutMs', 'mathPkgs', 'mathInstallScope',",
67
+ expect: 'six frozen param names in order',
68
+ },
69
+ {
70
+ name: 'no-subprocess-flag-ignored',
71
+ file: 'math-computation.js',
72
+ from: " if (typeof H.hasSubprocess === 'function' && H.hasSubprocess() === false) {",
73
+ to: ' if (false) {',
74
+ expect: 'host that declares no subprocess -> MATH_NO_SUBPROCESS (not ENGINE_NOT_FOUND)',
75
+ },
76
+ // ── P2a: archive -> edit -> re-run integrity ─────────────────────────────────────────────────
77
+ {
78
+ name: 'script-fields-dropped-from-return',
79
+ file: 'math-computation.js',
80
+ from: ' scriptPath: scriptRel, scriptHash: scriptHash,\n attempt: attempt, attemptDir: dir, baseRunDir: baseDir,\n scriptChanged: scriptChanged, scriptChangedDuringRun: scriptChangedDuringRun,\n sourceFile: fileRel || null,\n previousReceipt: receipt.previousReceipt,\n packages: pk.found,',
81
+ to: ' packages: pk.found,',
82
+ expect: 'scriptPath + scriptHash are in the return value AND the receipt',
83
+ },
84
+ {
85
+ name: 'script-changed-never-flagged',
86
+ file: 'math-computation.js',
87
+ from: " const scriptChanged = !!(mode === 'file' && prevReceipt && prevReceipt.scriptHash && prevReceipt.scriptHash !== scriptHash)",
88
+ to: ' const scriptChanged = false',
89
+ expect: 'edited file -> scriptChanged:true (no silent pass)',
90
+ },
91
+ {
92
+ name: 'mid-run-change-unflagged',
93
+ file: 'math-computation.js',
94
+ from: ' scriptChangedDuringRun = sourceHashAfter !== sourceHashBefore\n }\n if (!r) return fail(\'MATH_NO_SUBPROCESS\'',
95
+ to: ' scriptChangedDuringRun = false\n }\n if (!r) return fail(\'MATH_NO_SUBPROCESS\'',
96
+ expect: 'mid-run edit -> scriptChangedDuringRun:true',
97
+ },
98
+ {
99
+ name: 'archive-overwrite-instead-of-append',
100
+ file: 'math-computation.js',
101
+ from: " const hasFirst = await H.exists(baseDir + '/receipt.json')\n if (!hasFirst) return { attempt: 1, dir: baseDir }\n let n = 2",
102
+ to: " const hasFirst = await H.exists(baseDir + '/receipt.json')\n return { attempt: 1, dir: baseDir }\n let n = 2",
103
+ expect: 'a repeat is appended as attempt 2 (never overwriting attempt 1)',
104
+ },
105
+ {
106
+ name: 'retention-warning-silenced',
107
+ file: 'math-computation.js',
108
+ from: ' if (attempt > MATH_ARCHIVE_MAX_ATTEMPTS_PER_RUN) {',
109
+ to: ' if (false) {',
110
+ expect: 'over the retention cap the tool warns (ARCHIVE_RETENTION_EXCEEDED)',
111
+ },
112
+ {
113
+ name: 'receipt-reconciliation-skipped',
114
+ file: 'math-computation.js',
115
+ from: ' const scriptChanged = scriptMissing || (currentHash !== null && recordedHash !== null && currentHash !== recordedHash)',
116
+ to: ' const scriptChanged = false',
117
+ expect: 'op=receipt reports scriptChanged for an edited archive script',
118
+ },
119
+ // ── audit-C fixes ───────────────────────────────────────────────────────────────────────────
120
+ {
121
+ name: 'archive-allocation-unlocked',
122
+ file: 'math-computation.js',
123
+ // Each call gets its own lock chain, i.e. the per-archive-id serialisation is gone while the
124
+ // call itself is still awaited.
125
+ from: ' return await withArchiveLock(baseDir, async () => {',
126
+ to: " return await withArchiveLock(baseDir + ':' + Math.random(), async () => {",
127
+ expect: '★ 同一 id 的并发运行绝不共用一个 attempt 目录(并发下的 append-only)',
128
+ },
129
+ {
130
+ name: 'deleted-source-unflagged',
131
+ file: 'math-computation.js',
132
+ from: ' scriptChangedDuringRun = sourceHashAfter !== sourceHashBefore',
133
+ to: ' scriptChangedDuringRun = false',
134
+ expect: 'the source file disappearing mid-run is a change: scriptChangedDuringRun:true',
135
+ },
136
+ {
137
+ name: 'missing-archive-script-unflagged',
138
+ file: 'math-computation.js',
139
+ from: ' const scriptChanged = scriptMissing || (currentHash !== null && recordedHash !== null && currentHash !== recordedHash)',
140
+ to: ' const scriptChanged = false',
141
+ expect: 'a deleted archive script makes op=receipt report scriptChanged (fail-closed)',
142
+ },
143
+ {
144
+ name: 'listdir-retention-skipped',
145
+ file: 'math-computation.js',
146
+ from: " if (typeof H.listDir === 'function' && H.listDir) {",
147
+ to: ' if (false) {',
148
+ expect: 'listDir shows more than MATH_ARCHIVE_MAX_RUNS -> ARCHIVE_RETENTION_EXCEEDED (warn only)',
149
+ },
150
+ // ── audit-B fixes ───────────────────────────────────────────────────────────────────────────
151
+ {
152
+ name: 'override-ignored',
153
+ file: 'math-computation.js',
154
+ from: ' const o = ov && isObj(ov) ? ov[det.name] : null\n if (!o) return d',
155
+ to: ' const o = ov && isObj(ov) ? ov[det.name] : null\n if (!o || o) return d',
156
+ expect: 'the overridden scriptArgv really changes the assembled argv',
157
+ },
158
+ {
159
+ name: 'override-not-accepted',
160
+ file: 'math-computation.js',
161
+ from: "'dryRun', 'confirm', 'mathEngineOverride']",
162
+ to: "'dryRun', 'confirm']",
163
+ expect: 'mathEngineOverride is ACCEPTED (previously rejected as an unknown argument)',
164
+ },
165
+ {
166
+ name: 'archive-missing-not-refused',
167
+ file: 'math-computation.js',
168
+ from: " if (json === undefined) return fail('MATH_REFUSED', null, '没有找到回执归档:",
169
+ to: " if (json === undefined) return fail('MATH_INVALID_ARGUMENT', null, '没有找到回执归档:",
170
+ expect: 'op=receipt on a missing archive -> MATH_REFUSED',
171
+ },
172
+ {
173
+ name: 'shell-line-in-all-tiers',
174
+ file: 'math-computation.js',
175
+ from: " const shell = mode === 'typed+shell' ? '\\n' + MATH_SHELL_RULE_LINE : ''",
176
+ to: " const shell = '\\n' + MATH_SHELL_RULE_LINE",
177
+ expect: 'typed tier drops the shell-fallback sentence',
178
+ },
179
+ ]
180
+
181
+ let ok = 0, bad = 0
182
+ console.log('-- math_computation sensitivity probes --')
183
+ console.log('(a probe passes when the mutation makes the shared contract suite RED for the named assertion)')
184
+ console.log('')
185
+
186
+ function runSuite(modulePath) {
187
+ const r = spawnSync(process.execPath, [SUITE], {
188
+ cwd: REPO, encoding: 'utf8',
189
+ env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: modulePath }),
190
+ })
191
+ const out = String(r.stdout || '') + String(r.stderr || '')
192
+ const fails = out.split('\n').filter((l) => /FAIL /.test(l)).map((l) => l.trim())
193
+ return { code: r.status, fails, out }
194
+ }
195
+
196
+ // control: the unmutated canonical pair must be GREEN, otherwise every probe below is meaningless
197
+ {
198
+ const dir = mkdtempSync(join(tmpdir(), 'mc-sens-control-'))
199
+ copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
200
+ copyFileSync(join(SRC, 'math-computation.js'), join(dir, 'math-computation.js'))
201
+ const r = runSuite(join(dir, 'math-computation.js'))
202
+ const tail = r.out.trim().split('\n').filter(Boolean).slice(-1)[0] || ''
203
+ if (r.code === 0) { ok++; console.log(' ok control (unmutated copy) -> suite GREEN ' + tail) }
204
+ else { bad++; console.log(' FAIL control (unmutated copy) went RED: ' + r.fails.slice(0, 3).join(' | ')) }
205
+ rmSync(dir, { recursive: true, force: true })
206
+ }
207
+
208
+ for (const p of PROBES) {
209
+ const dir = mkdtempSync(join(tmpdir(), 'mc-sens-' + p.name + '-'))
210
+ copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
211
+ const src = readFileSync(join(SRC, p.file), 'utf8')
212
+ if (src.indexOf(p.from) === -1) {
213
+ bad++
214
+ console.log(' FAIL ' + p.name + ' — mutation anchor no longer applies (drift): ' + JSON.stringify(p.from.slice(0, 60)))
215
+ rmSync(dir, { recursive: true, force: true })
216
+ continue
217
+ }
218
+ writeFileSync(join(dir, p.file), src.split(p.from).join(p.to))
219
+ const r = runSuite(join(dir, p.file))
220
+ const hit = r.fails.some((f) => f.indexOf(p.expect) !== -1)
221
+ if (r.code !== 0 && hit) { ok++; console.log(' ok ' + p.name + ' -> RED on "' + p.expect + '"') }
222
+ else {
223
+ bad++
224
+ console.log(' FAIL ' + p.name + ' -> exit=' + r.code + ' expectedLabelFound=' + hit + ' fails=' + r.fails.slice(0, 3).join(' | '))
225
+ }
226
+ rmSync(dir, { recursive: true, force: true })
227
+ }
228
+
229
+ console.log('')
230
+ console.log('math_computation sensitivity: ' + ok + ' probe(s) detected the break, ' + bad + ' problem(s)')
231
+ process.exit(bad === 0 ? 0 : 1)