dsh-vibe-math 2.2.2 → 2.3.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 (36) hide show
  1. package/AUDIT-CHECKLIST.md +28 -0
  2. package/README.md +98 -1
  3. package/RELEASE-NOTES-2.3.0.md +207 -0
  4. package/audit-formal-sensitivity.mjs +247 -0
  5. package/audit-persona-sensitivity.mjs +249 -0
  6. package/audit-persona-surface.test.mjs +349 -0
  7. package/audit-v5-integrity.mjs +40 -1
  8. package/audit-v5-sensitivity.mjs +77 -6
  9. package/docs/formal-verification.md +321 -0
  10. package/docs/generate_framework_diagram_v5.mjs +22 -16
  11. package/formal-verify-v2.test.mjs +672 -0
  12. package/formal-verify-v3.test.mjs +824 -0
  13. package/formal-verify-v4.test.mjs +603 -0
  14. package/formal-verify-v5.test.mjs +526 -0
  15. package/package.json +15 -2
  16. package/prompt-corpus-persona/persona-corpus.json +32 -0
  17. package/prompt-corpus-persona/persona-corpus.md +674 -0
  18. package/prompt-corpus-v3/formal-verify-v3.json +280 -0
  19. package/prompt-corpus-v3/formal-verify-v3.md +2826 -0
  20. package/prompt-corpus-v5/prompt-corpus-v5.json +75 -9
  21. package/prompt-corpus-v5/prompt-corpus-v5.md +384 -65
  22. package/prompt-v5-integrity.test.mjs +111 -10
  23. package/vibe-math-v2/agent.cordis.yml +40 -2
  24. package/vibe-math-v2/vibe-math-v2.js +627 -19
  25. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +145 -1
  26. package/vibe-math-v3/agent.cordis.yml +46 -2
  27. package/vibe-math-v3/vibe-math-v3.js +749 -21
  28. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +87 -2
  29. package/vibe-math-v4/agent.cordis.yml +46 -4
  30. package/vibe-math-v4/vibe-math-v4.js +652 -15
  31. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +226 -0
  32. package/vibe-math-v5/agent.cordis.yml +41 -5
  33. package/vibe-math-v5/vibe-math-v5.js +562 -9
  34. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +108 -4
  35. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +57 -0
  36. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +51 -46
