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.
Files changed (106) 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 +758 -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 +2 -0
  14. package/docs/release-notes/README.md +2 -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-notes/RELEASE-NOTES-2.8.1.en.md +49 -0
  36. package/docs/release-notes/RELEASE-NOTES-2.8.1.md +49 -0
  37. package/docs/release-playbook.md +111 -0
  38. package/docs/slv-playbook.md +117 -0
  39. package/docs/test-timing.md +158 -157
  40. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  41. package/installer.js +792 -792
  42. package/package.json +18 -3
  43. package/prompt-corpus-v2/formal-verify-v2.json +2 -2
  44. package/prompt-corpus-v2/formal-verify-v2.md +4 -0
  45. package/prompt-corpus-v3/formal-verify-v3.json +7 -7
  46. package/prompt-corpus-v3/formal-verify-v3.md +14 -0
  47. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  48. package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
  49. package/scripts/clean-temp.mjs +234 -0
  50. package/scripts/release-check.mjs +181 -0
  51. package/tests/audit-engine-faces.mjs +298 -0
  52. package/tests/audit-formal-sensitivity.mjs +345 -342
  53. package/tests/audit-installer-compat.test.mjs +256 -256
  54. package/tests/audit-math-computation-sensitivity.mjs +1 -1
  55. package/tests/audit-persona-sensitivity.mjs +323 -323
  56. package/tests/audit-persona-surface.test.mjs +431 -431
  57. package/tests/audit-prompt-duplication.mutants.mjs +3 -2
  58. package/tests/audit-prompt-invariants.mjs +589 -546
  59. package/tests/audit-registration.mjs +78 -78
  60. package/tests/audit-spec-traceability.mjs +193 -193
  61. package/tests/audit-status-report-fields.mjs +172 -172
  62. package/tests/audit-v5-integrity.mjs +795 -795
  63. package/tests/audit-v5-sensitivity.mjs +433 -432
  64. package/tests/e2e-v5-round2.test.mjs +2453 -2426
  65. package/tests/formal-verify-v2.test.mjs +1806 -1800
  66. package/tests/formal-verify-v3.test.mjs +1757 -1750
  67. package/tests/formal-verify-v4.test.mjs +2114 -2106
  68. package/tests/formal-verify-v5.test.mjs +774 -767
  69. package/tests/math-computation-discovery.mutants.mjs +3 -3
  70. package/tests/math-computation-shared.test.mjs +122 -5
  71. package/tests/math-computation-v2.test.mjs +14 -2
  72. package/tests/math-computation-v3.test.mjs +14 -2
  73. package/tests/math-computation-v4.test.mjs +739 -739
  74. package/tests/prompt-v5-integrity.test.mjs +1291 -1291
  75. package/tests/release-check.mutants.mjs +78 -0
  76. package/tests/run-tests.mjs +393 -332
  77. package/tests/selfdrive-v5.mjs +560 -560
  78. package/tests/temp-hygiene.mutants.mjs +80 -0
  79. package/tests/v2-fix-probes.mutants.mjs +4 -0
  80. package/tests/v2-fix-probes.test.mjs +1034 -1033
  81. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  82. package/tests/v3-fix-probes.mutants.mjs +99 -95
  83. package/tests/v3-fix-probes.test.mjs +891 -890
  84. package/tests/v4-final-paper.mutants.mjs +116 -0
  85. package/tests/v4-final-paper.test.mjs +628 -0
  86. package/tests/v5-institute-fixes.mutants.mjs +218 -190
  87. package/vibe-math-v2/agent.cordis.yml +376 -376
  88. package/vibe-math-v2/math-computation.js +17 -15
  89. package/vibe-math-v2/preset.yml +2 -2
  90. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  91. package/vibe-math-v2/vibe-math-v2.js +5078 -5011
  92. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
  93. package/vibe-math-v3/agent.cordis.yml +420 -420
  94. package/vibe-math-v3/math-computation.js +17 -15
  95. package/vibe-math-v3/preset.yml +2 -2
  96. package/vibe-math-v3/vibe-math-v3.js +6215 -6149
  97. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  98. package/vibe-math-v4/agent.cordis.yml +438 -438
  99. package/vibe-math-v4/math-computation.js +17 -15
  100. package/vibe-math-v4/preset.yml +2 -2
  101. package/vibe-math-v4/vibe-math-v4.js +4520 -4422
  102. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
  103. package/vibe-math-v5/math-computation.js +17 -15
  104. package/vibe-math-v5/vibe-math-v5.js +93 -11
  105. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
  106. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -0,0 +1,111 @@
