dsh-vibe-math 2.2.2 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT-CHECKLIST.md +61 -3
- package/README.md +119 -1
- package/RELEASE-NOTES-2.3.0.md +207 -0
- package/RELEASE-NOTES-2.3.1.md +134 -0
- package/audit-formal-sensitivity.mjs +333 -0
- package/audit-persona-sensitivity.mjs +249 -0
- package/audit-persona-surface.test.mjs +349 -0
- package/audit-v5-integrity.mjs +43 -2
- package/audit-v5-sensitivity.mjs +77 -6
- package/docs/formal-verification.md +401 -0
- package/docs/generate_framework_diagram_v5.mjs +22 -16
- package/docs/test-timing.md +79 -0
- package/formal-verify-v2.test.mjs +951 -0
- package/formal-verify-v3.test.mjs +1031 -0
- package/formal-verify-v4.test.mjs +882 -0
- package/formal-verify-v5.test.mjs +598 -0
- package/package.json +22 -2
- package/prompt-corpus-persona/persona-corpus.json +32 -0
- package/prompt-corpus-persona/persona-corpus.md +674 -0
- package/prompt-corpus-v2/formal-verify-v2.json +394 -0
- package/prompt-corpus-v2/formal-verify-v2.md +4250 -0
- package/prompt-corpus-v3/formal-verify-v3.json +382 -0
- package/prompt-corpus-v3/formal-verify-v3.md +3843 -0
- package/prompt-corpus-v4/formal-verify-v4.json +84 -0
- package/prompt-corpus-v4/formal-verify-v4.md +255 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +109 -5
- package/prompt-corpus-v5/prompt-corpus-v5.md +653 -109
- package/prompt-v5-integrity.test.mjs +1158 -984
- package/run-tests.mjs +99 -0
- package/vibe-math-v2/agent.cordis.yml +40 -2
- package/vibe-math-v2/vibe-math-v2.js +811 -21
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +218 -1
- package/vibe-math-v3/agent.cordis.yml +46 -2
- package/vibe-math-v3/vibe-math-v3.js +810 -21
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +104 -2
- package/vibe-math-v4/agent.cordis.yml +46 -4
- package/vibe-math-v4/vibe-math-v4.js +744 -15
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +255 -0
- package/vibe-math-v5/agent.cordis.yml +41 -5
- package/vibe-math-v5/vibe-math-v5.js +621 -9
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +131 -4
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +57 -0
- 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,333 @@
|
|
|
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 arg = process.argv.find((a) => a.startsWith('--only='))
|
|
235
|
+
return arg ? arg.split('=')[1] : ''
|
|
236
|
+
})()
|
|
237
|
+
const selected = probes.filter((p) => !ONLY || p.name.includes(ONLY) || p.preset === ONLY)
|
|
238
|
+
if (process.argv.includes('--list')) {
|
|
239
|
+
for (const p of selected) console.log(p.preset + ' ' + p.name)
|
|
240
|
+
process.exit(0)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function runAsync(cmd, args, opts) {
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
const child = spawn(cmd, args, opts)
|
|
246
|
+
let out = '', err = ''
|
|
247
|
+
child.stdout.on('data', (d) => { out += d.toString() })
|
|
248
|
+
child.stderr.on('data', (d) => { err += d.toString() })
|
|
249
|
+
child.on('error', (e) => resolve({ status: null, error: e, stdout: out, stderr: err }))
|
|
250
|
+
child.on('close', (status) => resolve({ status, stdout: out, stderr: err }))
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// One probe = one targeted mutation + one run of that preset's suite. Returns a verdict object;
|
|
255
|
+
// never throws, so a single bad probe cannot take the pool down.
|
|
256
|
+
async function runProbe(p) {
|
|
257
|
+
const t0 = Date.now()
|
|
258
|
+
const preset = PLUGINS[p.preset]
|
|
259
|
+
const original = ORIGINAL[p.preset]
|
|
260
|
+
const done = (kind, detail) => ({ p, kind, detail, ms: Date.now() - t0 })
|
|
261
|
+
if (p.from.startsWith('PLACEHOLDER_')) return done('setup', 'anchor not filled in yet')
|
|
262
|
+
const want = p.expect === undefined ? 1 : p.expect
|
|
263
|
+
const occurrences = original.split(p.from).length - 1
|
|
264
|
+
if (occurrences !== want) return done('setup', 'anchor matched ' + occurrences + ' times (need exactly ' + want + ')')
|
|
265
|
+
// Mutate EVERY occurrence the anchor was asserted to have. `replace()` would only hit the first
|
|
266
|
+
// one, which silently produced a fake blind spot: the v5 reply contract is emitted in two places
|
|
267
|
+
// (replySpec + the voting prompt), so replacing just one left the other intact and the suite —
|
|
268
|
+
// correctly — stayed green.
|
|
269
|
+
const mutated = want > 1 ? original.split(p.from).join(p.to) : original.replace(p.from, p.to)
|
|
270
|
+
// One directory per probe: the mutated copy AND its corpus output (several suites write a
|
|
271
|
+
// corpus, and concurrent runs of the same suite must not race on that file).
|
|
272
|
+
const pdir = join(dir, p.name)
|
|
273
|
+
mkdirSync(pdir, { recursive: true })
|
|
274
|
+
const file = join(pdir, 'plugin.js')
|
|
275
|
+
writeFileSync(file, mutated, 'utf8')
|
|
276
|
+
// A mutation that does not even parse is red for the WRONG reason.
|
|
277
|
+
const chk = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
|
|
278
|
+
if (chk.status !== 0) return done('setup', 'the mutated copy has a syntax error: ' + String(chk.stderr || '').split('\n').slice(0, 4).join(' '))
|
|
279
|
+
const env = Object.assign({}, process.env)
|
|
280
|
+
env[preset.env] = file
|
|
281
|
+
if (preset.corpusEnv) env[preset.corpusEnv] = join(pdir, 'corpus')
|
|
282
|
+
const r = await runAsync(process.execPath, [join(REPO, preset.suite)], { env, encoding: 'utf8', cwd: REPO })
|
|
283
|
+
if (r.status === null) return done('setup', 'the suite could not be started (' + String(r.error && r.error.message) + ')')
|
|
284
|
+
if (r.status !== 0) return done('ok', '')
|
|
285
|
+
return done('blind', '')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const results = new Array(selected.length)
|
|
289
|
+
let cursor = 0
|
|
290
|
+
let finished = 0
|
|
291
|
+
const wall0 = Date.now()
|
|
292
|
+
async function worker() {
|
|
293
|
+
for (;;) {
|
|
294
|
+
const i = cursor++
|
|
295
|
+
if (i >= selected.length) return
|
|
296
|
+
const res = await runProbe(selected[i])
|
|
297
|
+
results[i] = res
|
|
298
|
+
finished++
|
|
299
|
+
const tag = '[' + String(finished).padStart(2) + '/' + selected.length + ']'
|
|
300
|
+
const secs = (res.ms / 1000).toFixed(1) + 's'
|
|
301
|
+
if (res.kind === 'ok') console.log(' ok - ' + res.p.name + ' [' + res.p.preset + '] => suite went RED as required (' + secs + ') [' + res.p.guarantee + ']')
|
|
302
|
+
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)
|
|
303
|
+
else console.error(' SETUP-FAIL ' + tag + ' - ' + res.p.name + ' [' + res.p.preset + '] (' + secs + '): ' + res.detail)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, selected.length) }, () => worker()))
|
|
307
|
+
|
|
308
|
+
// ── timing summary: this is the feedback that decides the next run's strategy ────────────
|
|
309
|
+
{
|
|
310
|
+
const wall = (Date.now() - wall0) / 1000
|
|
311
|
+
const sum = results.reduce((a, r) => a + (r ? r.ms : 0), 0) / 1000
|
|
312
|
+
const perPreset = {}
|
|
313
|
+
for (const r of results) {
|
|
314
|
+
if (!r) continue
|
|
315
|
+
perPreset[r.p.preset] = perPreset[r.p.preset] || { n: 0, s: 0 }
|
|
316
|
+
perPreset[r.p.preset].n++
|
|
317
|
+
perPreset[r.p.preset].s += r.ms / 1000
|
|
318
|
+
}
|
|
319
|
+
const slow = results.filter(Boolean).slice().sort((a, b) => b.ms - a.ms).slice(0, 5)
|
|
320
|
+
console.log('')
|
|
321
|
+
console.log('-- timing --')
|
|
322
|
+
console.log(' concurrency ' + CONCURRENCY + ' · wall ' + wall.toFixed(1) + 's · sum of probe times ' + sum.toFixed(1) + 's'
|
|
323
|
+
+ ' · speed-up x' + (sum / Math.max(wall, 0.001)).toFixed(2))
|
|
324
|
+
console.log(' per preset: ' + Object.keys(perPreset).sort().map((k) => k + ' ' + perPreset[k].n + ' probes/' + perPreset[k].s.toFixed(0) + 's').join(' · '))
|
|
325
|
+
console.log(' slowest: ' + slow.map((r) => r.p.name + ' ' + (r.ms / 1000).toFixed(1) + 's').join(' · '))
|
|
326
|
+
ok = results.filter((r) => r && r.kind === 'ok').length
|
|
327
|
+
bad = results.filter((r) => r && r.kind !== 'ok').length
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log('formal sensitivity: ' + ok + ' probes detected the break, ' + bad + ' problems')
|
|
331
|
+
if (bad) process.exit(1)
|
|
332
|
+
console.log('ALL FORMAL PROBES RED AS REQUIRED')
|
|
333
|
+
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)
|