dsh-vibe-math 2.3.11 → 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.
- package/README.md +17 -40
- package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
- package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
- package/docs/generate_framework_diagram_v2.py +114 -0
- package/docs/generate_framework_diagram_v3.py +127 -0
- package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
- package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
- package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
- package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
- package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
- package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
- package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
- package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -0
- package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
- package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
- package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
- package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
- package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
- package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
- package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
- package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
- package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
- package/docs/test-timing.md +18 -17
- package/installer.js +258 -53
- package/package.json +50 -40
- package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
- package/tests/audit-installer-compat.test.mjs +136 -0
- package/tests/audit-installer-policy.test.mjs +245 -0
- package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
- package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
- package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
- package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
- package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
- package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
- package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
- package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
- package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
- package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
- package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
- package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
- package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
- package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
- package//347/244/272/344/276/213/345/233/276//345/256/236/351/231/205/344/275/277/347/224/250/347/244/272/344/276/213-/351/225/277/346/210/252/345/233/276.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v2.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v3.png +0 -0
- /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
|
@@ -1,249 +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('
|
|
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)
|
|
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 tests/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, 'tests', '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)
|