1
+ // Registered mutant harness for the v2/v3 math-computation suites (task-14 / SLV probe P3): the suites'
2
+ // §19 D1 section already covers "unknown child" and "empty childId" — this family makes the missing one
3
+ // executable: the "host REFUSES to interrupt" branch must keep its NAMED code and its actionable `next`.
4
+ //
5
+ // Measured traps reused from tests/math-computation-v4.mutants.mjs:
6
+ // · the seam must point at the PLUGIN file (`MC_V2_PLUGIN` / `MC_V3_PLUGIN`), and the copied graph must
7
+ // include the plugin's local dependencies, or the suite dies before reaching any assertion;
8
+ // · an anchor that matches a different number of times than expected is a SETUP failure, not a red.
9
+ //
10
+ // Usage: node tests/v2v3-interrupt.mutants.mjs
11
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
12
+ import { execFileSync } from 'node:child_process'
13
+ import { dirname, join } from 'node:path'
14
+ import { tmpdir } from 'node:os'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const HERE = dirname(fileURLToPath(import.meta.url))
18
+ const REPO = join(HERE, '..')
19
+ const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 300000)
20
+ const TIMES = []
21
+ const hangs = []
22
+ const skipped = []
23
+
24
+ function copyGraph(preset, file, dest) {
25
+ const src = join(REPO, preset, file)
26
+ copyFileSync(src, join(dest, file))
27
+ for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
28
+ const dep = m[1].slice(2)
29
+ if (!readdirSync(dest).includes(dep)) copyGraph(preset, dep, dest)
30
+ }
31
+ }
32
+
33
+ function runFamily(f) {
34
+ const dest = join(tmpdir(), f.preset.replace(/[^a-z0-9]/gi, '') + '-mut-' + Math.random().toString(36).slice(2, 10))
35
+ mkdirSync(dest, { recursive: true })
36
+ copyGraph(f.preset, f.main, dest)
37
+ const target = join(dest, f.main)
38
+ // task-7 discipline: anchor matching is done on an EOL-normalised copy, and a leaked CR is a SETUP
39
+ // failure (a lone CR would silently change the anchor's match count).
40
+ let before = readFileSync(target, 'utf8')
41
+ if (/\r/.test(before.replace(/\r\n/g, '\n'))) {
42
+ console.error('SETUP-FAIL - the anchor source was not EOL-normalised (a lone CR leaked in)')
43
+ rmSync(dest, { recursive: true, force: true })
44
+ process.exit(1)
45
+ }
46
+ const n = before.split(f.from).length - 1
47
+ if (n !== 1) {
48
+ skipped.push(f.name + ' (anchor x' + n + ')')
49
+ console.error(' SKIP - ' + f.name + ' (anchor x' + n + ', expected exactly 1)')
50
+ rmSync(dest, { recursive: true, force: true })
51
+ return false
52
+ }
53
+ writeFileSync(target, before.replace(f.from, f.to), 'utf8')
54
+ try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
55
+ catch (e) {
56
+ writeFileSync(target, before, 'utf8')
57
+ skipped.push(f.name + ' (mutation did not compile)')
58
+ console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)')
59
+ rmSync(dest, { recursive: true, force: true })
60
+ return false
61
+ }
62
+ const t0 = Date.now()
63
+ let out = ''
64
+ let code = 0
65
+ let hang = false
66
+ try {
67
+ out = execFileSync(process.execPath, [f.suite], {
68
+ cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL',
69
+ env: Object.assign({}, process.env, { [f.env]: join(dest, f.main) }),
70
+ })
71
+ } catch (e) {
72
+ if (e && (e.killed || e.signal === 'SIGKILL')) hang = true
73
+ code = (e && e.status) || 1
74
+ out = String((e && e.stdout) || '') + String((e && e.stderr) || '')
75
+ }
76
+ const ms = Date.now() - t0
77
+ TIMES.push([f.name, ms])
78
+ if (hang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)')
79
+ const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
80
+ const ok = !hang && code !== 0 && named.length > 0
81
+ console.log((hang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + ' [' + ms + 'ms]' + (named.length ? ' :: ' + named[0].slice(0, 150) : ' :: no named red (exit=' + code + ')'))
82
+ rmSync(dest, { recursive: true, force: true })
83
+ return ok
84
+ }
85
+
86
+ const FAMILIES = [
87
+ {
88
+ name: 'P3/v2: the host-refused-interrupt code is renamed (the named failure disappears)',
89
+ preset: 'vibe-math-v2', main: 'vibe-math-v2.js',
90
+ suite: 'tests/math-computation-v2.test.mjs', env: 'MC_V2_PLUGIN',
91
+ from: 'VIBE_MATH_INTERRUPT_FAILED', to: 'VIBE_MATH_INTERRUPT_RENAMED',
92
+ expect: /\[P3\/v2\] 宿主拒绝中断/,
93
+ },
94
+ {
95
+ name: 'P3/v3: the host-refused-interrupt code is renamed (the named failure disappears)',
96
+ preset: 'vibe-math-v3', main: 'vibe-math-v3.js',
97
+ suite: 'tests/math-computation-v3.test.mjs', env: 'MC_V3_PLUGIN',
98
+ from: 'VIBE_MATH_INTERRUPT_FAILED', to: 'VIBE_MATH_INTERRUPT_RENAMED',
99
+ expect: /\[P3\/v3\] 宿主拒绝中断/,
100
+ },
101
+ ]
102
+
103
+ let red = 0
104
+ for (const f of FAMILIES) { if (runFamily(f)) red++ }
105
+ console.log('')
106
+ console.log('mutant families reddening the v2/v3 host-refused-interrupt guard by name: ' + red + '/' + FAMILIES.length)
107
+ console.log('skipped=[' + skipped.join(' | ') + ']')
108
+ console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
109
+ console.log('hangs=[' + hangs.join(' | ') + ']')
110
+ if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
111
+ console.log('ALL MUTANTS RED AS REQUIRED')
@@ -1,95 +1,99 @@
1
- // Registered mutant harness for tests/v3-fix-probes.test.mjs — the probe carries its own proof.
2
- // Defect class covered: silent-failure surfacing (F6a/F6b/F6c), cap sharing (F2cap), declared asymmetry (F-A),
3
- // fence-root drift (F5), shell-fallback silence (F4c). Every family is a SINGLE content-anchored mutation
4
- // of the v3 preset (no line numbers) and the suite runs through its V3_PLUGIN seam.
5
- // Exit criterion (audit-path-discipline.mutants.mjs paradigm): every family must redden BY NAME;
6
- // ALL MUTANTS RED AS REQUIRED + exit 0 only when all do.
7
- // Run: node tests/v3-fix-probes.mutants.mjs
8
- import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
9
- import { execFileSync } from 'node:child_process'
10
- import { dirname, join } from 'node:path'
11
- import { tmpdir } from 'node:os'
12
- import { fileURLToPath } from 'node:url'
13
- const HERE = dirname(fileURLToPath(import.meta.url))
14
- const REPO = join(HERE, '..')
15
- const PRESET = 'vibe-math-v3'
16
- const SUITE = 'tests/v3-fix-probes.test.mjs'
17
- const ENV = 'V3_PLUGIN'
18
- // Bound every child run: run-tests has no per-suite timeout, so an unbounded child hangs the whole gate.
19
- // A timeout is reported as HANG, counts as a failure for that family, and is listed in hangs=[].
20
- const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 120000)
21
- const TIMES = []
22
- // R15: whole-process wall time, printed at the end (the timeout row quotes this line).
23
- const START_MS = Date.now()
24
- const hangs = []
25
- let lastHang = false
26
- function copyGraph(file, dest) {
27
- const src = join(REPO, PRESET, file)
28
- copyFileSync(src, join(dest, file))
29
- for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
30
- const dep = m[1].slice(2)
31
- if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
32
- }
33
- }
34
- function runFamily(f) {
35
- const dest = join(tmpdir(), 'v3probes-mut-' + Math.random().toString(36).slice(2, 10))
36
- mkdirSync(dest, { recursive: true })
37
- copyGraph(PRESET + '.js', dest)
38
- const target = join(dest, f.editFile || (PRESET + '.js'))
39
- const before = readFileSync(target, 'utf8')
40
- const n = before.split(f.from).length - 1
41
- if (f.lastOnly ? n < 1 : n !== 1) { console.error(' NO-OP - ' + f.name + ' (anchor x' + n + (f.lastOnly ? ', lastOnly' : '') + ')'); rmSync(dest, { recursive: true, force: true }); return false }
42
- const mutated = f.lastOnly
43
- ? (() => { const i = before.lastIndexOf(f.from); return before.slice(0, i) + f.to + before.slice(i + f.from.length) })()
44
- : before.split(f.from).join(f.to)
45
- writeFileSync(target, mutated, 'utf8')
46
- try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
47
- catch (e) { writeFileSync(target, before, 'utf8'); console.error(' FAIL - ' + f.name + ' (mutation did not compile; restored)'); rmSync(dest, { recursive: true, force: true }); return false }
48
- let out = ''
49
- let code = 0
50
- try { out = execFileSync(process.execPath, [SUITE], { cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL', env: Object.assign({}, process.env, { [ENV]: join(dest, PRESET + '.js') }) }) }
51
- catch (e) { if (e && (e.killed || e.signal === 'SIGKILL')) lastHang = true; code = (e && e.status) || 1; out = String((e && e.stdout) || '') + String((e && e.stderr) || '') }
52
- const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
53
- const ok = !lastHang && code !== 0 && named.length > 0
54
- console.log((lastHang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + (named.length ? ' :: ' + named[0].slice(0, 140) : ' :: no named red (exit=' + code + ')'))
55
- rmSync(dest, { recursive: true, force: true })
56
- return ok
57
- }
58
- const FAMILIES = [
59
- { name: 'F2cap: status site reverted to a literal 10', expect: /\[F2cap\] status 与 report 两处都走同一常量/,
60
- from: "recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)), params: params,",
61
- to: "recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params," },
62
- { name: 'F2cap: param description hard-codes the number', expect: /\[F2cap\] 参数描述\*\*引用\*\*该常量/,
63
- from: "' + ACTIVITY_REPORT_MAX + ' 条)', suggestion: 100 },", to: "' + 30 + ' 条)', suggestion: 100 }," },
64
- { name: 'F-A: status gains pendingDecisionItems', expect: /\[F-A\] status \*\*不得\*\*携带明细/,
65
- from: "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,",
66
- to: "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,\n pendingDecisionItems: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),",
67
- lastOnly: true },
68
- { name: 'F5: fence-root comparison removed', expect: /\[F5\/v3\] 围栏根漂移/,
69
- from: 'if (actual && expected && actual !== expected) warnFenceDriftOnce(actual, expected)', to: 'void actual' },
70
- { name: 'F4c: ensureDirs failure warning removed', expect: /\[F4c\] ensureDirs 失败不是沉默/,
71
- from: "if (!r || !r.ok) warnShellOnce('ensureDirs (mkdir ' + base + ')', r)", to: 'void r' },
72
- { name: 'F6a: provider list-failure warning removed', expect: /\[F6a\/v3\]/,
73
- from: "warnProviderFallback('subagents.list() failed: ' + ((e && e.message) || e))", to: 'void e' },
74
- { name: 'F6b: interrupt emit statement deleted (anchored by content)', expect: /\[F6b\/v3\]/,
75
- from: "if (r && r.ok === false) logActivity('interrupt', '中断失败(' + why + '):' + String(cid) + ' — ' + String(r.message || r.code || ''))", to: 'void r' },
76
- { name: 'F6c: project-pointer write failure re-silenced', expect: /\[F6c\/v3\]/,
77
- from: "noteStateWriteFailure('current.' + safeId(sessionId) + '.json', (e && e.message) || e)", to: 'void e' },
78
- ]
79
- let red = 0
80
- FAMILIES.push(
81
- { name: 'F-B: derivation drops the source descriptions', expect: /\[F-B\/v3\] 每个机读描述都来自真源且非空/,
82
- from: 'if (e.description) p.description = e.description', to: 'void e /* MUTANT F-B: no source description */' },
83
- { name: 'F-B: one exception entry deleted', expect: /\[F-B\/v3\] 例外集合 == docs\/parameter-schema\.md/,
84
- from: "mode: { type: 'string', enum: ['manual', 'auto'] },", to: '' },
85
- { name: 'F-B: one registration site reverted to a literal', expect: /\[F-B\/v3\] 两处注册点都由 paramProps\(\)/,
86
- from: "objParams(paramProps()), 'vibe_math_set_params')", to: "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }), 'vibe_math_set_params')" },
87
- )
88
- for (const f of FAMILIES) { const t0 = Date.now(); lastHang = false; const ok = runFamily(f); const ms = Date.now() - t0; TIMES.push([f.name, ms]); if (lastHang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)'); if (ok) red++ }
89
- console.log('')
90
- console.log('mutant families reddening the v3 probe by name: ' + red + '/' + FAMILIES.length)
91
- console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
92
- console.log('TOTAL WALL TIME (all families + setup): ' + (Date.now() - START_MS) + 'ms (' + Math.round((Date.now() - START_MS) / 1000) + 's)')
93
- console.log('hangs=[' + hangs.join(' | ') + ']')
94
- if (red !== FAMILIES.length || hangs.length) process.exit(1)
95
- console.log('ALL MUTANTS RED AS REQUIRED')
1
+ // Registered mutant harness for tests/v3-fix-probes.test.mjs — the probe carries its own proof.
2
+ // Defect class covered: silent-failure surfacing (F6a/F6b/F6c), cap sharing (F2cap), declared asymmetry (F-A),
3
+ // fence-root drift (F5), shell-fallback silence (F4c). Every family is a SINGLE content-anchored mutation
4
+ // of the v3 preset (no line numbers) and the suite runs through its V3_PLUGIN seam.
5
+ // Exit criterion (audit-path-discipline.mutants.mjs paradigm): every family must redden BY NAME;
6
+ // ALL MUTANTS RED AS REQUIRED + exit 0 only when all do.
7
+ // Run: node tests/v3-fix-probes.mutants.mjs
8
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
9
+ import { execFileSync } from 'node:child_process'
10
+ import { dirname, join } from 'node:path'
11
+ import { tmpdir } from 'node:os'
12
+ import { fileURLToPath } from 'node:url'
13
+ const HERE = dirname(fileURLToPath(import.meta.url))
14
+ const REPO = join(HERE, '..')
15
+ const PRESET = 'vibe-math-v3'
16
+ const SUITE = 'tests/v3-fix-probes.test.mjs'
17
+ const ENV = 'V3_PLUGIN'
18
+ // Bound every child run: run-tests has no per-suite timeout, so an unbounded child hangs the whole gate.
19
+ // A timeout is reported as HANG, counts as a failure for that family, and is listed in hangs=[].
20
+ const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 120000)
21
+ const TIMES = []
22
+ // R15: whole-process wall time, printed at the end (the timeout row quotes this line).
23
+ const START_MS = Date.now()
24
+ const hangs = []
25
+ let lastHang = false
26
+ function copyGraph(file, dest) {
27
+ const src = join(REPO, PRESET, file)
28
+ copyFileSync(src, join(dest, file))
29
+ for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
30
+ const dep = m[1].slice(2)
31
+ if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
32
+ }
33
+ }
34
+ function runFamily(f) {
35
+ const dest = join(tmpdir(), 'v3probes-mut-' + Math.random().toString(36).slice(2, 10))
36
+ mkdirSync(dest, { recursive: true })
37
+ copyGraph(PRESET + '.js', dest)
38
+ const target = join(dest, f.editFile || (PRESET + '.js'))
39
+ const before = readFileSync(target, 'utf8')
40
+ const n = before.split(f.from).length - 1
41
+ if (f.lastOnly ? n < 1 : n !== 1) { console.error(' NO-OP - ' + f.name + ' (anchor x' + n + (f.lastOnly ? ', lastOnly' : '') + ')'); rmSync(dest, { recursive: true, force: true }); return false }
42
+ const mutated = f.lastOnly
43
+ ? (() => { const i = before.lastIndexOf(f.from); return before.slice(0, i) + f.to + before.slice(i + f.from.length) })()
44
+ : before.split(f.from).join(f.to)
45
+ writeFileSync(target, mutated, 'utf8')
46
+ try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
47
+ catch (e) { writeFileSync(target, before, 'utf8'); console.error(' FAIL - ' + f.name + ' (mutation did not compile; restored)'); rmSync(dest, { recursive: true, force: true }); return false }
48
+ let out = ''
49
+ let code = 0
50
+ try { out = execFileSync(process.execPath, [SUITE], { cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL', env: Object.assign({}, process.env, { [ENV]: join(dest, PRESET + '.js') }) }) }
51
+ catch (e) { if (e && (e.killed || e.signal === 'SIGKILL')) lastHang = true; code = (e && e.status) || 1; out = String((e && e.stdout) || '') + String((e && e.stderr) || '') }
52
+ const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
53
+ const ok = !lastHang && code !== 0 && named.length > 0
54
+ console.log((lastHang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + (named.length ? ' :: ' + named[0].slice(0, 140) : ' :: no named red (exit=' + code + ')'))
55
+ rmSync(dest, { recursive: true, force: true })
56
+ return ok
57
+ }
58
+ const FAMILIES = [
59
+ { name: 'F2cap: status site reverted to a literal 10', expect: /\[F2cap\] status 与 report 两处都走同一常量/,
60
+ from: "recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)), params: params,",
61
+ to: "recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params," },
62
+ { name: 'F2cap: param description hard-codes the number', expect: /\[F2cap\] 参数描述\*\*引用\*\*该常量/,
63
+ from: "' + ACTIVITY_REPORT_MAX + ' 条)', suggestion: 100 },", to: "' + 30 + ' 条)', suggestion: 100 }," },
64
+ { name: 'F-A: status gains pendingDecisionItems', expect: /\[F-A\] status \*\*不得\*\*携带明细/,
65
+ from: "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,",
66
+ to: "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,\n pendingDecisionItems: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),",
67
+ lastOnly: true },
68
+ { name: 'F5: fence-root comparison removed', expect: /\[F5\/v3\] 围栏根漂移/,
69
+ from: 'if (actual && expected && actual !== expected) warnFenceDriftOnce(actual, expected)', to: 'void actual' },
70
+ { name: 'F4c: ensureDirs failure warning removed', expect: /\[F4c\] ensureDirs 失败不是沉默/,
71
+ from: "if (!r || !r.ok) warnShellOnce('ensureDirs (mkdir ' + base + ')', r)", to: 'void r' },
72
+ { name: 'F6a: provider list-failure warning removed', expect: /\[F6a\/v3\]/,
73
+ from: "warnProviderFallback('subagents.list() failed: ' + ((e && e.message) || e))", to: 'void e' },
74
+ { name: 'F6b: interrupt emit statement deleted (anchored by content)', expect: /\[F6b\/v3\]/,
75
+ from: "if (r && r.ok === false) logActivity('interrupt', '中断失败(' + why + '):' + String(cid) + ' — ' + String(r.message || r.code || ''))", to: 'void r' },
76
+ { name: 'F6c: project-pointer write failure re-silenced', expect: /\[F6c\/v3\]/,
77
+ from: "noteStateWriteFailure('current.' + safeId(sessionId) + '.json', (e && e.message) || e)", to: 'void e' },
78
+ ]
79
+ let red = 0
80
+ FAMILIES.push(
81
+ { name: 'F-B: derivation drops the source descriptions', expect: /\[F-B\/v3\] 每个机读描述都来自真源且非空/,
82
+ from: 'if (e.description) p.description = e.description', to: 'void e /* MUTANT F-B: no source description */' },
83
+ { name: 'F-B: one exception entry deleted', expect: /\[F-B\/v3\] 例外集合 == docs\/parameter-schema\.md/,
84
+ from: "mode: { type: 'string', enum: ['manual', 'auto'] },", to: '' },
85
+ { name: 'F-B: one registration site reverted to a literal', expect: /\[F-B\/v3\] 两处注册点都由 paramProps\(\)/,
86
+ from: "objParams(paramProps()), 'vibe_math_set_params')", to: "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }), 'vibe_math_set_params')" },
87
+ )
88
+ FAMILIES.push(
89
+ { name: 'task-9/v3: the not-detected log drops the actionable sentence', expect: /\[task-9\/v3\]/,
90
+ from: '已探测 PATH 与文档化的常见 TeX 根;可用 paperLatexCommand 指定绝对路径。', to: '' },
91
+ )
92
+ for (const f of FAMILIES) { const t0 = Date.now(); lastHang = false; const ok = runFamily(f); const ms = Date.now() - t0; TIMES.push([f.name, ms]); if (lastHang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)'); if (ok) red++ }
93
+ console.log('')
94
+ console.log('mutant families reddening the v3 probe by name: ' + red + '/' + FAMILIES.length)
95
+ console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
96
+ console.log('TOTAL WALL TIME (all families + setup): ' + (Date.now() - START_MS) + 'ms (' + Math.round((Date.now() - START_MS) / 1000) + 's)')
97
+ console.log('hangs=[' + hangs.join(' | ') + ']')
98
+ if (red !== FAMILIES.length || hangs.length) process.exit(1)
99
+ console.log('ALL MUTANTS RED AS REQUIRED')