@@ -0,0 +1,247 @@
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 } from 'node:fs'
26
+ import { tmpdir } from 'node:os'
27
+ import { join } from 'node:path'
28
+ import { spawnSync } from 'node:child_process'
29
+ import { fileURLToPath } from 'node:url'
30
+
31
+ const REPO = fileURLToPath(new URL('.', import.meta.url))
32
+ const dir = mkdtempSync(join(tmpdir(), 'v5-formal-sens-'))
33
+
34
+ const PLUGINS = {
35
+ v2: { file: join(REPO, 'vibe-math-v2', 'vibe-math-v2.js'), suite: 'formal-verify-v2.test.mjs', env: 'V2_PLUGIN' },
36
+ v3: { file: join(REPO, 'vibe-math-v3', 'vibe-math-v3.js'), suite: 'formal-verify-v3.test.mjs', env: 'V3_PLUGIN' },
37
+ v4: { file: join(REPO, 'vibe-math-v4', 'vibe-math-v4.js'), suite: 'formal-verify-v4.test.mjs', env: 'V4_PLUGIN' },
38
+ v5: { file: join(REPO, 'vibe-math-v5', 'vibe-math-v5.js'), suite: 'formal-verify-v5.test.mjs', env: 'V5_PLUGIN' },
39
+ }
40
+ const ORIGINAL = {}
41
+ for (const [k, v] of Object.entries(PLUGINS)) ORIGINAL[k] = readFileSync(v.file, 'utf8')
42
+
43
+ // Each probe: { name, preset, guarantee, from, to }
44
+ // `from` must occur EXACTLY once, so a mutation can never quietly hit the wrong site.
45
+ const probes = [
46
+ // ── v5 ─────────────────────────────────────────────────────────────────────
47
+ { name: 'v5-formal-off-is-not-a-no-op', preset: 'v5',
48
+ guarantee: "off (the default) must be a TRUE no-op: no Lean text, no gate",
49
+ from: " const formalOn = () => formalMode() !== 'off'",
50
+ to: " const formalOn = () => true" },
51
+ { name: 'v5-unknown-mode-upgrades', preset: 'v5',
52
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode (a typo must not force formalization)',
53
+ from: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'",
54
+ to: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'require'" },
55
+ { name: 'v5-fidelity-switch-removed', preset: 'v5',
56
+ guarantee: 'a passing Lean run must switch the voting prompt to a FIDELITY review (the whole point of the feature)',
57
+ from: " if (rec.status === 'passed') {\n // The whole point of the feature: the review subject CHANGES.",
58
+ to: " if (false) {\n // The whole point of the feature: the review subject CHANGES." },
59
+ { name: 'v5-require-gate-removed', preset: 'v5',
60
+ guarantee: 'require mode must withhold a true/false verdict until the object is Lean-passed or explicitly blocked',
61
+ from: " if (formalMode() === 'require' && !formalGateOk(rec)) {",
62
+ to: " if (false) {" },
63
+ { name: 'v5-blocked-note-not-required', preset: 'v5',
64
+ guarantee: 'a "we judged it infeasible" record must carry a reason (the difficulty decision is auditable, not silent)',
65
+ from: " if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
66
+ to: " if (false) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
67
+ { name: 'v5-proof-not-archived-under-verified', preset: 'v5',
68
+ guarantee: "a passing proof must be archived as that object's proof (Verified/Lean/<id>.lean)",
69
+ from: " if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
70
+ to: " if (false) await writeTextRel('Verified/Lean/' + target + '.lean', body)" },
71
+ { name: 'v5-reuse-not-cross-project', preset: 'v5',
72
+ guarantee: 'reuse must be CROSS-PROJECT: reusable definitions go to the global Formal/Lib, not inside one institute',
73
+ from: " const okWrite = await writeTextAbs(instRootless(rel), body)",
74
+ to: " const okWrite = await writeTextRel(rel, body)" },
75
+ { name: 'v5-lean-path-guard-naive', preset: 'v5',
76
+ guarantee: 'the Lean path guard must normalise .. (a string prefix check lets a traversal through)',
77
+ from: " const abs = leanAbsPath(rel)\n if (abs === null) {",
78
+ to: " const abs = (rel.charAt(0) === '/' || /^[a-z]:/i.test(rel)) ? rel.replace(/\\\\/g, '/') : instRoot() + '/' + rel\n if (abs.indexOf(vibeRoot() + '/') !== 0) {" },
79
+ { name: 'v5-lean-run-tool-not-registered', preset: 'v5',
80
+ guarantee: 'the three Lean tools must be registered (agents can only formalize if the tools exist)',
81
+ from: " registerTool('vibe_v5_lean_run',",
82
+ to: " if (false) registerTool('vibe_v5_lean_run'," },
83
+
84
+ // ── v4 ─────────────────────────────────────────────────────────────────────
85
+ { name: 'v4-formal-off-is-not-a-no-op', preset: 'v4',
86
+ guarantee: "off (the default) must be a TRUE no-op",
87
+ from: "const formalOn=()=>formalMode()!=='off'",
88
+ to: "const formalOn=()=>true" },
89
+ { name: 'v4-unknown-mode-upgrades', preset: 'v4',
90
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
91
+ from: "if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'off'",
92
+ to: "if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'require'" },
93
+ { name: 'v4-fidelity-switch-removed', preset: 'v4',
94
+ guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
95
+ from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
96
+ to: "if(false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
97
+ { name: 'v4-require-gate-removed', preset: 'v4',
98
+ guarantee: 'require mode must withhold a verdict until the object is Lean-passed or explicitly blocked',
99
+ from: "if(formalMode()==='require' && !formalGateOk(rec)) await deferForFormal(vs,allTrue)",
100
+ to: "if(false) await deferForFormal(vs,allTrue)" },
101
+ { name: 'v4-blocked-note-not-required', preset: 'v4',
102
+ guarantee: 'a blocker record must carry a reason',
103
+ from: "if(!note) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)",
104
+ to: "if(false) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)" },
105
+ { name: 'v4-proof-not-archived-under-verified', preset: 'v4',
106
+ guarantee: "a passing proof must be archived as that object's proof",
107
+ from: "if(passed) await writeText('Verified/Lean/'+key+'.lean',got.body)",
108
+ to: "if(false) await writeText('Verified/Lean/'+key+'.lean',got.body)" },
109
+ { name: 'v4-lean-path-guard-naive', preset: 'v4',
110
+ guarantee: 'the Lean path guard must normalise ..',
111
+ from: "const norm=normalizeAbsPath(abs)",
112
+ to: "const norm=abs" },
113
+ { name: 'v4-lean-run-tool-not-registered', preset: 'v4',
114
+ guarantee: 'the three Lean tools must be registered',
115
+ from: "registerTool('vibe_v4_lean_run',",
116
+ to: "if(false) registerTool('vibe_v4_lean_run'," },
117
+
118
+ // ── v3 ─────────────────────────────────────────────────────────────────────
119
+ { name: 'v3-formal-off-is-not-a-no-op', preset: 'v3',
120
+ guarantee: "off (the default) must be a TRUE no-op",
121
+ from: "function formalOn() { return formalMode() !== 'off' }",
122
+ to: "function formalOn() { return true }" },
123
+ { name: 'v3-unknown-mode-upgrades', preset: 'v3',
124
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
125
+ from: "else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'off' }",
126
+ to: "else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'require' }" },
127
+ { name: 'v3-fidelity-switch-removed', preset: 'v3',
128
+ guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
129
+ from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
130
+ to: "if (false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
131
+ { name: 'v3-require-gate-removed', preset: 'v3',
132
+ guarantee: 'require mode must withhold a verdict until the object is Lean-passed or explicitly blocked',
133
+ from: "function formalBlocksConclusion(target) { return formalMode() === 'require' && !formalGateOk(formalOf(target)) }",
134
+ to: "function formalBlocksConclusion(target) { return false }" },
135
+ { name: 'v3-blocked-note-not-required', preset: 'v3',
136
+ guarantee: 'a blocker record must carry a reason',
137
+ from: "if (!note) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
138
+ to: "if (false) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
139
+ { name: 'v3-proof-not-archived-under-verified', preset: 'v3',
140
+ guarantee: "a passing proof must be archived as that object's proof",
141
+ from: "if (passed) await writeText('Verified/Lean/' + target + '.lean', body)",
142
+ to: "if (false) await writeText('Verified/Lean/' + target + '.lean', body)" },
143
+ { name: 'v3-lean-path-guard-naive', preset: 'v3',
144
+ guarantee: 'the Lean path guard must normalise ..',
145
+ from: "const norm = normalizeAbsPath(abs)",
146
+ to: "const norm = abs" },
147
+ { name: 'v3-lean-run-tool-handler-unbound', preset: 'v3',
148
+ guarantee: 'the three Lean tools must be registered and dispatched',
149
+ from: ", 'vibe_math_lean_run')",
150
+ to: ", 'vibe_math_lean_run_DISABLED')" },
151
+
152
+ // ── v2 ─────────────────────────────────────────────────────────────────────
153
+ { name: 'v2-formal-off-is-not-a-no-op', preset: 'v2',
154
+ guarantee: "off (the default) must be a TRUE no-op",
155
+ from: "function formalOn() { return formalMode() !== 'off' }",
156
+ to: "function formalOn() { return true }" },
157
+ { name: 'v2-unknown-mode-upgrades', preset: 'v2',
158
+ guarantee: 'an unknown mode must degrade to off, never to a STRONGER mode',
159
+ from: "if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = DEFAULT_PARAMS.formalVerify",
160
+ to: "if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = 'require'" },
161
+ { name: 'v2-fidelity-switch-removed', preset: 'v2',
162
+ guarantee: 'a passing Lean run must switch the verification prompt to a FIDELITY review',
163
+ from: "L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')",
164
+ to: "if (false) L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')" },
165
+ // v2 enforces `require` at SEVERAL sites, and they mask each other on purpose (defence in
166
+ // depth): the pre-mutation guard in settleVerdict/processStatusUpdates, and the card-writing
167
+ // choke points writeVerifiedCardIfNeeded / writeVerifiedProblemCardIfNeeded. Removing ONE
168
+ // site is therefore semantically INERT — the others still defer — so a per-site probe would
169
+ // always be green and would report a blind spot that does not exist (AUDIT-CHECKLIST §2.5,
170
+ // "变异必须真的改变行为"). The observable probe is the SHARED predicate that every site
171
+ // consults: `formalRequired()` returning false disables the whole gate.
172
+ { name: 'v2-require-gate-disabled', preset: 'v2',
173
+ guarantee: 'require mode must withhold a true/false verdict (proposition AND problem path) until the object is Lean-passed or explicitly blocked',
174
+ from: "function formalRequired() { return formalMode() === 'require' }",
175
+ to: "function formalRequired() { return false }" },
176
+ { name: 'v2-blocked-note-not-required', preset: 'v2',
177
+ guarantee: 'a blocker record must carry a reason',
178
+ from: "if (!note) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
179
+ to: "if (false) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)" },
180
+ { name: 'v2-proof-not-archived-under-verified', preset: 'v2',
181
+ guarantee: "a passing proof must be archived as that object's proof",
182
+ from: "if (passed) await writeText('Verified/Lean/' + target + '.lean', body)",
183
+ to: "if (false) await writeText('Verified/Lean/' + target + '.lean', body)" },
184
+ { name: 'v2-lean-path-guard-naive', preset: 'v2',
185
+ guarantee: 'the Lean path guard must normalise ..',
186
+ from: "const norm = normalizeAbsPath(abs)",
187
+ to: "const norm = abs" },
188
+ { name: 'v2-lean-run-tool-handler-unbound', preset: 'v2',
189
+ guarantee: 'the three Lean tools must be registered and dispatched',
190
+ from: ", 'vibe_math_lean_run')",
191
+ to: ", 'vibe_math_lean_run_DISABLED')" },
192
+ ]
193
+
194
+ let ok = 0, bad = 0
195
+ console.log('-- Lean formal-verification sensitivity probes --')
196
+ console.log('(a probe passes when breaking the guarantee turns that preset\'s suite RED)')
197
+ console.log('')
198
+
199
+ for (const p of probes) {
200
+ const preset = PLUGINS[p.preset]
201
+ const original = ORIGINAL[p.preset]
202
+ if (p.from.startsWith('PLACEHOLDER_')) {
203
+ console.error(' SETUP-FAIL - ' + p.name + ': anchor not filled in yet')
204
+ bad++
205
+ continue
206
+ }
207
+ const occurrences = original.split(p.from).length - 1
208
+ if (occurrences !== 1) {
209
+ console.error(' SETUP-FAIL - ' + p.name + ' [' + p.preset + ']: anchor matched ' + occurrences + ' times (need exactly 1)')
210
+ bad++
211
+ continue
212
+ }
213
+ const mutated = original.replace(p.from, p.to)
214
+ const file = join(dir, p.name + '.js')
215
+ writeFileSync(file, mutated, 'utf8')
216
+
217
+ // A mutation that does not even parse is red for the WRONG reason.
218
+ const chk = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
219
+ if (chk.status !== 0) {
220
+ console.error(' SETUP-FAIL - ' + p.name + ': the mutated copy has a syntax error:\n' + String(chk.stderr || '').split('\n').slice(0, 4).join('\n'))
221
+ bad++
222
+ continue
223
+ }
224
+
225
+ const env = Object.assign({}, process.env)
226
+ env[preset.env] = file
227
+ const r = spawnSync(process.execPath, [join(REPO, preset.suite)], { env, encoding: 'utf8', cwd: REPO })
228
+ if (r.status === null) {
229
+ console.error(' SETUP-FAIL - ' + p.name + ': the suite could not be started (' + String(r.error && r.error.message) + ')')
230
+ bad++
231
+ continue
232
+ }
233
+ if (r.status !== 0) {
234
+ ok++
235
+ console.log(' ok - ' + p.name + ' [' + p.preset + '] => suite went RED as required [' + p.guarantee + ']')
236
+ } else {
237
+ bad++
238
+ console.error(' BLIND SPOT - ' + p.name + ' [' + p.preset + '] => suite stayed GREEN, so it does NOT detect: ' + p.guarantee)
239
+ }
240
+ }
241
+
242
+ rmSync(dir, { recursive: true, force: true })
243
+ console.log('')
244
+ console.log('formal sensitivity: ' + ok + ' probes detected the break, ' + bad + ' problems')
245
+ if (bad) process.exit(1)
246
+ console.log('ALL FORMAL PROBES RED AS REQUIRED')
247
+ process.exit(0)
@@ -0,0 +1,249 @@
1
+ // ============================================================
2
+ // PERSONA SURFACE SENSITIVITY PROBES
3
+ //
4
+ // `audit-persona-surface.test.mjs` compares each preset's persona prompt against the tools
5
+ // that preset actually registers. It is the only guard against two failures that no other
6
+ // suite can see (they all call apply(ctx) directly and never load the YAML):
7
+ //
8
+ // · a registered tool that the persona never names → the agent cannot discover it;
9
+ // · a persona that names an unregistered tool → the agent is told to call a tool
10
+ // that does not exist.
11
+ //
12
+ // The Lean formal-verification feature shipped with both, in three of four presets. This
13
+ // script proves the guard is not vacuous: each probe copies the preset tree, applies ONE
14
+ // mutation, and requires the suite to go RED.
15
+ //
16
+ // False-green traps handled (see AUDIT-CHECKLIST.md §2):
17
+ // 1. a probe whose own spawn fails — reported as SETUP-FAIL, never as a detection;
18
+ // 2. a suite that ignores its override env var — `PERSONA_ROOT` is exercised in both
19
+ // directions (a NON-mutated copy must keep the suite GREEN before any probe runs);
20
+ // 3. a semantically inert mutation — the anchor must match an exact, asserted count, and
21
+ // for YAML probes the mutated copy must still yield the persona's two literal blocks;
22
+ // 4. a mutation that breaks syntax — every mutated `.js` copy is `node --check`ed, and a
23
+ // mutated `.yml` whose persona markers are damaged is a SETUP-FAIL, not a detection.
24
+ //
25
+ // Run: node audit-persona-sensitivity.mjs
26
+ // ============================================================
27
+ import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, cpSync } from 'node:fs'
28
+ import { tmpdir } from 'node:os'
29
+ import { join } from 'node:path'
30
+ import { spawnSync } from 'node:child_process'
31
+ import { fileURLToPath } from 'node:url'
32
+
33
+ const REPO = fileURLToPath(new URL('.', import.meta.url))
34
+ const SUITE = join(REPO, 'audit-persona-surface.test.mjs')
35
+ const PRESETS = [
36
+ { dir: 'vibe-math-v2', js: 'vibe-math-v2.js' },
37
+ { dir: 'vibe-math-v3', js: 'vibe-math-v3.js' },
38
+ { dir: 'vibe-math-v4', js: 'vibe-math-v4.js' },
39
+ { dir: 'vibe-math-v5', js: 'vibe-math-v5.js' },
40
+ ]
41
+
42
+ /** Prepare a private copy of every preset (persona + plugin), so one probe cannot leak. */
43
+ function prepareRoot() {
44
+ const root = mkdtempSync(join(tmpdir(), 'persona-sens-'))
45
+ for (const P of PRESETS) {
46
+ mkdirSync(join(root, P.dir), { recursive: true })
47
+ cpSync(join(REPO, P.dir, 'agent.cordis.yml'), join(root, P.dir, 'agent.cordis.yml'))
48
+ cpSync(join(REPO, P.dir, P.js), join(root, P.dir, P.js))
49
+ }
50
+ return root
51
+ }
52
+
53
+ /** The persona's two literal blocks must survive a YAML mutation, or the probe is unfair. */
54
+ function yamlStillHasPersona(yml) {
55
+ if (!/^\s*-\s*id:\s*persona\s*$/m.test(yml)) return false
56
+ const markers = yml.match(/^\s*(?:prefix|text):\s*\|-?\s*$/gm) || []
57
+ return markers.length === 2
58
+ }
59
+
60
+ function runSuite(root) {
61
+ const env = Object.assign({}, process.env, { PERSONA_ROOT: root })
62
+ const r = spawnSync(process.execPath, [SUITE], { env, encoding: 'utf8', cwd: REPO })
63
+ return { status: r.status, error: r.error, out: String(r.stdout || '') + String(r.stderr || '') }
64
+ }
65
+
66
+ // A sanity gate first: the UNMUTATED copy must be GREEN through the override, otherwise
67
+ // every probe below would "detect" the override itself rather than the mutation (trap 2).
68
+ const sanityRoot = prepareRoot()
69
+ {
70
+ const r = runSuite(sanityRoot)
71
+ if (r.status !== 0) {
72
+ console.error(' SETUP-FAIL - the suite is RED on an unmutated copy through PERSONA_ROOT:')
73
+ console.error(r.out.split('\n').filter((l) => l.includes('FAIL')).slice(0, 8).join('\n'))
74
+ rmSync(sanityRoot, { recursive: true, force: true })
75
+ process.exit(1)
76
+ }
77
+ console.log(' ok - control: unmutated copy is GREEN through PERSONA_ROOT (the override works)')
78
+ }
79
+ rmSync(sanityRoot, { recursive: true, force: true })
80
+
81
+ const probes = [
82
+ // ── v2 ─────────────────────────────────────────────────────────────────────
83
+ {
84
+ name: 'v2-lean-tools-line-removed',
85
+ preset: 'vibe-math-v2', file: 'agent.cordis.yml', nth: 'all', expect: 2,
86
+ guarantee: 'the three Lean tools must be named in the persona the main agent receives',
87
+ from: ' - vibe_math_lean_run / vibe_math_lean_archive / vibe_math_lean_lib — Lean formal\n verification (execute / archive / list the reuse library). The scheduler\'s child agents\n use them too; they work in every mode.\n',
88
+ to: '',
89
+ },
90
+ {
91
+ name: 'v2-phantom-tool-mentioned',
92
+ preset: 'vibe-math-v2', file: 'agent.cordis.yml', nth: 'all', expect: 2,
93
+ guarantee: 'the persona must not advertise a tool that is not registered (the agent would call it and fail)',
94
+ from: ' - vibe_math_lean_run / vibe_math_lean_archive / vibe_math_lean_lib — Lean formal',
95
+ to: ' - vibe_math_lean_exec / vibe_math_lean_run / vibe_math_lean_archive / vibe_math_lean_lib — Lean formal',
96
+ },
97
+
98
+ // ── v3 ─────────────────────────────────────────────────────────────────────
99
+ {
100
+ name: 'v3-persona-tool-name-typo',
101
+ preset: 'vibe-math-v3', file: 'agent.cordis.yml', nth: 'all', expect: 2,
102
+ guarantee: 'a typo in a persona tool name must be caught (an agent copying it calls a tool that does not exist)',
103
+ from: 'vibe_math_lean_archive',
104
+ to: 'vibe_math_lean_archiv',
105
+ },
106
+ {
107
+ name: 'v3-tool-registered-but-not-documented',
108
+ preset: 'vibe-math-v3', file: 'vibe-math-v3.js', nth: 'first', expect: 2,
109
+ guarantee: 'a newly registered tool must be documented in the persona (or the snapshot updated on purpose)',
110
+ from: " registerTool('vibe_math_lean_run', '(member) Execute the Lean toolchain",
111
+ to: " registerTool('vibe_math_extra_tool', 'x', objParams({}), async function () { return {} })\n registerTool('vibe_math_lean_run', '(member) Execute the Lean toolchain",
112
+ },
113
+
114
+ // ── v4 ─────────────────────────────────────────────────────────────────────
115
+ {
116
+ name: 'v4-formal-mode-renamed-in-persona',
117
+ preset: 'vibe-math-v4', file: 'agent.cordis.yml', nth: 'all', expect: 2,
118
+ guarantee: 'the persona must spell the three mode names exactly as the parameter accepts them',
119
+ from: " - 'off' (default, no extra requirement) | 'encourage' (the residents decide by implementation\n difficulty whether to formalize in Lean; once a Lean run passes, their unanimous vote becomes\n a FIDELITY review — do the Lean definitions/objects/conditions/assumptions/conclusion match\n the proposition as stated) | 'require' (same, plus a gate: a unanimous true/false verdict is",
120
+ to: " - 'off' (default, no extra requirement) | 'encourage' (the residents decide by implementation\n difficulty whether to formalize in Lean; once a Lean run passes, their unanimous vote becomes\n a FIDELITY review — do the Lean definitions/objects/conditions/assumptions/conclusion match\n the proposition as stated) | 'forced' (same, plus a gate: a unanimous true/false verdict is",
121
+ },
122
+ {
123
+ name: 'v4-formal-report-tool-hidden',
124
+ preset: 'vibe-math-v4', file: 'agent.cordis.yml', nth: 'all', expect: 2,
125
+ guarantee: 'the formal-report tool must be named in the persona (it is the coordinator-facing Lean view)',
126
+ from: ' - vibe_v4_formal_report — human-readable Lean formal-verification mirror (mode, Lean-passed',
127
+ to: ' - vibe_v4_formal_reportX — human-readable Lean formal-verification mirror (mode, Lean-passed',
128
+ },
129
+
130
+ // ── v5 ─────────────────────────────────────────────────────────────────────
131
+ {
132
+ name: 'v5-prefix-text-drift',
133
+ preset: 'vibe-math-v5', file: 'agent.cordis.yml', nth: 'second', expect: 2,
134
+ guarantee: 'prefix and text must stay identical apart from line 0 (old and new hosts must see the same surface)',
135
+ from: 'TRUST RULE: only Verified/',
136
+ to: 'TRUST RULE (amended): only Verified/',
137
+ },
138
+ {
139
+ name: 'v5-tool-renamed-in-plugin',
140
+ preset: 'vibe-math-v5', file: 'vibe-math-v5.js', nth: 'first', expect: 1,
141
+ guarantee: 'renaming a registered tool must be caught (the persona still advertises the old name)',
142
+ from: " registerTool('vibe_v5_meeting',",
143
+ to: " registerTool('vibe_v5_meeting2',",
144
+ },
145
+ {
146
+ name: 'v4-usage-advertises-phantom-subcommand',
147
+ preset: 'vibe-math-v4', file: 'vibe-math-v4.js', nth: 'first', expect: 1,
148
+ guarantee: 'the unknown-subcommand usage string must not advertise a subcommand that no branch implements',
149
+ from: "usage:'configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set'",
150
+ to: "usage:'configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|scan|set'",
151
+ },
152
+ {
153
+ name: 'v4-hint-advertises-unimplemented-subcommand',
154
+ preset: 'vibe-math-v4', file: 'vibe-math-v4.js', nth: 'first', expect: 1,
155
+ guarantee: 'the typing hint must not advertise a subcommand the handler does not implement',
156
+ from: "message <to|all> <content>|meeting|members|add|remove|set]'",
157
+ to: "message <to|all> <content>|meeting|add|remove|set]'",
158
+ },
159
+ {
160
+ name: 'v5-persona-slash-list-drops-add-remove',
161
+ preset: 'vibe-math-v5', file: 'agent.cordis.yml', nth: 'all', expect: 2,
162
+ guarantee: "the persona's /v5 list must match the command hint (a human reading the persona must see every subcommand)",
163
+ from: 'meeting|hire|fire|add|remove|set)',
164
+ to: 'meeting|hire|fire|set)',
165
+ },
166
+ ]
167
+
168
+ let ok = 0
169
+ let bad = 0
170
+ console.log('')
171
+ console.log('-- persona surface sensitivity probes --')
172
+ console.log('(a probe passes when breaking the guarantee turns audit-persona-surface.test.mjs RED)')
173
+ console.log('')
174
+
175
+ for (const p of probes) {
176
+ const root = prepareRoot()
177
+ const target = join(root, p.preset, p.file)
178
+ const original = readFileSync(target, 'utf8')
179
+ // The repo's YAML files are CRLF; anchors are written with \n. Normalise both directions so
180
+ // a multi-line anchor matches regardless of the checkout's line endings.
181
+ const eol = original.includes('\r\n') ? '\r\n' : '\n'
182
+ const from = p.from.split('\n').join(eol)
183
+ const to = p.to.split('\n').join(eol)
184
+ p.from = from
185
+ p.to = to
186
+ const occurrences = original.split(p.from).length - 1
187
+ if (occurrences !== p.expect) {
188
+ console.error(` SETUP-FAIL - ${p.name}: anchor matched ${occurrences} times (need ${p.expect})`)
189
+ bad++
190
+ rmSync(root, { recursive: true, force: true })
191
+ continue
192
+ }
193
+ let mutated
194
+ if (p.nth === 'all') mutated = original.split(p.from).join(p.to)
195
+ else if (p.nth === 'first') mutated = original.replace(p.from, p.to)
196
+ else if (p.nth === 'second') {
197
+ const i = original.indexOf(p.from)
198
+ const j = original.indexOf(p.from, i + 1)
199
+ mutated = original.slice(0, j) + p.to + original.slice(j + p.from.length)
200
+ } else throw new Error('unknown nth ' + p.nth)
201
+
202
+ if (mutated === original) {
203
+ console.error(` SETUP-FAIL - ${p.name}: the mutation is a no-op`)
204
+ bad++
205
+ rmSync(root, { recursive: true, force: true })
206
+ continue
207
+ }
208
+ if (p.file.endsWith('.yml') && !yamlStillHasPersona(mutated)) {
209
+ console.error(` SETUP-FAIL - ${p.name}: the mutated persona no longer has its two literal blocks (unfair mutation)`)
210
+ bad++
211
+ rmSync(root, { recursive: true, force: true })
212
+ continue
213
+ }
214
+ writeFileSync(target, mutated, 'utf8')
215
+
216
+ if (p.file.endsWith('.js')) {
217
+ const chk = spawnSync(process.execPath, ['--check', target], { encoding: 'utf8' })
218
+ if (chk.status !== 0) {
219
+ console.error(` SETUP-FAIL - ${p.name}: the mutated copy has a syntax error:\n` + String(chk.stderr || '').split('\n').slice(0, 4).join('\n'))
220
+ bad++
221
+ rmSync(root, { recursive: true, force: true })
222
+ continue
223
+ }
224
+ }
225
+
226
+ const r = runSuite(root)
227
+ if (r.status === null) {
228
+ console.error(` SETUP-FAIL - ${p.name}: the suite could not be started (${String(r.error && r.error.message)})`)
229
+ bad++
230
+ rmSync(root, { recursive: true, force: true })
231
+ continue
232
+ }
233
+ if (r.status !== 0) {
234
+ const firstFail = (r.out.split('\n').find((l) => l.trim().startsWith('FAIL')) || '').trim().slice(0, 110)
235
+ ok++
236
+ console.log(` ok - ${p.name} [${p.preset}] => suite went RED as required [${p.guarantee}]`)
237
+ if (firstFail) console.log(` ${firstFail}`)
238
+ } else {
239
+ bad++
240
+ console.error(` BLIND SPOT - ${p.name} [${p.preset}] => suite stayed GREEN, so it does NOT detect: ${p.guarantee}`)
241
+ }
242
+ rmSync(root, { recursive: true, force: true })
243
+ }
244
+
245
+ console.log('')
246
+ console.log(`persona sensitivity: ${ok} probes detected the break, ${bad} problems`)
247
+ if (bad) process.exit(1)
248
+ console.log('ALL PERSONA PROBES RED AS REQUIRED')
249
+ process.exit(0)