dsh-vibe-math 2.3.12 → 2.3.13

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 (47) hide show
  1. package/README.md +17 -13
  2. package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
  3. package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
  4. package/docs/generate_framework_diagram_v2.py +114 -0
  5. package/docs/generate_framework_diagram_v3.py +127 -0
  6. package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
  7. package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
  8. package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
  9. package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
  10. package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
  11. package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
  12. package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
  13. package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
  14. package/{RELEASE-NOTES-2.3.12.md → docs/release-notes/RELEASE-NOTES-2.3.12.md} +80 -80
  15. package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
  16. package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
  17. package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
  18. package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
  19. package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
  20. package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
  21. package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
  22. package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
  23. package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
  24. package/docs/test-timing.md +18 -17
  25. package/installer.js +105 -41
  26. package/package.json +42 -37
  27. package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
  28. package/{audit-installer-compat.test.mjs → tests/audit-installer-compat.test.mjs} +136 -136
  29. package/tests/audit-installer-policy.test.mjs +245 -0
  30. package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
  31. package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
  32. package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
  33. package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
  34. package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
  35. package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
  36. package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
  37. package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
  38. package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
  39. package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
  40. package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
  41. package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
  42. package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
  43. package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
  44. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  45. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  46. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
  47. /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
@@ -1,342 +1,342 @@
1
- // ============================================================
2
- // LEAN FORMAL-VERIFICATION SENSITIVITY PROBES (all four architectures)
3
- //
4
- // The feature contract is `docs/formal-verification.md`. This script proves the four
5
- // `formal-verify-vN.test.mjs` suites are not vacuous: each probe copies that preset's plugin,
6
- // applies ONE targeted mutation that breaks a specific guarantee, and runs the preset's suite
7
- // against the mutated copy. A probe PASSES when the suite goes RED.
8
- //
9
- // Why a separate audit from `audit-v5-sensitivity.mjs`: this feature is implemented
10
- // independently in four single-file plugins, and the guarantee that matters most — "a passing
11
- // Lean run changes WHAT the voters are asked to review" — is the kind of thing a suite can
12
- // quietly stop testing while staying green.
13
- //
14
- // Four false-green traps this script is written to avoid (see AUDIT-CHECKLIST.md §2):
15
- // 1. the probe's own spawn failing (a bad cwd) counted as "the suite went red";
16
- // 2. a suite that ignores its plugin-override env var, so the mutation is never loaded;
17
- // 3. a mutation that does not actually change behaviour (semantically inert);
18
- // 4. a mutation that introduces a syntax error, which is red for the wrong reason.
19
- // (1) and (2) are handled here; (3) is handled by choosing anchors on the deciding branch;
20
- // (4) is handled by `node --check`-ing every mutated copy and failing the probe if it does not
21
- // parse — a syntax error must never be mistaken for a detection.
22
- //
23
- // Run: node audit-formal-sensitivity.mjs
24
- // ============================================================
25
- import { readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync } from 'node:fs'
26
- import { tmpdir } from 'node:os'
27
- import { join } from 'node:path'
28
- import { spawn, spawnSync } from 'node:child_process'
29
- import { cpus } from 'node:os'
30
- import { fileURLToPath } from 'node:url'
31
-
32
- const REPO = fileURLToPath(new URL('.', import.meta.url))
33
- const dir = mkdtempSync(join(tmpdir(), 'v5-formal-sens-'))
34
-
35
- const PLUGINS = {
36
- // corpusEnv: each suite writes a human-reviewable prompt corpus; concurrent probes of the SAME
37
- // suite must not race on it, so every probe gets its own corpus dir.
38
- v2: { file: join(REPO, 'vibe-math-v2', 'vibe-math-v2.js'), suite: 'formal-verify-v2.test.mjs', env: 'V2_PLUGIN', corpusEnv: 'V2_CORPUS_DIR' },
39
- v3: { file: join(REPO, 'vibe-math-v3', 'vibe-math-v3.js'), suite: 'formal-verify-v3.test.mjs', env: 'V3_PLUGIN', corpusEnv: 'V3_CORPUS_DIR' },
40
- v4: { file: join(REPO, 'vibe-math-v4', 'vibe-math-v4.js'), suite: 'formal-verify-v4.test.mjs', env: 'V4_PLUGIN', corpusEnv: 'V4_CORPUS_DIR' },
41
- v5: { file: join(REPO, 'vibe-math-v5', 'vibe-math-v5.js'), suite: 'formal-verify-v5.test.mjs', env: 'V5_PLUGIN', corpusEnv: 'V5_CORPUS_DIR' },
42
- }
43
- const ORIGINAL = {}
44
- for (const [k, v] of Object.entries(PLUGINS)) ORIGINAL[k] = readFileSync(v.file, 'utf8')
45
-
46
- // Each probe: { name, preset, guarantee, from, to }
47
- // `from` must occur EXACTLY once, so a mutation can never quietly hit the wrong site.
48
- const probes = [
49
- // ── v5 ─────────────────────────────────────────────────────────────────────
50
- { name: 'v5-formal-off-is-not-a-no-op', preset: 'v5',
51
- guarantee: "off (the default) must be a TRUE no-op: no Lean text, no gate",
52
- from: " const formalOn = () => formalMode() !== 'off'",
53
- to: " const formalOn = () => true" },
54
- { name: 'v5-unknown-mode-upgrades', preset: 'v5',
55
- guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode (a typo must not force formalization)',
56
- from: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'",
57
- to: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'require'" },
58
- { name: 'v5-fidelity-switch-removed', preset: 'v5',
59
- guarantee: 'a passing Lean run must switch the voting prompt to a FIDELITY review (the whole point of the feature)',
60
- from: " if (rec.status === 'passed') {\n // The whole point of the feature: the review subject CHANGES.",
61
- to: " if (false) {\n // The whole point of the feature: the review subject CHANGES." },
62
- { name: 'v5-require-gate-removed', preset: 'v5',
63
- guarantee: 'require mode must withhold a true/false verdict until the object is Lean-passed or explicitly blocked',
64
- from: " if (formalMode() === 'require' && !formalGateOk(rec)) {",
65
- to: " if (false) {" },
66
- { name: 'v5-blocked-note-not-required', preset: 'v5',
67
- guarantee: 'a "we judged it infeasible" record must carry a reason (the difficulty decision is auditable, not silent)',
68
- from: " if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
69
- to: " if (false) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
70
- { name: 'v5-proof-not-archived-under-verified', preset: 'v5',
71
- guarantee: "a passing proof must be archived as that object's proof (Verified/Lean/<id>.lean)",
72
- from: " if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
73
- to: " if (false) await writeTextRel('Verified/Lean/' + target + '.lean', body)" },
74
- { name: 'v5-reuse-not-cross-project', preset: 'v5',
75
- guarantee: 'reuse must be CROSS-PROJECT: reusable definitions go to the global Formal/Lib, not inside one institute',
76
- from: " const okWrite = await writeTextAbs(instRootless(rel), body)",
77
- to: " const okWrite = await writeTextRel(rel, body)" },
78
- { name: 'v5-lean-path-guard-naive', preset: 'v5',
79
- guarantee: 'the Lean path guard must normalise .. (a string prefix check lets a traversal through)',
80
- from: " const abs = leanAbsPath(rel)\n if (abs === null) {",
81
- to: " const abs = (rel.charAt(0) === '/' || /^[a-z]:/i.test(rel)) ? rel.replace(/\\\\/g, '/') : instRoot() + '/' + rel\n if (abs.indexOf(vibeRoot() + '/') !== 0) {" },
82
- { name: 'v5-lean-run-tool-not-registered', preset: 'v5',
83
- guarantee: 'the three Lean tools must be registered (agents can only formalize if the tools exist)',
84
- from: " registerTool('vibe_v5_lean_run',",
85
- to: " if (false) registerTool('vibe_v5_lean_run'," },
86
-
87
- // ── v4 ─────────────────────────────────────────────────────────────────────
88
- { name: 'v4-formal-off-is-not-a-no-op', preset: 'v4',
89
- guarantee: "off (the default) must be a TRUE no-op",
90
- from: "const formalOn=()=>formalMode()!=='off'",
91
- to: "const formalOn=()=>true" },
92
- { name: 'v4-unknown-mode-upgrades', preset: 'v4',
93
- guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
94
- from: "if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'off'",
95
- to: "if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'require'" },
96
- { name: 'v4-fidelity-switch-removed', preset: 'v4',
97
- guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
98
- from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
99
- to: "if(false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
100
- { name: 'v4-require-gate-removed', preset: 'v4',
101
- guarantee: 'require mode must withhold a verdict until the object is Lean-passed or explicitly blocked',
102
- from: "if(formalMode()==='require' && !formalGateOk(rec)) await deferForFormal(vs,allTrue)",
103
- to: "if(false) await deferForFormal(vs,allTrue)" },
104
- { name: 'v4-blocked-note-not-required', preset: 'v4',
105
- guarantee: 'a blocker record must carry a reason',
106
- from: "if(!note) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)",
107
- to: "if(false) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)" },
108
- { name: 'v4-proof-not-archived-under-verified', preset: 'v4',
109
- guarantee: "a passing proof must be archived as that object's proof",
110
- from: "if(passed) await writeText('Verified/Lean/'+key+'.lean',got.body)",
111
- to: "if(false) await writeText('Verified/Lean/'+key+'.lean',got.body)" },
112
- { name: 'v4-lean-path-guard-naive', preset: 'v4',
113
- guarantee: 'the Lean path guard must normalise ..',
114
- from: "const norm=normalizeAbsPath(abs)",
115
- to: "const norm=abs" },
116
- { name: 'v4-lean-run-tool-not-registered', preset: 'v4',
117
- guarantee: 'the three Lean tools must be registered',
118
- from: "registerTool('vibe_v4_lean_run',",
119
- to: "if(false) registerTool('vibe_v4_lean_run'," },
120
-
121
- // ── v3 ─────────────────────────────────────────────────────────────────────
122
- { name: 'v3-formal-off-is-not-a-no-op', preset: 'v3',
123
- guarantee: "off (the default) must be a TRUE no-op",
124
- from: "function formalOn() { return formalMode() !== 'off' }",
125
- to: "function formalOn() { return true }" },
126
- { name: 'v3-unknown-mode-upgrades', preset: 'v3',
127
- guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
128
- from: "else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'off' }",
129
- to: "else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'require' }" },
130
- { name: 'v3-fidelity-switch-removed', preset: 'v3',
131
- guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
132
- from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
133
- to: "if (false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
134
- { name: 'v3-require-gate-removed', preset: 'v3',
135
- guarantee: 'require mode must withhold a verdict until the object is Lean-passed or explicitly blocked',
136
- from: "function formalBlocksConclusion(target) { return formalMode() === 'require' && !formalGateOk(formalOf(target)) }",
137
- to: "function formalBlocksConclusion(target) { return false }" },
138
- { name: 'v3-blocked-note-not-required', preset: 'v3',
139
- guarantee: 'a blocker record must carry a reason',
140
- from: "if (!note) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
141
- to: "if (false) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
142
- { name: 'v3-proof-not-archived-under-verified', preset: 'v3',
143
- guarantee: "a passing proof must be archived as that object's proof",
144
- from: "if (passed) await writeText('Verified/Lean/' + target + '.lean', body)",
145
- to: "if (false) await writeText('Verified/Lean/' + target + '.lean', body)" },
146
- { name: 'v3-lean-path-guard-naive', preset: 'v3',
147
- guarantee: 'the Lean path guard must normalise ..',
148
- from: "const norm = normalizeAbsPath(abs)",
149
- to: "const norm = abs" },
150
- { name: 'v3-lean-run-tool-handler-unbound', preset: 'v3',
151
- guarantee: 'the three Lean tools must be registered and dispatched',
152
- from: ", 'vibe_math_lean_run')",
153
- to: ", 'vibe_math_lean_run_DISABLED')" },
154
-
155
- // ── v2 ─────────────────────────────────────────────────────────────────────
156
- { name: 'v2-formal-off-is-not-a-no-op', preset: 'v2',
157
- guarantee: "off (the default) must be a TRUE no-op",
158
- from: "function formalOn() { return formalMode() !== 'off' }",
159
- to: "function formalOn() { return true }" },
160
- { name: 'v2-unknown-mode-upgrades', preset: 'v2',
161
- guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
162
- from: "if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = DEFAULT_PARAMS.formalVerify",
163
- to: "if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = 'require'" },
164
- { name: 'v2-fidelity-switch-removed', preset: 'v2',
165
- guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
166
- from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
167
- to: "if (false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
168
- // v2 enforces `require` at SEVERAL sites, and they mask each other on purpose (defence in
169
- // depth): the pre-mutation guard in settleVerdict/processStatusUpdates, and the card-writing
170
- // choke points writeVerifiedCardIfNeeded / writeVerifiedProblemCardIfNeeded. Removing ONE
171
- // site is therefore semantically INERT — the others still defer — so a per-site probe would
172
- // always be green and would report a blind spot that does not exist (AUDIT-CHECKLIST §2.5,
173
- // "变异必须真的改变行为"). The observable probe is the SHARED predicate that every site
174
- // consults: `formalRequired()` returning false disables the whole gate.
175
- { name: 'v2-require-gate-disabled', preset: 'v2',
176
- guarantee: 'require mode must withhold a true/false verdict (proposition AND problem path) until the object is Lean-passed or explicitly blocked',
177
- from: "function formalRequired() { return formalMode() === 'require' }",
178
- to: "function formalRequired() { return false }" },
179
- { name: 'v2-blocked-note-not-required', preset: 'v2',
180
- guarantee: 'a blocker record must carry a reason',
181
- from: "if (!note) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
182
- to: "if (false) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
183
- { name: 'v2-proof-not-archived-under-verified', preset: 'v2',
184
- guarantee: "a passing proof must be archived as that object's proof",
185
- from: "if (passed) await writeText('Verified/Lean/' + target + '.lean', body)",
186
- to: "if (false) await writeText('Verified/Lean/' + target + '.lean', body)" },
187
- { name: 'v2-lean-path-guard-naive', preset: 'v2',
188
- guarantee: 'the Lean path guard must normalise ..',
189
- from: "const norm = normalizeAbsPath(abs)",
190
- to: "const norm = abs" },
191
- { name: 'v2-lean-run-tool-handler-unbound', preset: 'v2',
192
- guarantee: 'the three Lean tools must be registered and dispatched',
193
- from: ", 'vibe_math_lean_run')",
194
- to: ", 'vibe_math_lean_run_DISABLED')" },
195
- ]
196
-
197
- // ── prompt-surface probes (2.3.1) ────────────────────────────────────────────────────────
198
- // "成员读到的文字就是产品" (AUDIT-CHECKLIST §0): the injected text is a contract, so breaking it
199
- // must turn the preset's suite RED. Each probe mutates ONE piece of the text agents actually read.
200
- // `expect` is the number of times that anchor legitimately occurs (v3 declares each reply
201
- // contract twice; v4/v5 document the formal contract in several prompt paths). The probe runner
202
- // reports SETUP-FAIL when a count drifts, so this table is self-checking.
203
- const DEFECT_CONTRACT_COUNT = { v2: 2, v3: 2, v4: 3, v5: 2 }
204
- for (const [tag, pfx] of [['v2', 'vibe_math_'], ['v3', 'vibe_math_'], ['v4', 'vibe_v4_'], ['v5', 'vibe_v5_']]) {
205
- probes.push(
206
- { name: tag + '-fidelity-defect-rule-removed', preset: tag, expect: 1,
207
- guarantee: 'a faithfulness defect must NEVER be expressed as a vote of 0 (= 命题为假); it is a formalisation defect, not a refutation',
208
- from: '发现任何偏差,不要投 0', to: '发现任何偏离一律投 0' },
209
- { name: tag + '-abbreviated-tool-name-injected', preset: tag, expect: 1,
210
- guarantee: 'injected text must use the REGISTERED tool name (an abbreviated lean_archive is not a tool: the agent calls nothing)',
211
- from: '· ' + pfx + 'lean_archive(归档)', to: '· lean_archive(归档)' },
212
- { name: tag + '-require-wording-removed', preset: tag, expect: 1,
213
- guarantee: "the require gate's own wording (mandatory formalisation + the formal-required reason code) must reach the voter",
214
- from: '**本模式要求**:必须产出 Lean 形式化', to: '**本模式要求**:可以不做形式化' },
215
- { name: tag + '-defect-decision-not-offered', preset: tag, expect: DEFECT_CONTRACT_COUNT[tag],
216
- guarantee: 'the reply contract must offer decision=defect (without it a faithfulness defect cannot be recorded at all)',
217
- from: '"decision":"used|blocked|defect"', to: '"decision":"used|blocked"' },
218
- )
219
- }
220
-
221
- let ok = 0, bad = 0
222
- console.log('-- Lean formal-verification sensitivity probes --')
223
- console.log('(a probe passes when breaking the guarantee turns that preset\'s suite RED)')
224
- console.log('')
225
-
226
- // ── timing feedback: per-probe durations so the next run's strategy comes from data ──────
227
- const CONCURRENCY = (() => {
228
- const arg = process.argv.find((a) => a.startsWith('--concurrency='))
229
- const env = process.env.PROBE_CONCURRENCY
230
- const v = Number((arg && arg.split('=')[1]) || env || Math.min(4, cpus().length))
231
- return Math.max(1, Number.isFinite(v) ? v : 1)
232
- })()
233
- const ONLY = (() => {
234
- const eq = process.argv.find((a) => a.startsWith('--only='))
235
- if (eq) return eq.split('=')[1]
236
- // also accept the space form (`--only v5`), which the usage line advertises
237
- const i = process.argv.indexOf('--only')
238
- return i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : ''
239
- })()
240
- const selected = probes.filter((p) => !ONLY || p.name.includes(ONLY) || p.preset === ONLY)
241
- if (process.argv.includes('--list')) {
242
- for (const p of selected) console.log(p.preset + ' ' + p.name)
243
- process.exit(0)
244
- }
245
- // A filter that matches nothing must FAIL, not report success: with zero probes the summary below
246
- // would say "0 problems / ALL PROBES RED" — a textbook false green (AUDIT-CHECKLIST §2.5).
247
- if (selected.length === 0) {
248
- console.error('no probes matched' + (ONLY ? ' --only=' + ONLY : '') + ' — refusing to report success on an empty run')
249
- process.exit(2)
250
- }
251
-
252
- function runAsync(cmd, args, opts) {
253
- return new Promise((resolve) => {
254
- const child = spawn(cmd, args, opts)
255
- let out = '', err = ''
256
- child.stdout.on('data', (d) => { out += d.toString() })
257
- child.stderr.on('data', (d) => { err += d.toString() })
258
- child.on('error', (e) => resolve({ status: null, error: e, stdout: out, stderr: err }))
259
- child.on('close', (status) => resolve({ status, stdout: out, stderr: err }))
260
- })
261
- }
262
-
263
- // One probe = one targeted mutation + one run of that preset's suite. Returns a verdict object;
264
- // never throws, so a single bad probe cannot take the pool down.
265
- async function runProbe(p) {
266
- const t0 = Date.now()
267
- const preset = PLUGINS[p.preset]
268
- const original = ORIGINAL[p.preset]
269
- const done = (kind, detail) => ({ p, kind, detail, ms: Date.now() - t0 })
270
- if (p.from.startsWith('PLACEHOLDER_')) return done('setup', 'anchor not filled in yet')
271
- const want = p.expect === undefined ? 1 : p.expect
272
- const occurrences = original.split(p.from).length - 1
273
- if (occurrences !== want) return done('setup', 'anchor matched ' + occurrences + ' times (need exactly ' + want + ')')
274
- // Mutate EVERY occurrence the anchor was asserted to have. `replace()` would only hit the first
275
- // one, which silently produced a fake blind spot: the v5 reply contract is emitted in two places
276
- // (replySpec + the voting prompt), so replacing just one left the other intact and the suite —
277
- // correctly — stayed green.
278
- const mutated = want > 1 ? original.split(p.from).join(p.to) : original.replace(p.from, p.to)
279
- // One directory per probe: the mutated copy AND its corpus output (several suites write a
280
- // corpus, and concurrent runs of the same suite must not race on that file).
281
- const pdir = join(dir, p.name)
282
- mkdirSync(pdir, { recursive: true })
283
- const file = join(pdir, 'plugin.js')
284
- writeFileSync(file, mutated, 'utf8')
285
- // A mutation that does not even parse is red for the WRONG reason.
286
- const chk = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
287
- if (chk.status !== 0) return done('setup', 'the mutated copy has a syntax error: ' + String(chk.stderr || '').split('\n').slice(0, 4).join(' '))
288
- const env = Object.assign({}, process.env)
289
- env[preset.env] = file
290
- if (preset.corpusEnv) env[preset.corpusEnv] = join(pdir, 'corpus')
291
- const r = await runAsync(process.execPath, [join(REPO, preset.suite)], { env, encoding: 'utf8', cwd: REPO })
292
- if (r.status === null) return done('setup', 'the suite could not be started (' + String(r.error && r.error.message) + ')')
293
- if (r.status !== 0) return done('ok', '')
294
- return done('blind', '')
295
- }
296
-
297
- const results = new Array(selected.length)
298
- let cursor = 0
299
- let finished = 0
300
- const wall0 = Date.now()
301
- async function worker() {
302
- for (;;) {
303
- const i = cursor++
304
- if (i >= selected.length) return
305
- const res = await runProbe(selected[i])
306
- results[i] = res
307
- finished++
308
- const tag = '[' + String(finished).padStart(2) + '/' + selected.length + ']'
309
- const secs = (res.ms / 1000).toFixed(1) + 's'
310
- if (res.kind === 'ok') console.log(' ok - ' + res.p.name + ' [' + res.p.preset + '] => suite went RED as required (' + secs + ') [' + res.p.guarantee + ']')
311
- else if (res.kind === 'blind') console.error(' BLIND SPOT ' + tag + ' - ' + res.p.name + ' [' + res.p.preset + '] (' + secs + ') => suite stayed GREEN, so it does NOT detect: ' + res.p.guarantee)
312
- else console.error(' SETUP-FAIL ' + tag + ' - ' + res.p.name + ' [' + res.p.preset + '] (' + secs + '): ' + res.detail)
313
- }
314
- }
315
- await Promise.all(Array.from({ length: Math.min(CONCURRENCY, selected.length) }, () => worker()))
316
-
317
- // ── timing summary: this is the feedback that decides the next run's strategy ────────────
318
- {
319
- const wall = (Date.now() - wall0) / 1000
320
- const sum = results.reduce((a, r) => a + (r ? r.ms : 0), 0) / 1000
321
- const perPreset = {}
322
- for (const r of results) {
323
- if (!r) continue
324
- perPreset[r.p.preset] = perPreset[r.p.preset] || { n: 0, s: 0 }
325
- perPreset[r.p.preset].n++
326
- perPreset[r.p.preset].s += r.ms / 1000
327
- }
328
- const slow = results.filter(Boolean).slice().sort((a, b) => b.ms - a.ms).slice(0, 5)
329
- console.log('')
330
- console.log('-- timing --')
331
- console.log(' concurrency ' + CONCURRENCY + ' · wall ' + wall.toFixed(1) + 's · sum of probe times ' + sum.toFixed(1) + 's'
332
- + ' · speed-up x' + (sum / Math.max(wall, 0.001)).toFixed(2))
333
- console.log(' per preset: ' + Object.keys(perPreset).sort().map((k) => k + ' ' + perPreset[k].n + ' probes/' + perPreset[k].s.toFixed(0) + 's').join(' · '))
334
- console.log(' slowest: ' + slow.map((r) => r.p.name + ' ' + (r.ms / 1000).toFixed(1) + 's').join(' · '))
335
- ok = results.filter((r) => r && r.kind === 'ok').length
336
- bad = results.filter((r) => r && r.kind !== 'ok').length
337
- }
338
-
339
- console.log('formal sensitivity: ' + ok + ' probes detected the break, ' + bad + ' problems')
340
- if (bad) process.exit(1)
341
- console.log('ALL FORMAL PROBES RED AS REQUIRED')
342
- process.exit(0)
1
+ // ============================================================
2
+ // LEAN FORMAL-VERIFICATION SENSITIVITY PROBES (all four architectures)
3
+ //
4
+ // The feature contract is `docs/formal-verification.md`. This script proves the four
5
+ // `formal-verify-vN.test.mjs` suites are not vacuous: each probe copies that preset's plugin,
6
+ // applies ONE targeted mutation that breaks a specific guarantee, and runs the preset's suite
7
+ // against the mutated copy. A probe PASSES when the suite goes RED.
8
+ //
9
+ // Why a separate audit from `audit-v5-sensitivity.mjs`: this feature is implemented
10
+ // independently in four single-file plugins, and the guarantee that matters most — "a passing
11
+ // Lean run changes WHAT the voters are asked to review" — is the kind of thing a suite can
12
+ // quietly stop testing while staying green.
13
+ //
14
+ // Four false-green traps this script is written to avoid (see AUDIT-CHECKLIST.md §2):
15
+ // 1. the probe's own spawn failing (a bad cwd) counted as "the suite went red";
16
+ // 2. a suite that ignores its plugin-override env var, so the mutation is never loaded;
17
+ // 3. a mutation that does not actually change behaviour (semantically inert);
18
+ // 4. a mutation that introduces a syntax error, which is red for the wrong reason.
19
+ // (1) and (2) are handled here; (3) is handled by choosing anchors on the deciding branch;
20
+ // (4) is handled by `node --check`-ing every mutated copy and failing the probe if it does not
21
+ // parse — a syntax error must never be mistaken for a detection.
22
+ //
23
+ // Run: node tests/audit-formal-sensitivity.mjs
24
+ // ============================================================
25
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync } from 'node:fs'
26
+ import { tmpdir } from 'node:os'
27
+ import { join } from 'node:path'
28
+ import { spawn, spawnSync } from 'node:child_process'
29
+ import { cpus } from 'node:os'
30
+ import { fileURLToPath } from 'node:url'
31
+
32
+ const REPO = fileURLToPath(new URL('..', import.meta.url))
33
+ const dir = mkdtempSync(join(tmpdir(), 'v5-formal-sens-'))
34
+
35
+ const PLUGINS = {
36
+ // corpusEnv: each suite writes a human-reviewable prompt corpus; concurrent probes of the SAME
37
+ // suite must not race on it, so every probe gets its own corpus dir.
38
+ v2: { file: join(REPO, 'vibe-math-v2', 'vibe-math-v2.js'), suite: 'tests/formal-verify-v2.test.mjs', env: 'V2_PLUGIN', corpusEnv: 'V2_CORPUS_DIR' },
39
+ v3: { file: join(REPO, 'vibe-math-v3', 'vibe-math-v3.js'), suite: 'tests/formal-verify-v3.test.mjs', env: 'V3_PLUGIN', corpusEnv: 'V3_CORPUS_DIR' },
40
+ v4: { file: join(REPO, 'vibe-math-v4', 'vibe-math-v4.js'), suite: 'tests/formal-verify-v4.test.mjs', env: 'V4_PLUGIN', corpusEnv: 'V4_CORPUS_DIR' },
41
+ v5: { file: join(REPO, 'vibe-math-v5', 'vibe-math-v5.js'), suite: 'tests/formal-verify-v5.test.mjs', env: 'V5_PLUGIN', corpusEnv: 'V5_CORPUS_DIR' },
42
+ }
43
+ const ORIGINAL = {}
44
+ for (const [k, v] of Object.entries(PLUGINS)) ORIGINAL[k] = readFileSync(v.file, 'utf8')
45
+
46
+ // Each probe: { name, preset, guarantee, from, to }
47
+ // `from` must occur EXACTLY once, so a mutation can never quietly hit the wrong site.
48
+ const probes = [
49
+ // ── v5 ─────────────────────────────────────────────────────────────────────
50
+ { name: 'v5-formal-off-is-not-a-no-op', preset: 'v5',
51
+ guarantee: "off (the default) must be a TRUE no-op: no Lean text, no gate",
52
+ from: " const formalOn = () => formalMode() !== 'off'",
53
+ to: " const formalOn = () => true" },
54
+ { name: 'v5-unknown-mode-upgrades', preset: 'v5',
55
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode (a typo must not force formalization)',
56
+ from: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'",
57
+ to: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'require'" },
58
+ { name: 'v5-fidelity-switch-removed', preset: 'v5',
59
+ guarantee: 'a passing Lean run must switch the voting prompt to a FIDELITY review (the whole point of the feature)',
60
+ from: " if (rec.status === 'passed') {\n // The whole point of the feature: the review subject CHANGES.",
61
+ to: " if (false) {\n // The whole point of the feature: the review subject CHANGES." },
62
+ { name: 'v5-require-gate-removed', preset: 'v5',
63
+ guarantee: 'require mode must withhold a true/false verdict until the object is Lean-passed or explicitly blocked',
64
+ from: " if (formalMode() === 'require' && !formalGateOk(rec)) {",
65
+ to: " if (false) {" },
66
+ { name: 'v5-blocked-note-not-required', preset: 'v5',
67
+ guarantee: 'a "we judged it infeasible" record must carry a reason (the difficulty decision is auditable, not silent)',
68
+ from: " if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
69
+ to: " if (false) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
70
+ { name: 'v5-proof-not-archived-under-verified', preset: 'v5',
71
+ guarantee: "a passing proof must be archived as that object's proof (Verified/Lean/<id>.lean)",
72
+ from: " if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
73
+ to: " if (false) await writeTextRel('Verified/Lean/' + target + '.lean', body)" },
74
+ { name: 'v5-reuse-not-cross-project', preset: 'v5',
75
+ guarantee: 'reuse must be CROSS-PROJECT: reusable definitions go to the global Formal/Lib, not inside one institute',
76
+ from: " const okWrite = await writeTextAbs(instRootless(rel), body)",
77
+ to: " const okWrite = await writeTextRel(rel, body)" },
78
+ { name: 'v5-lean-path-guard-naive', preset: 'v5',
79
+ guarantee: 'the Lean path guard must normalise .. (a string prefix check lets a traversal through)',
80
+ from: " const abs = leanAbsPath(rel)\n if (abs === null) {",
81
+ to: " const abs = (rel.charAt(0) === '/' || /^[a-z]:/i.test(rel)) ? rel.replace(/\\\\/g, '/') : instRoot() + '/' + rel\n if (abs.indexOf(vibeRoot() + '/') !== 0) {" },
82
+ { name: 'v5-lean-run-tool-not-registered', preset: 'v5',
83
+ guarantee: 'the three Lean tools must be registered (agents can only formalize if the tools exist)',
84
+ from: " registerTool('vibe_v5_lean_run',",
85
+ to: " if (false) registerTool('vibe_v5_lean_run'," },
86
+
87
+ // ── v4 ─────────────────────────────────────────────────────────────────────
88
+ { name: 'v4-formal-off-is-not-a-no-op', preset: 'v4',
89
+ guarantee: "off (the default) must be a TRUE no-op",
90
+ from: "const formalOn=()=>formalMode()!=='off'",
91
+ to: "const formalOn=()=>true" },
92
+ { name: 'v4-unknown-mode-upgrades', preset: 'v4',
93
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
94
+ from: "if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'off'",
95
+ to: "if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'require'" },
96
+ { name: 'v4-fidelity-switch-removed', preset: 'v4',
97
+ guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
98
+ from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
99
+ to: "if(false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
100
+ { name: 'v4-require-gate-removed', preset: 'v4',
101
+ guarantee: 'require mode must withhold a verdict until the object is Lean-passed or explicitly blocked',
102
+ from: "if(formalMode()==='require' && !formalGateOk(rec)) await deferForFormal(vs,allTrue)",
103
+ to: "if(false) await deferForFormal(vs,allTrue)" },
104
+ { name: 'v4-blocked-note-not-required', preset: 'v4',
105
+ guarantee: 'a blocker record must carry a reason',
106
+ from: "if(!note) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)",
107
+ to: "if(false) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)" },
108
+ { name: 'v4-proof-not-archived-under-verified', preset: 'v4',
109
+ guarantee: "a passing proof must be archived as that object's proof",
110
+ from: "if(passed) await writeText('Verified/Lean/'+key+'.lean',got.body)",
111
+ to: "if(false) await writeText('Verified/Lean/'+key+'.lean',got.body)" },
112
+ { name: 'v4-lean-path-guard-naive', preset: 'v4',
113
+ guarantee: 'the Lean path guard must normalise ..',
114
+ from: "const norm=normalizeAbsPath(abs)",
115
+ to: "const norm=abs" },
116
+ { name: 'v4-lean-run-tool-not-registered', preset: 'v4',
117
+ guarantee: 'the three Lean tools must be registered',
118
+ from: "registerTool('vibe_v4_lean_run',",
119
+ to: "if(false) registerTool('vibe_v4_lean_run'," },
120
+
121
+ // ── v3 ─────────────────────────────────────────────────────────────────────
122
+ { name: 'v3-formal-off-is-not-a-no-op', preset: 'v3',
123
+ guarantee: "off (the default) must be a TRUE no-op",
124
+ from: "function formalOn() { return formalMode() !== 'off' }",
125
+ to: "function formalOn() { return true }" },
126
+ { name: 'v3-unknown-mode-upgrades', preset: 'v3',
127
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
128
+ from: "else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'off' }",
129
+ to: "else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'require' }" },
130
+ { name: 'v3-fidelity-switch-removed', preset: 'v3',
131
+ guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
132
+ from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
133
+ to: "if (false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
134
+ { name: 'v3-require-gate-removed', preset: 'v3',
135
+ guarantee: 'require mode must withhold a verdict until the object is Lean-passed or explicitly blocked',
136
+ from: "function formalBlocksConclusion(target) { return formalMode() === 'require' && !formalGateOk(formalOf(target)) }",
137
+ to: "function formalBlocksConclusion(target) { return false }" },
138
+ { name: 'v3-blocked-note-not-required', preset: 'v3',
139
+ guarantee: 'a blocker record must carry a reason',
140
+ from: "if (!note) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
141
+ to: "if (false) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
142
+ { name: 'v3-proof-not-archived-under-verified', preset: 'v3',
143
+ guarantee: "a passing proof must be archived as that object's proof",
144
+ from: "if (passed) await writeText('Verified/Lean/' + target + '.lean', body)",
145
+ to: "if (false) await writeText('Verified/Lean/' + target + '.lean', body)" },
146
+ { name: 'v3-lean-path-guard-naive', preset: 'v3',
147
+ guarantee: 'the Lean path guard must normalise ..',
148
+ from: "const norm = normalizeAbsPath(abs)",
149
+ to: "const norm = abs" },
150
+ { name: 'v3-lean-run-tool-handler-unbound', preset: 'v3',
151
+ guarantee: 'the three Lean tools must be registered and dispatched',
152
+ from: ", 'vibe_math_lean_run')",
153
+ to: ", 'vibe_math_lean_run_DISABLED')" },
154
+
155
+ // ── v2 ─────────────────────────────────────────────────────────────────────
156
+ { name: 'v2-formal-off-is-not-a-no-op', preset: 'v2',
157
+ guarantee: "off (the default) must be a TRUE no-op",
158
+ from: "function formalOn() { return formalMode() !== 'off' }",
159
+ to: "function formalOn() { return true }" },
160
+ { name: 'v2-unknown-mode-upgrades', preset: 'v2',
161
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
162
+ from: "if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = DEFAULT_PARAMS.formalVerify",
163
+ to: "if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = 'require'" },
164
+ { name: 'v2-fidelity-switch-removed', preset: 'v2',
165
+ guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
166
+ from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
167
+ to: "if (false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
168
+ // v2 enforces `require` at SEVERAL sites, and they mask each other on purpose (defence in
169
+ // depth): the pre-mutation guard in settleVerdict/processStatusUpdates, and the card-writing
170
+ // choke points writeVerifiedCardIfNeeded / writeVerifiedProblemCardIfNeeded. Removing ONE
171
+ // site is therefore semantically INERT — the others still defer — so a per-site probe would
172
+ // always be green and would report a blind spot that does not exist (AUDIT-CHECKLIST §2.5,
173
+ // "变异必须真的改变行为"). The observable probe is the SHARED predicate that every site
174
+ // consults: `formalRequired()` returning false disables the whole gate.
175
+ { name: 'v2-require-gate-disabled', preset: 'v2',
176
+ guarantee: 'require mode must withhold a true/false verdict (proposition AND problem path) until the object is Lean-passed or explicitly blocked',
177
+ from: "function formalRequired() { return formalMode() === 'require' }",
178
+ to: "function formalRequired() { return false }" },
179
+ { name: 'v2-blocked-note-not-required', preset: 'v2',
180
+ guarantee: 'a blocker record must carry a reason',
181
+ from: "if (!note) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
182
+ to: "if (false) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
183
+ { name: 'v2-proof-not-archived-under-verified', preset: 'v2',
184
+ guarantee: "a passing proof must be archived as that object's proof",
185
+ from: "if (passed) await writeText('Verified/Lean/' + target + '.lean', body)",
186
+ to: "if (false) await writeText('Verified/Lean/' + target + '.lean', body)" },
187
+ { name: 'v2-lean-path-guard-naive', preset: 'v2',
188
+ guarantee: 'the Lean path guard must normalise ..',
189
+ from: "const norm = normalizeAbsPath(abs)",
190
+ to: "const norm = abs" },
191
+ { name: 'v2-lean-run-tool-handler-unbound', preset: 'v2',
192
+ guarantee: 'the three Lean tools must be registered and dispatched',
193
+ from: ", 'vibe_math_lean_run')",
194
+ to: ", 'vibe_math_lean_run_DISABLED')" },
195
+ ]
196
+
197
+ // ── prompt-surface probes (2.3.1) ────────────────────────────────────────────────────────
198
+ // "成员读到的文字就是产品" (AUDIT-CHECKLIST §0): the injected text is a contract, so breaking it
199
+ // must turn the preset's suite RED. Each probe mutates ONE piece of the text agents actually read.
200
+ // `expect` is the number of times that anchor legitimately occurs (v3 declares each reply
201
+ // contract twice; v4/v5 document the formal contract in several prompt paths). The probe runner
202
+ // reports SETUP-FAIL when a count drifts, so this table is self-checking.
203
+ const DEFECT_CONTRACT_COUNT = { v2: 2, v3: 2, v4: 3, v5: 2 }
204
+ for (const [tag, pfx] of [['v2', 'vibe_math_'], ['v3', 'vibe_math_'], ['v4', 'vibe_v4_'], ['v5', 'vibe_v5_']]) {
205
+ probes.push(
206
+ { name: tag + '-fidelity-defect-rule-removed', preset: tag, expect: 1,
207
+ guarantee: 'a faithfulness defect must NEVER be expressed as a vote of 0 (= 命题为假); it is a formalisation defect, not a refutation',
208
+ from: '发现任何偏差,不要投 0', to: '发现任何偏离一律投 0' },
209
+ { name: tag + '-abbreviated-tool-name-injected', preset: tag, expect: 1,
210
+ guarantee: 'injected text must use the REGISTERED tool name (an abbreviated lean_archive is not a tool: the agent calls nothing)',
211
+ from: '· ' + pfx + 'lean_archive(归档)', to: '· lean_archive(归档)' },
212
+ { name: tag + '-require-wording-removed', preset: tag, expect: 1,
213
+ guarantee: "the require gate's own wording (mandatory formalisation + the formal-required reason code) must reach the voter",
214
+ from: '**本模式要求**:必须产出 Lean 形式化', to: '**本模式要求**:可以不做形式化' },
215
+ { name: tag + '-defect-decision-not-offered', preset: tag, expect: DEFECT_CONTRACT_COUNT[tag],
216
+ guarantee: 'the reply contract must offer decision=defect (without it a faithfulness defect cannot be recorded at all)',
217
+ from: '"decision":"used|blocked|defect"', to: '"decision":"used|blocked"' },
218
+ )
219
+ }
220
+
221
+ let ok = 0, bad = 0
222
+ console.log('-- Lean formal-verification sensitivity probes --')
223
+ console.log('(a probe passes when breaking the guarantee turns that preset\'s suite RED)')
224
+ console.log('')
225
+
226
+ // ── timing feedback: per-probe durations so the next run's strategy comes from data ──────
227
+ const CONCURRENCY = (() => {
228
+ const arg = process.argv.find((a) => a.startsWith('--concurrency='))
229
+ const env = process.env.PROBE_CONCURRENCY
230
+ const v = Number((arg && arg.split('=')[1]) || env || Math.min(4, cpus().length))
231
+ return Math.max(1, Number.isFinite(v) ? v : 1)
232
+ })()
233
+ const ONLY = (() => {
234
+ const eq = process.argv.find((a) => a.startsWith('--only='))
235
+ if (eq) return eq.split('=')[1]
236
+ // also accept the space form (`--only v5`), which the usage line advertises
237
+ const i = process.argv.indexOf('--only')
238
+ return i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : ''
239
+ })()
240
+ const selected = probes.filter((p) => !ONLY || p.name.includes(ONLY) || p.preset === ONLY)
241
+ if (process.argv.includes('--list')) {
242
+ for (const p of selected) console.log(p.preset + ' ' + p.name)
243
+ process.exit(0)
244
+ }
245
+ // A filter that matches nothing must FAIL, not report success: with zero probes the summary below
246
+ // would say "0 problems / ALL PROBES RED" — a textbook false green (AUDIT-CHECKLIST §2.5).
247
+ if (selected.length === 0) {
248
+ console.error('no probes matched' + (ONLY ? ' --only=' + ONLY : '') + ' — refusing to report success on an empty run')
249
+ process.exit(2)
250
+ }
251
+
252
+ function runAsync(cmd, args, opts) {
253
+ return new Promise((resolve) => {
254
+ const child = spawn(cmd, args, opts)
255
+ let out = '', err = ''
256
+ child.stdout.on('data', (d) => { out += d.toString() })
257
+ child.stderr.on('data', (d) => { err += d.toString() })
258
+ child.on('error', (e) => resolve({ status: null, error: e, stdout: out, stderr: err }))
259
+ child.on('close', (status) => resolve({ status, stdout: out, stderr: err }))
260
+ })
261
+ }
262
+
263
+ // One probe = one targeted mutation + one run of that preset's suite. Returns a verdict object;
264
+ // never throws, so a single bad probe cannot take the pool down.
265
+ async function runProbe(p) {
266
+ const t0 = Date.now()
267
+ const preset = PLUGINS[p.preset]
268
+ const original = ORIGINAL[p.preset]
269
+ const done = (kind, detail) => ({ p, kind, detail, ms: Date.now() - t0 })
270
+ if (p.from.startsWith('PLACEHOLDER_')) return done('setup', 'anchor not filled in yet')
271
+ const want = p.expect === undefined ? 1 : p.expect
272
+ const occurrences = original.split(p.from).length - 1
273
+ if (occurrences !== want) return done('setup', 'anchor matched ' + occurrences + ' times (need exactly ' + want + ')')
274
+ // Mutate EVERY occurrence the anchor was asserted to have. `replace()` would only hit the first
275
+ // one, which silently produced a fake blind spot: the v5 reply contract is emitted in two places
276
+ // (replySpec + the voting prompt), so replacing just one left the other intact and the suite —
277
+ // correctly — stayed green.
278
+ const mutated = want > 1 ? original.split(p.from).join(p.to) : original.replace(p.from, p.to)
279
+ // One directory per probe: the mutated copy AND its corpus output (several suites write a
280
+ // corpus, and concurrent runs of the same suite must not race on that file).
281
+ const pdir = join(dir, p.name)
282
+ mkdirSync(pdir, { recursive: true })
283
+ const file = join(pdir, 'plugin.js')
284
+ writeFileSync(file, mutated, 'utf8')
285
+ // A mutation that does not even parse is red for the WRONG reason.
286
+ const chk = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
287
+ if (chk.status !== 0) return done('setup', 'the mutated copy has a syntax error: ' + String(chk.stderr || '').split('\n').slice(0, 4).join(' '))
288
+ const env = Object.assign({}, process.env)
289
+ env[preset.env] = file
290
+ if (preset.corpusEnv) env[preset.corpusEnv] = join(pdir, 'corpus')
291
+ const r = await runAsync(process.execPath, [join(REPO, preset.suite)], { env, encoding: 'utf8', cwd: REPO })
292
+ if (r.status === null) return done('setup', 'the suite could not be started (' + String(r.error && r.error.message) + ')')
293
+ if (r.status !== 0) return done('ok', '')
294
+ return done('blind', '')
295
+ }
296
+
297
+ const results = new Array(selected.length)
298
+ let cursor = 0
299
+ let finished = 0
300
+ const wall0 = Date.now()
301
+ async function worker() {
302
+ for (;;) {
303
+ const i = cursor++
304
+ if (i >= selected.length) return
305
+ const res = await runProbe(selected[i])
306
+ results[i] = res
307
+ finished++
308
+ const tag = '[' + String(finished).padStart(2) + '/' + selected.length + ']'
309
+ const secs = (res.ms / 1000).toFixed(1) + 's'
310
+ if (res.kind === 'ok') console.log(' ok - ' + res.p.name + ' [' + res.p.preset + '] => suite went RED as required (' + secs + ') [' + res.p.guarantee + ']')
311
+ else if (res.kind === 'blind') console.error(' BLIND SPOT ' + tag + ' - ' + res.p.name + ' [' + res.p.preset + '] (' + secs + ') => suite stayed GREEN, so it does NOT detect: ' + res.p.guarantee)
312
+ else console.error(' SETUP-FAIL ' + tag + ' - ' + res.p.name + ' [' + res.p.preset + '] (' + secs + '): ' + res.detail)
313
+ }
314
+ }
315
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, selected.length) }, () => worker()))
316
+
317
+ // ── timing summary: this is the feedback that decides the next run's strategy ────────────
318
+ {
319
+ const wall = (Date.now() - wall0) / 1000
320
+ const sum = results.reduce((a, r) => a + (r ? r.ms : 0), 0) / 1000
321
+ const perPreset = {}
322
+ for (const r of results) {
323
+ if (!r) continue
324
+ perPreset[r.p.preset] = perPreset[r.p.preset] || { n: 0, s: 0 }
325
+ perPreset[r.p.preset].n++
326
+ perPreset[r.p.preset].s += r.ms / 1000
327
+ }
328
+ const slow = results.filter(Boolean).slice().sort((a, b) => b.ms - a.ms).slice(0, 5)
329
+ console.log('')
330
+ console.log('-- timing --')
331
+ console.log(' concurrency ' + CONCURRENCY + ' · wall ' + wall.toFixed(1) + 's · sum of probe times ' + sum.toFixed(1) + 's'
332
+ + ' · speed-up x' + (sum / Math.max(wall, 0.001)).toFixed(2))
333
+ console.log(' per preset: ' + Object.keys(perPreset).sort().map((k) => k + ' ' + perPreset[k].n + ' probes/' + perPreset[k].s.toFixed(0) + 's').join(' · '))
334
+ console.log(' slowest: ' + slow.map((r) => r.p.name + ' ' + (r.ms / 1000).toFixed(1) + 's').join(' · '))
335
+ ok = results.filter((r) => r && r.kind === 'ok').length
336
+ bad = results.filter((r) => r && r.kind !== 'ok').length
337
+ }
338
+
339
+ console.log('formal sensitivity: ' + ok + ' probes detected the break, ' + bad + ' problems')
340
+ if (bad) process.exit(1)
341
+ console.log('ALL FORMAL PROBES RED AS REQUIRED')
342
+ process.exit(0)