dsh-vibe-math 2.7.2 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -0,0 +1,132 @@
1
+ // Registered mutant harness for the v5 self-drive suite — the suite carries its own proof.
2
+ // Mirrors tests/formal-verify-v4.mutants.mjs / formal-verify-v3.mutants.mjs: a SINGLE content-anchored
3
+ // mutation of the v5 preset (no line numbers), run through the V5_PLUGIN seam, bounded child runs (a
4
+ // timeout is a HANG, never a pass), skipped families printed and counted, and the family's TOTAL WALL
5
+ // TIME reported (per-family numbers alone can hide a timeout).
6
+ // Run: node tests/selfdrive-v5.mutants.mjs
7
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
8
+ import { execFileSync } from 'node:child_process'
9
+ import { dirname, join } from 'node:path'
10
+ import { tmpdir } from 'node:os'
11
+ import { fileURLToPath } from 'node:url'
12
+ const HERE = dirname(fileURLToPath(import.meta.url))
13
+ const REPO = join(HERE, '..')
14
+ const PRESET = 'vibe-math-v5'
15
+ const MAIN = PRESET + '.js'
16
+ const SUITE = 'tests/selfdrive-v5.mjs'
17
+ const ENV = 'V5_PLUGIN'
18
+ // Measured: a selfdrive-v5 child run is ~2 s; MUTANT_CHILD_TIMEOUT_MS overrides it.
19
+ const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 300000)
20
+ const TIMES = []
21
+ const hangs = []
22
+ const skipped = []
23
+ function copyGraph(file, dest) {
24
+ const src = join(REPO, PRESET, file)
25
+ copyFileSync(src, join(dest, file))
26
+ for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
27
+ const dep = m[1].slice(2)
28
+ if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
29
+ }
30
+ }
31
+ function runFamily(f) {
32
+ const dest = join(tmpdir(), 'v5sd-mut-' + Math.random().toString(36).slice(2, 10))
33
+ mkdirSync(dest, { recursive: true })
34
+ copyGraph(MAIN, dest)
35
+ const target = join(dest, f.editFile || MAIN)
36
+ const before = readFileSync(target, 'utf8')
37
+ const n = before.split(f.from).length - 1
38
+ const okCount = f.lastOnly || f.all ? n >= 1 : n === 1
39
+ if (!okCount) {
40
+ skipped.push(f.name + ' (anchor x' + n + ')')
41
+ console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')')
42
+ rmSync(dest, { recursive: true, force: true })
43
+ return false
44
+ }
45
+ writeFileSync(target, before.split(f.from).join(f.to), 'utf8')
46
+ try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
47
+ catch (e) {
48
+ writeFileSync(target, before, 'utf8')
49
+ skipped.push(f.name + ' (mutation did not compile)')
50
+ console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)')
51
+ rmSync(dest, { recursive: true, force: true })
52
+ return false
53
+ }
54
+ const t0 = Date.now()
55
+ let out = ''
56
+ let code = 0
57
+ let hang = false
58
+ try {
59
+ out = execFileSync(process.execPath, [SUITE], {
60
+ cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL',
61
+ env: Object.assign({}, process.env, { [ENV]: join(dest, MAIN) }),
62
+ })
63
+ } catch (e) {
64
+ if (e && (e.killed || e.signal === 'SIGKILL')) hang = true
65
+ code = (e && e.status) || 1
66
+ out = String((e && e.stdout) || '') + String((e && e.stderr) || '')
67
+ }
68
+ const ms = Date.now() - t0
69
+ TIMES.push([f.name, ms])
70
+ if (hang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)')
71
+ const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
72
+ const ok = !hang && code !== 0 && named.length > 0
73
+ console.log((hang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + ' [' + ms + 'ms]' + (named.length ? ' :: ' + named[0].slice(0, 150) : ' :: no named red (exit=' + code + ')'))
74
+ rmSync(dest, { recursive: true, force: true })
75
+ return ok
76
+ }
77
+ const FAMILIES = [
78
+ {
79
+ // V5-A1: the LAST voter is replaced by a DUPLICATE of the first, so P stays 4 (the count assertion
80
+ // passes) while the voter id SET loses a member - the roster identity, not the head-count.
81
+ name: 'V5-A1: the last voter is a duplicate of the first (P still 4)',
82
+ from: "const voters = () => activeMembers().filter((m) => m.kind === 'academician' || m.kind === 'researcher')",
83
+ to: "const voters = () => { const v = activeMembers().filter((m) => m.kind === 'academician' || m.kind === 'researcher'); return v.length > 1 ? v.slice(0, -1).concat([v[0]]) : v }",
84
+ expect: /V5-A1 the voter id SET is exactly/,
85
+ },
86
+ {
87
+ // F6 tightening (roster): the published voter set drops one member while its COUNT stays put.
88
+ name: 'V5-A2: the published voter set disagrees with its own count',
89
+ from: 'voters: vs.map((m) => m.id),',
90
+ to: 'voters: vs.slice(1).map((m) => m.id),',
91
+ expect: /V5-A2 the published voter SET agrees with its own count/,
92
+ },
93
+ {
94
+ // F6 tightening (staff): the member persona ignores `params.staffPersona` again.
95
+ name: 'V5-A3: the staff persona never reaches the prompts',
96
+ from: "const extra = String(params.staffPersona || '').trim()",
97
+ to: "const extra = ''",
98
+ expect: /V5-A3 the staff persona reaches the member persona\/prompts/,
99
+ },
100
+ {
101
+ // ④ CAS: the stale-revision guard is disabled, so a stale update lands (the return-value
102
+ // assertions AND the new "unchanged after refusal" assertion redden).
103
+ // R17 scope refinement for V5-A4: it detects any refused-but-written change VISIBLE IN THE
104
+ // OBSERVABLE TRIPLE (revision|ownerId|status) - measured: an in-branch `task.status='done'`
105
+ // makes it RED (`before=1||pending after=1||done`), while a write OUTSIDE the triple (e.g.
106
+ // `task.notes`) keeps it green and remains a MEASURED BOUNDARY (invisible to the harness).
107
+ // assertions AND the new "unchanged after refusal" assertion redden).
108
+ name: 'V5-A4: the stale-revision guard is disabled (a stale CAS lands)',
109
+ from: 'if (expected !== task.revision) {',
110
+ to: 'if (false) {',
111
+ expect: /V5-A4 a REFUSED stale CAS leaves the task UNCHANGED|a stale revision is refused/,
112
+ },
113
+ {
114
+ // ⑤ path contract: ONLY the documented composition moves (instRootRel), while the framework
115
+ // keeps writing through instRoot() - exactly the v4 58fff7f defect class this guard exists for.
116
+ name: 'V5-A5: the documented member root diverges from the written root',
117
+ from: "const instRootRel = () => 'VibeMath/Projects/' + project + '/Institutes/' + instituteName",
118
+ to: "const instRootRel = () => 'VibeMath/Projects/' + project + '/Institutes/' + instituteName + 'X'",
119
+ expect: /V5-A5 the documented member root equals the root the framework writes/
120
+ },
121
+ ]
122
+ let red = 0
123
+ for (const f of FAMILIES) { const ok = runFamily(f); if (ok) red++ }
124
+ const totalMs = TIMES.reduce((a, t) => a + t[1], 0)
125
+ console.log('')
126
+ console.log('mutant families reddening the v5 self-drive suite by name: ' + red + '/' + FAMILIES.length)
127
+ console.log('timings (per family): ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
128
+ console.log('TOTAL WALL TIME (all families + setup): ' + totalMs + 'ms (' + Math.round(totalMs / 1000) + 's)')
129
+ console.log('hangs=[' + hangs.join(' | ') + ']')
130
+ console.log('skipped=[' + skipped.join(' | ') + ']')
131
+ if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
132
+ console.log('ALL MUTANTS RED AS REQUIRED')
@@ -0,0 +1,80 @@
1
+ // Falsifiability driver for scripts/clean-temp.mjs (task-12): a SINGLE-SITE edit in the sweeper must
2
+ // turn its own guard (`--self-test`) NAMED-red, while the unmutated control stays green. The mutated
3
+ // copy is written as a DOT-SIBLING inside scripts/ so the module still resolves REPO/tests correctly
4
+ // (a copy in %TEMP% would fail the prefix-extraction check for the wrong reason), and it is always
5
+ // removed in `finally`.
6
+ import { spawnSync } from 'node:child_process'
7
+ import { readFileSync, rmSync, writeFileSync } from 'node:fs'
8
+ import { dirname, join } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const HERE = dirname(fileURLToPath(import.meta.url))
12
+ const REPO = join(HERE, '..')
13
+ const SRC = join(REPO, 'scripts', 'clean-temp.mjs')
14
+
15
+ const FAMILIES = [
16
+ {
17
+ // The age threshold is the whole safety property: without it every suite dir looks deletable.
18
+ name: 'the age threshold is dropped (a FRESH suite dir starts looking stale)',
19
+ from: '.filter((e) => Number(e.mtimeMs) < cut)',
20
+ to: '.filter(() => true)',
21
+ expect: /a FRESH suite dir is never swept/,
22
+ },
23
+ {
24
+ // The prefix test keeps the sweeper from touching ANY top-level directory of the temp root.
25
+ name: 'the suite-prefix test is dropped (any top-level dir starts looking like ours)',
26
+ from: 'prefixes.some((p) => String(e.name).startsWith(p))',
27
+ to: 'true',
28
+ expect: /only exact suite prefixes are swept/,
29
+ },
30
+ {
31
+ // task-15: "parent is gone" is the safety property - dropping it makes OUR OWN running gate look like
32
+ // a leftover, which is exactly the false positive that would make this tool dangerous to auto-run.
33
+ name: 'the parent-gone test is dropped (a live-parented gate looks like a leftover)',
34
+ from: 'return e.parentGone === true',
35
+ to: 'return true',
36
+ expect: /a process with a LIVE parent .* is never called a leftover/,
37
+ },
38
+ {
39
+ // task-15: the command-line gate is what keeps unrelated programs (editors, other stacks) out.
40
+ name: 'the command-line gate is dropped (any orphaned program looks like ours)',
41
+ from: "if (!patterns.some((re) => re.test(String(e.cmd || '')))) return false",
42
+ to: 'if (false) return false',
43
+ expect: /an unrelated program is never reported/,
44
+ },
45
+ ]
46
+
47
+ function runSelftest(scriptPath) {
48
+ const r = spawnSync(process.execPath, [scriptPath, '--self-test'], { encoding: 'utf8', timeout: 60000, cwd: REPO })
49
+ return { code: r.status, out: String(r.stdout || '') + String(r.stderr || '') }
50
+ }
51
+
52
+ const control = runSelftest(SRC)
53
+ console.log('control (unmutated sweeper): exit=' + control.code + (control.code === 0 ? ' green, as required' : ' NOT GREEN'))
54
+ if (control.code !== 0) console.log(control.out.split(/\r?\n/).filter((l) => /FAIL/.test(l)).slice(0, 3).join('\n'))
55
+
56
+ let red = 0
57
+ for (const f of FAMILIES) {
58
+ const dest = join(REPO, 'scripts', '.tmp-hygiene-' + Math.random().toString(36).slice(2, 10) + '.mjs')
59
+ let ok = false
60
+ try {
61
+ let text = readFileSync(SRC, 'utf8').replace(/\r\n?/g, '\n')
62
+ if (/\r/.test(text)) { console.error('SETUP-FAIL - the anchor source was not EOL-normalised (CR leaked into anchor matching)'); process.exit(1) }
63
+ const hits = text.split(f.from).length - 1
64
+ if (hits !== 1) { console.error('SETUP-FAIL - anchor ' + JSON.stringify(f.from) + ' matched ' + hits + ' time(s), expected exactly 1'); process.exit(1) }
65
+ writeFileSync(dest, text.replace(f.from, f.to), 'utf8')
66
+ const r = runSelftest(dest)
67
+ const named = r.out.split(/\r?\n/).filter((l) => /^\s*FAIL - /.test(l) && f.expect.test(l)).map((l) => l.trim())
68
+ ok = r.code !== 0 && named.length > 0
69
+ console.log((ok ? ' ok - ' : ' FAIL - ') + f.name + ' [exit=' + r.code + ']' + (named.length ? ' :: ' + named[0].slice(0, 140) : ' :: no named red (exit=' + r.code + ')'))
70
+ if (ok) red++
71
+ } finally {
72
+ try { rmSync(dest, { force: true }) } catch (e) { /* best effort */ }
73
+ }
74
+ }
75
+
76
+ console.log('')
77
+ console.log('mutant families reddening the temp-hygiene guard by name: ' + red + '/' + FAMILIES.length)
78
+ console.log('control green: ' + (control.code === 0))
79
+ if (red !== FAMILIES.length || control.code !== 0) process.exit(1)
80
+ console.log('ALL MUTANTS RED AS REQUIRED')
@@ -0,0 +1,104 @@
1
+ // Registered mutant harness for tests/v2-fix-probes.test.mjs — the probe carries its own proof.
2
+ // Defect classes: surface-shape contract (F1), declared asymmetry (F-A), path-base mixing (F6),
3
+ // silent-failure surfacing (F6a/F6b/F6c). Single content-anchored mutations; V2_PLUGIN seam.
4
+ // Exit criterion: every family reddens BY NAME; any skipped family is printed and counted (never a
5
+ // silent NO-OP impersonating a pass); ALL MUTANTS RED AS REQUIRED + exit 0 only then.
6
+ // Run: node tests/v2-fix-probes.mutants.mjs
7
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
8
+ import { execFileSync } from 'node:child_process'
9
+ import { dirname, join } from 'node:path'
10
+ import { tmpdir } from 'node:os'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ // Bound every child run: run-tests has no per-suite timeout, so an unbounded child hangs the gate.
14
+ // A timeout is reported as HANG, counts as a failure for that family, and is listed in hangs=[].
15
+ const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 120000)
16
+ const TIMES = []
17
+ // R15: whole-process wall time, printed at the end (the timeout row quotes this line).
18
+ const START_MS = Date.now()
19
+ const hangs = []
20
+ let lastHang = false
21
+ const HERE = dirname(fileURLToPath(import.meta.url))
22
+ const REPO = join(HERE, '..')
23
+ const PRESET = 'vibe-math-v2'
24
+ const MAIN = PRESET + '.js'
25
+ const SUITE = 'tests/v2-fix-probes.test.mjs'
26
+ const ENV = 'V2_PLUGIN'
27
+ function copyGraph(file, dest) {
28
+ const src = join(REPO, PRESET, file)
29
+ copyFileSync(src, join(dest, file))
30
+ for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
31
+ const dep = m[1].slice(2)
32
+ if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
33
+ }
34
+ }
35
+ const skipped = []
36
+ function runFamily(f) {
37
+ const dest = join(tmpdir(), 'v2probes-mut-' + Math.random().toString(36).slice(2, 10))
38
+ mkdirSync(dest, { recursive: true })
39
+ copyGraph(MAIN, dest)
40
+ const target = join(dest, f.editFile || MAIN)
41
+ const before = readFileSync(target, 'utf8')
42
+ const n = before.split(f.from).length - 1
43
+ const okCount = f.lastOnly || f.firstOnly || f.all ? n >= 1 : n === 1
44
+ if (!okCount) { skipped.push(f.name + ' (anchor x' + n + ')'); console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')'); rmSync(dest, { recursive: true, force: true }); return false }
45
+ let mutated
46
+ if (f.lastOnly) { const i = before.lastIndexOf(f.from); mutated = before.slice(0, i) + f.to + before.slice(i + f.from.length) }
47
+ else if (f.firstOnly) { const i = before.indexOf(f.from); mutated = before.slice(0, i) + f.to + before.slice(i + f.from.length) }
48
+ else if (f.all) mutated = before.split(f.from).join(f.to)
49
+ else mutated = before.split(f.from).join(f.to)
50
+ writeFileSync(target, mutated, 'utf8')
51
+ try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
52
+ catch (e) { writeFileSync(target, before, 'utf8'); skipped.push(f.name + ' (mutation did not compile)'); console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)'); rmSync(dest, { recursive: true, force: true }); return false }
53
+ let out = ''
54
+ let code = 0
55
+ try { out = execFileSync(process.execPath, [SUITE], { cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL', env: Object.assign({}, process.env, { [ENV]: join(dest, MAIN) }) }) }
56
+ 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) || '') }
57
+ const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
58
+ const ok = code !== 0 && named.length > 0
59
+ console.log((ok ? ' ok - ' : ' FAIL - ') + f.name + (named.length ? ' :: ' + named[0].slice(0, 130) : ' :: no named red (exit=' + code + ')'))
60
+ rmSync(dest, { recursive: true, force: true })
61
+ return ok
62
+ }
63
+ const ANCHOR_LEN = "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,"
64
+ const FAMILIES = [
65
+ { name: 'F1: report pendingDecisions back to an array', expect: /\[F1\] pendingDecisions 两面同形/, from: ANCHOR_LEN, firstOnly: true,
66
+ to: "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } })," },
67
+ { name: 'F-A: status gains pendingDecisionItems', expect: /\[F-A\] status \*\*不得\*\*携带明细/, from: ANCHOR_LEN,
68
+ to: ANCHOR_LEN + "\n pendingDecisionItems: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),", lastOnly: true },
69
+ { name: 'F6: paths base dropped from both surfaces', expect: /\[F6\] status\.formal\.paths 带 base \+ note/,
70
+ from: "paths: { base: frameworkRoot() + '/', project: 'Formal/', proofs: 'Verified/Lean/', lib:",
71
+ to: "paths: { project: 'Formal/', proofs: 'Verified/Lean/', lib:", all: true },
72
+ { name: 'F6a: provider list-failure warning removed', expect: /\[F6a\]/,
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\/v2\]/,
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\]/,
77
+ from: "noteStateWriteFailure('current.' + safeId(sessionId) + '.json', (e && e.message) || e)", to: 'void e' },
78
+ ]
79
+ let red = 0
80
+ FAMILIES.push(
81
+ { name: 'F5/v2: fence-root comparison removed', expect: /\[F5\/v2\] 围栏根漂移/,
82
+ from: 'if (actual && expected && actual !== expected) warnFenceDriftOnce(actual, expected)', to: 'void actual' },
83
+ )
84
+ FAMILIES.push(
85
+ { name: 'F-B: derivation drops the source descriptions', expect: /\[F-B\/v2\] 每个机读描述都来自真源且非空/,
86
+ from: 'if (e.description) p.description = e.description', to: 'void e /* MUTANT F-B: no source description */' },
87
+ { name: 'F-B: one exception entry deleted', expect: /\[F-B\/v2\] 例外集合 == docs\/parameter-schema\.md/,
88
+ from: "solverToolAllow: { type: 'array', items: { type: 'string' } },", to: '' },
89
+ { name: 'F-B: one registration site reverted to a literal', expect: /\[F-B\/v2\] 两处注册点都由 paramProps\(\)/,
90
+ from: "objParams(paramProps()), 'vibe_math_set_params')", to: "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }), 'vibe_math_set_params')" },
91
+ )
92
+ FAMILIES.push(
93
+ { name: 'task-9/v2: the not-detected log drops the actionable sentence', expect: /\[task-9\/v2\]/,
94
+ from: '已探测 PATH 与文档化的常见 TeX 根;可用 paperLatexCommand 指定绝对路径。', to: '' },
95
+ )
96
+ 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++ }
97
+ console.log('')
98
+ console.log('mutant families reddening the v2 probe by name: ' + red + '/' + FAMILIES.length)
99
+ console.log('skipped=[' + skipped.join(' | ') + ']')
100
+ console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
101
+ console.log('TOTAL WALL TIME (all families + setup): ' + (Date.now() - START_MS) + 'ms (' + Math.round((Date.now() - START_MS) / 1000) + 's)')
102
+ console.log('hangs=[' + hangs.join(' | ') + ']')
103
+ if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
104
+ console.log('ALL MUTANTS RED AS REQUIRED')