dsh-vibe-math 2.3.12 → 2.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -13
- 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/{RELEASE-NOTES-2.3.12.md → docs/release-notes/RELEASE-NOTES-2.3.12.md} +80 -80
- 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 +105 -41
- package/package.json +42 -37
- package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
- package/{audit-installer-compat.test.mjs → tests/audit-installer-compat.test.mjs} +136 -136
- 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/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
|
@@ -1,448 +1,448 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// V5 INTEGRITY AUDIT — static self-check of vibe-math-v5.js against the ways this
|
|
3
|
-
// kind of single-file plugin actually breaks:
|
|
4
|
-
// 1. a function CALLED but never defined (ReferenceError at runtime only)
|
|
5
|
-
// 2. a `params.X` read for a key that DEFAULT_PARAMS never declares (silent undefined)
|
|
6
|
-
// 3. a tool handler calling `s.NAME(...)` on the session API object that the session
|
|
7
|
-
// never returns (TypeError only when that tool is used)
|
|
8
|
-
// 4. documented error codes that are never raised, and raised codes never documented
|
|
9
|
-
// 5. leftover development markers / TODO scaffolding
|
|
10
|
-
// Run: node audit-v5-integrity.mjs (exit 1 on any finding)
|
|
11
|
-
// ============================================================
|
|
12
|
-
import { readFileSync, existsSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
13
|
-
import { spawnSync } from 'node:child_process'
|
|
14
|
-
import { tmpdir } from 'node:os'
|
|
15
|
-
import { join } from 'node:path'
|
|
16
|
-
|
|
17
|
-
const FILE = new URL('
|
|
18
|
-
const raw = readFileSync(FILE, 'utf8')
|
|
19
|
-
// Strip comments, string literals AND regex literals before any identifier scan. Without this the
|
|
20
|
-
// heuristic matches English words inside comments that merely precede a '(' (e.g.
|
|
21
|
-
// "// per unit (" becomes a phantom call to unit()), drowning the real findings.
|
|
22
|
-
//
|
|
23
|
-
// REGEX LITERALS ARE NOT OPTIONAL HERE: v5 contains `/[\\/:*?"<>|\u0000-\u001f]+/` — a character class
|
|
24
|
-
// holding a DOUBLE QUOTE. A scanner without regex support reads that quote as a string start and
|
|
25
|
-
// mangles the rest of the line. Measured on this very file (old vs new, line by line): 30 of 4365
|
|
26
|
-
// lines differed and the stripped output did NOT parse. No identifier this audit checks
|
|
27
|
-
// (call names, `params.*` reads, `s.*()` methods) happened to be missed in the current source, so this
|
|
28
|
-
// was latent rather than exploited — but a single call appearing only on such a line would have been
|
|
29
|
-
// invisible. The self-check at the bottom of this file keeps the scanner honest.
|
|
30
|
-
// The keyword rule matters for the same reason as in audit-prompt-invariants.mjs (`return /…/`).
|
|
31
|
-
function stripNoise(s) {
|
|
32
|
-
let out = ''
|
|
33
|
-
let i = 0
|
|
34
|
-
const n = s.length
|
|
35
|
-
let prev = '' // last significant token (single char, or a whole word such as `return`)
|
|
36
|
-
let word = ''
|
|
37
|
-
const REGEX_AFTER_KEYWORD = /^(?:return|typeof|case|delete|void|instanceof|in|of|yield|await|new|do|else)$/
|
|
38
|
-
const regexAllowed = () => prev === '' || REGEX_AFTER_KEYWORD.test(prev) || /[(,=:[!&|?{};+\-*%~^<>]/.test(prev)
|
|
39
|
-
while (i < n) {
|
|
40
|
-
const c = s[i], c2 = s[i + 1]
|
|
41
|
-
if (c === '/' && c2 === '/') { while (i < n && s[i] !== '\n') i++; continue }
|
|
42
|
-
if (c === '/' && c2 === '*') { i += 2; while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++; i += 2; continue }
|
|
43
|
-
if (c === '/' && regexAllowed()) {
|
|
44
|
-
i++
|
|
45
|
-
let inClass = false
|
|
46
|
-
while (i < n) {
|
|
47
|
-
if (s[i] === '\\') { i += 2; continue }
|
|
48
|
-
if (s[i] === '[') inClass = true
|
|
49
|
-
else if (s[i] === ']') inClass = false
|
|
50
|
-
else if (s[i] === '/' && !inClass) { i++; break }
|
|
51
|
-
else if (s[i] === '\n') break // a regex literal cannot span lines
|
|
52
|
-
i++
|
|
53
|
-
}
|
|
54
|
-
while (i < n && /[a-z]/i.test(s[i])) i++ // flags
|
|
55
|
-
out += "''" // a value placeholder, like strings: keeps `X: <value>` shapes but no phantom calls
|
|
56
|
-
prev = ')'
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
if (c === "'" || c === '"' || c === '`') {
|
|
60
|
-
const q = c
|
|
61
|
-
i++
|
|
62
|
-
while (i < n) {
|
|
63
|
-
if (s[i] === '\\') { i += 2; continue }
|
|
64
|
-
if (s[i] === q) { i++; break }
|
|
65
|
-
if (q !== '`' && s[i] === '\n') break
|
|
66
|
-
i++
|
|
67
|
-
}
|
|
68
|
-
out += q + q // keep a placeholder so `X: ''` shape survives
|
|
69
|
-
prev = ')'
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
out += c
|
|
73
|
-
if (/[A-Za-z0-9_$]/.test(c)) word += c
|
|
74
|
-
else { if (word) { prev = word; word = '' } if (!/\s/.test(c)) prev = c }
|
|
75
|
-
i++
|
|
76
|
-
}
|
|
77
|
-
return out
|
|
78
|
-
}
|
|
79
|
-
const src = stripNoise(raw)
|
|
80
|
-
const findings = []
|
|
81
|
-
const notes = []
|
|
82
|
-
|
|
83
|
-
const lineOf = (idx) => src.slice(0, idx).split('\n').length
|
|
84
|
-
|
|
85
|
-
// ---- 1. called-but-undefined -------------------------------------------
|
|
86
|
-
const defined = new Set()
|
|
87
|
-
for (const m of src.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)) defined.add(m[1])
|
|
88
|
-
for (const m of src.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g)) defined.add(m[1])
|
|
89
|
-
for (const m of src.matchAll(/(?:const|let|var)\s+\{([^}]+)\}\s*=/g)) {
|
|
90
|
-
for (const part of m[1].split(',')) { const n = part.split(':').pop().trim(); if (n) defined.add(n) }
|
|
91
|
-
}
|
|
92
|
-
for (const m of src.matchAll(/catch\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) defined.add(m[1])
|
|
93
|
-
// Function/method PARAMETERS are locally bound identifiers too, not free calls.
|
|
94
|
-
for (const m of src.matchAll(/function\s*[A-Za-z_$]*\s*\(([^)]*)\)/g)) {
|
|
95
|
-
for (const raw of m[1].split(',')) {
|
|
96
|
-
const name = raw.trim().replace(/[={].*$/s, '').trim()
|
|
97
|
-
if (/^[A-Za-z_$][\w$]*$/.test(name)) defined.add(name)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
for (const m of src.matchAll(/\(([^()]*)\)\s*=>/g)) {
|
|
101
|
-
for (const raw of m[1].split(',')) {
|
|
102
|
-
const name = raw.trim().replace(/[={].*$/s, '').trim()
|
|
103
|
-
if (/^[A-Za-z_$][\w$]*$/.test(name)) defined.add(name)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
for (const m of src.matchAll(/([A-Za-z_$][\w$]*)\s*=>/g)) defined.add(m[1])
|
|
107
|
-
// JS/DSH globals that are legitimately free
|
|
108
|
-
const GLOBALS = new Set([
|
|
109
|
-
'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'function', 'new', 'await', 'delete', 'void', 'do', 'else', 'try',
|
|
110
|
-
'Array', 'Object', 'JSON', 'Number', 'String', 'Boolean', 'Math', 'Date', 'Promise', 'Set', 'Map', 'WeakRef', 'WeakMap', 'Symbol', 'Error',
|
|
111
|
-
'isFinite', 'isNaN', 'parseInt', 'parseFloat', 'encodeURIComponent', 'decodeURIComponent', 'structuredClone',
|
|
112
|
-
'AbortSignal', 'console', 'require', 'import', 'super', 'this', 'of', 'in', 'instanceof',
|
|
113
|
-
'RegExp', 'Proxy', 'Reflect', 'AggregateError', 'TextEncoder', 'TextDecoder', 'URL', 'BigInt', 'Intl', 'Buffer', 'process',
|
|
114
|
-
])
|
|
115
|
-
const LOCAL_METHODS = new Set()
|
|
116
|
-
for (const m of src.matchAll(/[{,]\s*([A-Za-z_$][\w$]*)\s*[:(]/g)) LOCAL_METHODS.add(m[1])
|
|
117
|
-
for (const m of src.matchAll(/\.\s*([A-Za-z_$][\w$]*)\s*\(/g)) LOCAL_METHODS.add(m[1])
|
|
118
|
-
|
|
119
|
-
const called = new Map()
|
|
120
|
-
for (const m of src.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
121
|
-
const name = m[1]
|
|
122
|
-
if (GLOBALS.has(name) || LOCAL_METHODS.has(name)) continue
|
|
123
|
-
if (!called.has(name)) called.set(name, { n: 0, line: lineOf(m.index) })
|
|
124
|
-
called.get(name).n++
|
|
125
|
-
}
|
|
126
|
-
for (const [name, info] of called) {
|
|
127
|
-
if (!defined.has(name)) findings.push('line ' + info.line + ': called but never defined: ' + name + '() (' + info.n + ' call site(s))')
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ---- 2. params keys ----------------------------------------------------
|
|
131
|
-
const dpBlock = /const DEFAULT_PARAMS = \{([\s\S]*?)\n \}/.exec(src)
|
|
132
|
-
if (!dpBlock) findings.push('could not locate DEFAULT_PARAMS')
|
|
133
|
-
else {
|
|
134
|
-
const declared = new Set()
|
|
135
|
-
for (const m of dpBlock[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)) declared.add(m[1])
|
|
136
|
-
const used = new Map()
|
|
137
|
-
for (const m of src.matchAll(/\bparams\.([A-Za-z_$][\w$]*)/g)) used.set(m[1], (used.get(m[1]) || 0) + 1)
|
|
138
|
-
for (const [k, n] of used) {
|
|
139
|
-
if (!declared.has(k)) findings.push('params.' + k + ' read but not declared in DEFAULT_PARAMS (' + n + ' use(s))')
|
|
140
|
-
}
|
|
141
|
-
notes.push('DEFAULT_PARAMS keys: ' + declared.size + '; params.* reads: ' + used.size)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ---- 3. session-API surface used by tool handlers -----------------------
|
|
145
|
-
// The returned API object is the last `return { ... }` inside makeSession.
|
|
146
|
-
const apiStart = src.lastIndexOf(' return {\n sessionId,')
|
|
147
|
-
if (apiStart === -1) findings.push('could not locate the session API return object')
|
|
148
|
-
else {
|
|
149
|
-
const apiEnd = src.indexOf('\n }\n }', apiStart)
|
|
150
|
-
const apiText = src.slice(apiStart, apiEnd === -1 ? apiStart + 4000 : apiEnd)
|
|
151
|
-
const apiKeys = new Set()
|
|
152
|
-
for (const m of apiText.matchAll(/(?:^|[\s{,])([A-Za-z_$][\w$]*)\s*:/g)) apiKeys.add(m[1])
|
|
153
|
-
for (const m of apiText.matchAll(/(?:^|[\s{,])([A-Za-z_$][\w$]*)\s*,/g)) apiKeys.add(m[1])
|
|
154
|
-
const usedOnS = new Map()
|
|
155
|
-
for (const m of src.matchAll(/(?<![\w$.])s\.([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
156
|
-
if (!usedOnS.has(m[1])) usedOnS.set(m[1], lineOf(m.index))
|
|
157
|
-
}
|
|
158
|
-
for (const [k, line] of usedOnS) {
|
|
159
|
-
if (!apiKeys.has(k)) findings.push('line ' + line + ': tool handler calls s.' + k + '() but the session API does not export it')
|
|
160
|
-
}
|
|
161
|
-
notes.push('session API keys: ' + apiKeys.size + '; s.*() called: ' + usedOnS.size)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ---- 4. error codes ----------------------------------------------------
|
|
165
|
-
// Scan the RAW source: these patterns live inside string literals, which `src` strips.
|
|
166
|
-
const raised = new Set()
|
|
167
|
-
for (const m of raw.matchAll(/v5err\(\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
168
|
-
for (const m of raw.matchAll(/code:\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
169
|
-
const docs = readFileSync(new URL('
|
|
170
|
-
const documented = new Set()
|
|
171
|
-
for (const m of docs.matchAll(/`(V5_[A-Z_]+)`/g)) documented.add(m[1])
|
|
172
|
-
const onlyDocs = new Set()
|
|
173
|
-
for (const m of docs.matchAll(/(V5_[A-Z_]+)/g)) onlyDocs.add(m[1])
|
|
174
|
-
const raisedNotDocumented = [...raised].filter(c => !onlyDocs.has(c))
|
|
175
|
-
const documentedNotRaised = [...documented].filter(c => !raised.has(c))
|
|
176
|
-
if (raisedNotDocumented.length) findings.push('error codes raised but absent from 实现方案.md: ' + raisedNotDocumented.join(', '))
|
|
177
|
-
if (documentedNotRaised.length) notes.push('documented but not raised in code (ok if advisory): ' + documentedNotRaised.join(', '))
|
|
178
|
-
notes.push('error codes raised: ' + raised.size)
|
|
179
|
-
|
|
180
|
-
// ---- 5. leftover scaffolding -------------------------------------------
|
|
181
|
-
for (const bad of ['@@V5_SECTION@@', 'TODO', 'FIXME', 'XXX', 'PLACEHOLDER']) {
|
|
182
|
-
if (src.includes(bad)) findings.push('leftover development marker: ' + bad)
|
|
183
|
-
}
|
|
184
|
-
// every event type declared in EV must be appended somewhere
|
|
185
|
-
const evBlock = /const EV = \{([\s\S]*?)\n\}/.exec(raw)
|
|
186
|
-
if (evBlock) {
|
|
187
|
-
const keys = [...evBlock[1].matchAll(/([A-Za-z]+):\s*'(vibe5\/[a-z]+)'/g)]
|
|
188
|
-
for (const [, prop, literal] of keys) {
|
|
189
|
-
const uses = (raw.match(new RegExp("EV\\." + prop + "\\b", 'g')) || []).length
|
|
190
|
-
if (uses <= 1) findings.push('event type EV.' + prop + ' (' + literal + ') is declared but never committed')
|
|
191
|
-
}
|
|
192
|
-
notes.push('event types declared: ' + keys.length)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ---- 6. composition sanity --------------------------------------------
|
|
196
|
-
// The v5 preset is a composition, and a typo in a row `name` is a mounting failure that
|
|
197
|
-
// no unit test of the plugin can catch (the mock calls apply() directly and never goes
|
|
198
|
-
// through the loader). v4 is a proven-good composition mounted on the same hosts, so any
|
|
199
|
-
// package row v5 names that v4 does not is either a genuine new dependency or a typo —
|
|
200
|
-
// and a new dependency must be justified, so surface it for review.
|
|
201
|
-
function packageRows(yaml) {
|
|
202
|
-
const rows = []
|
|
203
|
-
const lines = yaml.split('\n')
|
|
204
|
-
for (let i = 0; i < lines.length; i++) {
|
|
205
|
-
const idm = /^\s*-\s*id:\s*(\S+)\s*$/.exec(lines[i])
|
|
206
|
-
if (!idm) continue
|
|
207
|
-
const row = { id: idm[1], name: '', disabled: false, line: i + 1 }
|
|
208
|
-
for (let j = i + 1; j < lines.length && j < i + 12; j++) {
|
|
209
|
-
if (/^\s*-\s*id:\s*\S+\s*$/.test(lines[j])) break
|
|
210
|
-
const nm = /^\s*name:\s*'?([^'\n]+?)'?\s*$/.exec(lines[j])
|
|
211
|
-
if (nm && !row.name) row.name = nm[1].trim()
|
|
212
|
-
if (/^\s*disabled:\s*true/.test(lines[j])) row.disabled = true
|
|
213
|
-
}
|
|
214
|
-
if (row.name) rows.push(row)
|
|
215
|
-
}
|
|
216
|
-
return rows
|
|
217
|
-
}
|
|
218
|
-
const v5yaml = readFileSync(new URL('
|
|
219
|
-
const v4yaml = readFileSync(new URL('
|
|
220
|
-
const v5rows = packageRows(v5yaml)
|
|
221
|
-
const v4names = new Set(packageRows(v4yaml).map(r => r.name))
|
|
222
|
-
const RELATIVE_OK = new Set(['./vibe-math-v5.js'])
|
|
223
|
-
if (!v5rows.length) findings.push('composition: no rows parsed out of agent.cordis.yml (is the file still YAML?)')
|
|
224
|
-
for (const r of v5rows) {
|
|
225
|
-
if (r.name === 'cordis:group') continue
|
|
226
|
-
if (r.name.startsWith('./')) {
|
|
227
|
-
if (!RELATIVE_OK.has(r.name)) findings.push('composition line ' + r.line + ': relative row name "' + r.name + '" has no matching file in the preset directory')
|
|
228
|
-
continue
|
|
229
|
-
}
|
|
230
|
-
if (!v4names.has(r.name)) findings.push('composition line ' + r.line + ': row "' + r.id + '" names "' + r.name + '", which v4 does not — verify the package/name is correct')
|
|
231
|
-
}
|
|
232
|
-
// the preset must reference its own plugin row, and that file must exist
|
|
233
|
-
if (!v5rows.some(r => r.name === './vibe-math-v5.js')) findings.push('composition: the preset never mounts ./vibe-math-v5.js')
|
|
234
|
-
// prefix AND text are required on the persona row for the DSH schema and for back-compat
|
|
235
|
-
const personaBlock = /- id:\s*persona[\s\S]*?(?=\n-\s*id:)/.exec(v5yaml)
|
|
236
|
-
if (!personaBlock) findings.push('composition: no persona row found')
|
|
237
|
-
else {
|
|
238
|
-
if (!/^\s*prefix:\s*\|/m.test(personaBlock[0])) findings.push('composition: persona row lacks the required `prefix` key')
|
|
239
|
-
if (!/^\s*text:\s*\|/m.test(personaBlock[0])) findings.push('composition: persona row lacks the legacy `text` key')
|
|
240
|
-
}
|
|
241
|
-
notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5rows.filter(r => r.name !== 'cordis:group' && !r.name.startsWith('./') && !v4names.has(r.name)).length)
|
|
242
|
-
|
|
243
|
-
// ---- 7. requirements traceability against the plan ---------------------
|
|
244
|
-
// "No missing logic" is only checkable mechanically if the SPEC is machine-readable.
|
|
245
|
-
// The plan's §15 names every tool and §16 every parameter, so compare those sets
|
|
246
|
-
// against the implementation: a name in the plan but not the code is an unimplemented
|
|
247
|
-
// requirement, and a name in the code but not the plan is undocumented surface.
|
|
248
|
-
{
|
|
249
|
-
const plan = readFileSync(new URL('
|
|
250
|
-
const planTools = new Set()
|
|
251
|
-
for (const m of plan.matchAll(/\bvibe_v5_[a-z_]+/g)) planTools.add(m[0])
|
|
252
|
-
const codeTools = new Set()
|
|
253
|
-
for (const m of raw.matchAll(/registerTool\(\s*'(vibe_v5_[a-z_]+)'/g)) codeTools.add(m[1])
|
|
254
|
-
// documented-but-wildcarded placeholders are not real tools
|
|
255
|
-
const IGNORE = new Set(['vibe_v5_', 'vibe_v5_record_', 'vibe_v5_lean_', 'vibe_v5_task_'])
|
|
256
|
-
for (const t of planTools) {
|
|
257
|
-
if (IGNORE.has(t)) continue
|
|
258
|
-
if (!codeTools.has(t)) findings.push('plan names tool ' + t + ' but the plugin never registers it')
|
|
259
|
-
}
|
|
260
|
-
const undocumented = [...codeTools].filter(t => !planTools.has(t))
|
|
261
|
-
if (undocumented.length) notes.push('tools registered but not named in the plan: ' + undocumented.join(', '))
|
|
262
|
-
notes.push('plan tools: ' + planTools.size + '; registered tools: ' + codeTools.size)
|
|
263
|
-
|
|
264
|
-
// Parameters: only the §16 default table, NOT the §15 tool tables (whose rows also
|
|
265
|
-
// begin with a backticked name).
|
|
266
|
-
const declared2 = new Set()
|
|
267
|
-
if (dpBlock) for (const m of dpBlock[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)) declared2.add(m[1])
|
|
268
|
-
const sec16 = /##\s*16\.[\s\S]*?(?=\n##\s*17\.)/.exec(plan)
|
|
269
|
-
const paramRows = new Set()
|
|
270
|
-
if (sec16) {
|
|
271
|
-
for (const m of sec16[0].matchAll(/^\|\s*`([A-Za-z][\w]*)`(?:\s*\/\s*`([A-Za-z][\w]*)`)?\s*\|/gm)) {
|
|
272
|
-
paramRows.add(m[1])
|
|
273
|
-
if (m[2]) paramRows.add(m[2])
|
|
274
|
-
}
|
|
275
|
-
} else findings.push('could not locate the plan\'s §16 parameter table')
|
|
276
|
-
for (const p of paramRows) {
|
|
277
|
-
if (!declared2.has(p)) findings.push('plan documents parameter `' + p + '` but DEFAULT_PARAMS does not declare it')
|
|
278
|
-
}
|
|
279
|
-
notes.push('plan §16 parameter rows: ' + paramRows.size)
|
|
280
|
-
|
|
281
|
-
// The plan's philosophy is enforced by concrete gates; assert the load-bearing ones
|
|
282
|
-
// still exist so a future edit cannot quietly drop a guard.
|
|
283
|
-
const GATES = [
|
|
284
|
-
['temp workers cannot vote', "if (m.kind === 'temp') return // no vote"],
|
|
285
|
-
['temp workers cannot hire', "if (caller.kind === 'temp') return { ok: false, code: 'V5_NOT_VOTER'"],
|
|
286
|
-
['only the academician assigns', "code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks'"],
|
|
287
|
-
['only the academician sets priorities', "code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can set priorities'"],
|
|
288
|
-
['permanent-staff changes need the office', "code: 'V5_NOT_ACADEMICIAN', message: '解聘常驻研究员只能向所办提议,由所办批准(成员不能直接执行)'"],
|
|
289
|
-
['assignment objections are broadcast', 'if (p.reject_assign && typeof p.reject_assign === \'object\' && params.memberMayRejectAssign)'],
|
|
290
|
-
['the academician has no extra vote weight', 'const E = voters().map((m) => m.id)'],
|
|
291
|
-
['members may reject an assignment', "memberMayRejectAssign: true,"],
|
|
292
|
-
['the charter states the progress definition', "' · Progress/<你>/progress.md —— **你的研究日志**(叙述体,可追加)。'"],
|
|
293
|
-
['the charter describes the academician as organizer', "' 【四、你的组织职责与边界(院士)】'"],
|
|
294
|
-
['the framework never assigns on its own', "agenda: '本所较长时间没有新进展。请你们自行讨论:现在最该推进的是什么?谁来做?是否需要发起验证?'"],
|
|
295
|
-
|
|
296
|
-
// ── PROMPT / INTERACTION CORRECTNESS GATES ─────────────────────────────
|
|
297
|
-
// The 2026-09 field test shipped a framework whose every member brief named the WRONG
|
|
298
|
-
// member. These gates keep the structural fixes in place, and the companion
|
|
299
|
-
// prompt-v5-integrity.test.mjs asserts the TEXT those fixes produce.
|
|
300
|
-
['the status block takes the member it describes', 'function briefBlock(member) {'],
|
|
301
|
-
['an unknown identity fails loudly instead of being guessed', "throw v5err('V5_INTERNAL', 'briefBlock: a member is required"],
|
|
302
|
-
['the status block is built from that member, never a global', 'function stateBlock(member) {\n return briefBlock(member)\n }'],
|
|
303
|
-
['the joiner is committed to the roster BEFORE its brief is built', "member.phase = 'active'\n member.childId = ''\n await putMember(member)"],
|
|
304
|
-
['the charter is frozen at hire and reused on resume', 'const persona = member.persona || memberPersona(member)'],
|
|
305
|
-
['a rebuilt session is framed as a rebuild', "const resume = mode === 'resume'"],
|
|
306
|
-
['leadership text follows the live roster', 'function academicianId() {'],
|
|
307
|
-
['no charter invents a leader when there is none', "本所当前**没有在册院士**"],
|
|
308
|
-
['the office resolves as the office, never as a guessed member', "try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }"],
|
|
309
|
-
['the mailbox is acked BEFORE the round prompt is built', 'if (pending.length) await ackPending(pending)\n const base = typeof baseFn === \'function\' ? baseFn() : baseFn'],
|
|
310
|
-
['framework feedback has its own sender (never a self-message)', "return await say('framework', { to: memberId, kind: 'notice', text: String(text) })"],
|
|
311
|
-
['an assignment is framed by its true origin', "if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text"],
|
|
312
|
-
['a nudge is framed as supervision, not as an assignment', "to, kind: 'nudge',"],
|
|
313
|
-
['a voters-only broadcast is framed as such', "if (m.kind === 'voters') return '【研究所·致全体表决者 from ' + m.from + '】' + m.text"],
|
|
314
|
-
['relayed messages carry their true sender', "await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })"],
|
|
315
|
-
['a member that failed to provision stays visible', "b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))"],
|
|
316
|
-
['the objection channel is documented in the reply spec', '"reject_assign": {"task_id":"t-3"'],
|
|
317
|
-
['the task-done channel is documented in the reply spec', '"task_done": "t-3",'],
|
|
318
|
-
['the meeting input channel is documented in the reply spec', '"input": "本轮会议/辩论的发言正文'],
|
|
319
|
-
['the work push is paced, not an unbounded loop', 'if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue'],
|
|
320
|
-
|
|
321
|
-
// ── LEAN FORMAL VERIFICATION GATES (docs/formal-verification.md) ─────────
|
|
322
|
-
['the formal-verification mode defaults to off', "formalVerify: 'off',"],
|
|
323
|
-
['an unknown mode degrades to off, never to a stronger mode', "indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'"],
|
|
324
|
-
['the mode is read at prompt-build time (never frozen into the charter)', 'const formalOn = () => formalMode()'],
|
|
325
|
-
['a passing run — not merely an archived file — is what makes an object passed', 'const formalGateOk = (rec) => !!rec && (rec.status === \'passed\' || rec.status === \'blocked\')'],
|
|
326
|
-
['a passing Lean run switches the review subject to fidelity', '你的任务是**忠实性审查**'],
|
|
327
|
-
['the voting prompt tells voters not to re-derive once a proof exists', '**你不需要重新检查推导**'],
|
|
328
|
-
['require mode withholds a verdict without a formal record', 'if (formalMode() === \'require\' && !formalGateOk(rec)) {'],
|
|
329
|
-
['a withheld verdict is recorded as undecided with a machine-readable reason', 'formal-required:尚未取得 Lean 形式化通过'],
|
|
330
|
-
['the withheld object goes on a formalization TODO', "'Formal/TODO.md'"],
|
|
331
|
-
['a blocker record must carry a reason', "if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)"],
|
|
332
|
-
['the proof of an object is archived under Verified/Lean/', "await writeTextRel('Verified/Lean/' + target + '.lean', body)"],
|
|
333
|
-
['reusable definitions live in the GLOBAL cross-project library', 'const okWrite = await writeTextAbs(instRootless(rel), body)'],
|
|
334
|
-
['the Lean path guard normalises .. (no string-prefix traversal hole)', 'const norm = normalizeAbsPath(abs)'],
|
|
335
|
-
['a missing toolchain degrades to a readable result, not a crash', "code: 'LEAN_NOT_FOUND'"],
|
|
336
|
-
['the formal record is durable projection state', "formal: 'vibe5/formal',"],
|
|
337
|
-
['the state block advertises the formalization counts', "b.push('[形式化] ' + (formalMode()"],
|
|
338
|
-
['the reply contract carries the difficulty judgement', "L.push(' \"formal\": {"],
|
|
339
|
-
]
|
|
340
|
-
for (const [label, needle] of GATES) {
|
|
341
|
-
if (!raw.includes(needle)) findings.push('philosophy gate missing from the implementation: ' + label)
|
|
342
|
-
}
|
|
343
|
-
notes.push('philosophy gates checked: ' + GATES.length)
|
|
344
|
-
|
|
345
|
-
// The prompt corpus is a SHIPPED deliverable, not a build artifact: a human must be able
|
|
346
|
-
// to read the exact text every member receives without decoding session logs.
|
|
347
|
-
const REPO_ROOT = new URL('
|
|
348
|
-
const needFiles = [
|
|
349
|
-
['the prompt-integrity suite is shipped', 'prompt-v5-integrity.test.mjs'],
|
|
350
|
-
['the machine-readable prompt corpus is shipped', 'prompt-corpus-v5/prompt-corpus-v5.json'],
|
|
351
|
-
['the human-readable prompt corpus is shipped', 'prompt-corpus-v5/prompt-corpus-v5.md'],
|
|
352
|
-
['the persona-surface suite is shipped', 'audit-persona-surface.test.mjs'],
|
|
353
|
-
['the four-preset persona corpus is shipped (machine-readable)', 'prompt-corpus-persona/persona-corpus.json'],
|
|
354
|
-
['the four-preset persona corpus is shipped (human-readable)', 'prompt-corpus-persona/persona-corpus.md'],
|
|
355
|
-
]
|
|
356
|
-
for (const [label, rel] of needFiles) {
|
|
357
|
-
let ok = false
|
|
358
|
-
try { ok = existsSync(new URL(rel, REPO_ROOT)) } catch (e) { ok = false }
|
|
359
|
-
if (!ok) findings.push('missing shipped prompt-correctness artifact: ' + label + ' (' + rel + ')')
|
|
360
|
-
}
|
|
361
|
-
notes.push('prompt-correctness artifacts checked: ' + needFiles.length)
|
|
362
|
-
|
|
363
|
-
// The corpus must actually contain the interactions a reviewer needs to see, and must
|
|
364
|
-
// never contain a wrong-identity brief.
|
|
365
|
-
try {
|
|
366
|
-
const corpusPath = new URL('prompt-corpus-v5/prompt-corpus-v5.json', REPO_ROOT)
|
|
367
|
-
const c = JSON.parse(readFileSync(corpusPath, 'utf8'))
|
|
368
|
-
const kinds = new Set((c.prompts || []).map(p => p.kind))
|
|
369
|
-
const need = ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
|
|
370
|
-
'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
|
|
371
|
-
'inbox-office', 'inbox-assign', 'inbox-nudge', 'inbox-office-assign', 'inbox-office-nudge',
|
|
372
|
-
'notice', 'notice-claim', 'after-failure',
|
|
373
|
-
// the Lean formal-verification interaction must be reviewable by a human too — including
|
|
374
|
-
// the `require` gate wording and the state a member sees AFTER a fidelity defect withdrew
|
|
375
|
-
// a proof (both were missing from the first corpus, so nobody could read them).
|
|
376
|
-
'lean-work', 'lean-verify', 'lean-fidelity', 'lean-require', 'lean-after-defect']
|
|
377
|
-
for (const k of need) if (!kinds.has(k)) findings.push('the prompt corpus is missing a ' + k + ' prompt')
|
|
378
|
-
const all = (c.prompts || []).map(p => p.prompt + '\n' + (p.charter || '')).join('\n')
|
|
379
|
-
if (/你是 \?/.test(all)) findings.push('the prompt corpus contains a wrong-identity "你是 ?" brief')
|
|
380
|
-
if (/\[状态\][^\n]*你是\s+(\S+?)[^\n]*\n/.test(all)) {
|
|
381
|
-
// every [状态] line must name a real member id, never a placeholder
|
|
382
|
-
for (const m of all.match(/\[状态\][^\n]*/g) || []) {
|
|
383
|
-
const id = (/\[状态\]\s*你是\s+(\S+?)(/.exec(m) || [])[1]
|
|
384
|
-
if (!id || id === '?' || id === 'undefined') findings.push('the prompt corpus has a bad identity line: ' + m.slice(0, 60))
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
notes.push('prompt corpus: ' + (c.total || 0) + ' prompts, ' + kinds.size + ' kinds')
|
|
388
|
-
} catch (e) {
|
|
389
|
-
findings.push('the prompt corpus could not be read/parsed: ' + String((e && e.message) || e))
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// The persona surface of ALL FOUR presets is a shipped prompt-correctness artifact too:
|
|
393
|
-
// it is the only prompt the MAIN agent receives, and no e2e suite loads the YAML, so a
|
|
394
|
-
// human reviewer needs the corpus on disk (generated by audit-persona-surface.test.mjs).
|
|
395
|
-
try {
|
|
396
|
-
const pc = JSON.parse(readFileSync(new URL('prompt-corpus-persona/persona-corpus.json', REPO_ROOT), 'utf8'))
|
|
397
|
-
for (const d of ['vibe-math-v2', 'vibe-math-v3', 'vibe-math-v4', 'vibe-math-v5']) {
|
|
398
|
-
if (!(pc.presets || []).some((p) => p.preset === d)) findings.push('the persona corpus is missing preset ' + d)
|
|
399
|
-
}
|
|
400
|
-
const emptyBlocks = (pc.presets || []).filter((p) => !p.prefix || !p.text).map((p) => p.preset)
|
|
401
|
-
if (emptyBlocks.length) findings.push('the persona corpus has an empty persona block: ' + emptyBlocks.join(', '))
|
|
402
|
-
notes.push('persona corpus: ' + (pc.presets || []).length + ' presets')
|
|
403
|
-
} catch (e) {
|
|
404
|
-
findings.push('the persona corpus could not be read/parsed: ' + String((e && e.message) || e))
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// ---- scanner self-check -------------------------------------------------
|
|
409
|
-
// The whole audit rests on stripNoise(); a scanner that mis-lexes silently BLINDS it (that is how the
|
|
410
|
-
// regex-with-a-quote bug lived here). Two checks, both cheap and both measured to be sensitive:
|
|
411
|
-
// · the stripped source must still PARSE (the pre-fix scanner produced a SyntaxError);
|
|
412
|
-
// · a fixture with a quoted character class + a following comment must survive intact.
|
|
413
|
-
{
|
|
414
|
-
const tmp = mkdtempSync(join(tmpdir(), 'v5-integrity-strip-'))
|
|
415
|
-
try {
|
|
416
|
-
const f = join(tmp, 'v5.mjs')
|
|
417
|
-
writeFileSync(f, src)
|
|
418
|
-
const r = spawnSync(process.execPath, ['--check', f], { encoding: 'utf8' })
|
|
419
|
-
if (r.status !== 0) {
|
|
420
|
-
findings.push('stripNoise() corrupted the source it scans (the stripped text does not parse) — every identifier check below is unreliable: ' +
|
|
421
|
-
String(r.stderr || '').split('\n').filter((l) => l.trim()).slice(-2).join(' ').slice(0, 160))
|
|
422
|
-
}
|
|
423
|
-
const fixture = [
|
|
424
|
-
'function f(s) { return s.replace(/["\']/g, "-") }',
|
|
425
|
-
'// phantom_call( must not survive as a call site',
|
|
426
|
-
'const div = 6 / 2',
|
|
427
|
-
].join('\n')
|
|
428
|
-
const stripped = stripNoise(fixture)
|
|
429
|
-
if (stripped.includes('phantom_call')) findings.push('stripNoise() left a comment in the code stream (a phantom call site would be reported)')
|
|
430
|
-
if (!/'/.test(stripped)) findings.push('stripNoise() dropped a value placeholder')
|
|
431
|
-
notes.push('scanner self-check: stripped output parses=' + (r.status === 0) + '; quoted-class fixture=' + (!stripped.includes('phantom_call')))
|
|
432
|
-
} finally {
|
|
433
|
-
rmSync(tmp, { recursive: true, force: true })
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ---- report ------------------------------------------------------------
|
|
438
|
-
console.log('-- V5 integrity audit --')
|
|
439
|
-
for (const n of notes) console.log(' note: ' + n)
|
|
440
|
-
console.log('')
|
|
441
|
-
if (findings.length) {
|
|
442
|
-
for (const f of findings) console.error(' FINDING: ' + f)
|
|
443
|
-
console.error('')
|
|
444
|
-
console.error(findings.length + ' finding(s)')
|
|
445
|
-
process.exit(1)
|
|
446
|
-
}
|
|
447
|
-
console.log('clean: no undefined calls, no undeclared params keys, no missing session API, no leftover markers')
|
|
448
|
-
process.exit(0)
|
|
1
|
+
// ============================================================
|
|
2
|
+
// V5 INTEGRITY AUDIT — static self-check of vibe-math-v5.js against the ways this
|
|
3
|
+
// kind of single-file plugin actually breaks:
|
|
4
|
+
// 1. a function CALLED but never defined (ReferenceError at runtime only)
|
|
5
|
+
// 2. a `params.X` read for a key that DEFAULT_PARAMS never declares (silent undefined)
|
|
6
|
+
// 3. a tool handler calling `s.NAME(...)` on the session API object that the session
|
|
7
|
+
// never returns (TypeError only when that tool is used)
|
|
8
|
+
// 4. documented error codes that are never raised, and raised codes never documented
|
|
9
|
+
// 5. leftover development markers / TODO scaffolding
|
|
10
|
+
// Run: node tests/audit-v5-integrity.mjs (exit 1 on any finding)
|
|
11
|
+
// ============================================================
|
|
12
|
+
import { readFileSync, existsSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
13
|
+
import { spawnSync } from 'node:child_process'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
|
|
17
|
+
const FILE = new URL('../vibe-math-v5/vibe-math-v5.js', import.meta.url)
|
|
18
|
+
const raw = readFileSync(FILE, 'utf8')
|
|
19
|
+
// Strip comments, string literals AND regex literals before any identifier scan. Without this the
|
|
20
|
+
// heuristic matches English words inside comments that merely precede a '(' (e.g.
|
|
21
|
+
// "// per unit (" becomes a phantom call to unit()), drowning the real findings.
|
|
22
|
+
//
|
|
23
|
+
// REGEX LITERALS ARE NOT OPTIONAL HERE: v5 contains `/[\\/:*?"<>|\u0000-\u001f]+/` — a character class
|
|
24
|
+
// holding a DOUBLE QUOTE. A scanner without regex support reads that quote as a string start and
|
|
25
|
+
// mangles the rest of the line. Measured on this very file (old vs new, line by line): 30 of 4365
|
|
26
|
+
// lines differed and the stripped output did NOT parse. No identifier this audit checks
|
|
27
|
+
// (call names, `params.*` reads, `s.*()` methods) happened to be missed in the current source, so this
|
|
28
|
+
// was latent rather than exploited — but a single call appearing only on such a line would have been
|
|
29
|
+
// invisible. The self-check at the bottom of this file keeps the scanner honest.
|
|
30
|
+
// The keyword rule matters for the same reason as in audit-prompt-invariants.mjs (`return /…/`).
|
|
31
|
+
function stripNoise(s) {
|
|
32
|
+
let out = ''
|
|
33
|
+
let i = 0
|
|
34
|
+
const n = s.length
|
|
35
|
+
let prev = '' // last significant token (single char, or a whole word such as `return`)
|
|
36
|
+
let word = ''
|
|
37
|
+
const REGEX_AFTER_KEYWORD = /^(?:return|typeof|case|delete|void|instanceof|in|of|yield|await|new|do|else)$/
|
|
38
|
+
const regexAllowed = () => prev === '' || REGEX_AFTER_KEYWORD.test(prev) || /[(,=:[!&|?{};+\-*%~^<>]/.test(prev)
|
|
39
|
+
while (i < n) {
|
|
40
|
+
const c = s[i], c2 = s[i + 1]
|
|
41
|
+
if (c === '/' && c2 === '/') { while (i < n && s[i] !== '\n') i++; continue }
|
|
42
|
+
if (c === '/' && c2 === '*') { i += 2; while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++; i += 2; continue }
|
|
43
|
+
if (c === '/' && regexAllowed()) {
|
|
44
|
+
i++
|
|
45
|
+
let inClass = false
|
|
46
|
+
while (i < n) {
|
|
47
|
+
if (s[i] === '\\') { i += 2; continue }
|
|
48
|
+
if (s[i] === '[') inClass = true
|
|
49
|
+
else if (s[i] === ']') inClass = false
|
|
50
|
+
else if (s[i] === '/' && !inClass) { i++; break }
|
|
51
|
+
else if (s[i] === '\n') break // a regex literal cannot span lines
|
|
52
|
+
i++
|
|
53
|
+
}
|
|
54
|
+
while (i < n && /[a-z]/i.test(s[i])) i++ // flags
|
|
55
|
+
out += "''" // a value placeholder, like strings: keeps `X: <value>` shapes but no phantom calls
|
|
56
|
+
prev = ')'
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
60
|
+
const q = c
|
|
61
|
+
i++
|
|
62
|
+
while (i < n) {
|
|
63
|
+
if (s[i] === '\\') { i += 2; continue }
|
|
64
|
+
if (s[i] === q) { i++; break }
|
|
65
|
+
if (q !== '`' && s[i] === '\n') break
|
|
66
|
+
i++
|
|
67
|
+
}
|
|
68
|
+
out += q + q // keep a placeholder so `X: ''` shape survives
|
|
69
|
+
prev = ')'
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
out += c
|
|
73
|
+
if (/[A-Za-z0-9_$]/.test(c)) word += c
|
|
74
|
+
else { if (word) { prev = word; word = '' } if (!/\s/.test(c)) prev = c }
|
|
75
|
+
i++
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
const src = stripNoise(raw)
|
|
80
|
+
const findings = []
|
|
81
|
+
const notes = []
|
|
82
|
+
|
|
83
|
+
const lineOf = (idx) => src.slice(0, idx).split('\n').length
|
|
84
|
+
|
|
85
|
+
// ---- 1. called-but-undefined -------------------------------------------
|
|
86
|
+
const defined = new Set()
|
|
87
|
+
for (const m of src.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)) defined.add(m[1])
|
|
88
|
+
for (const m of src.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g)) defined.add(m[1])
|
|
89
|
+
for (const m of src.matchAll(/(?:const|let|var)\s+\{([^}]+)\}\s*=/g)) {
|
|
90
|
+
for (const part of m[1].split(',')) { const n = part.split(':').pop().trim(); if (n) defined.add(n) }
|
|
91
|
+
}
|
|
92
|
+
for (const m of src.matchAll(/catch\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) defined.add(m[1])
|
|
93
|
+
// Function/method PARAMETERS are locally bound identifiers too, not free calls.
|
|
94
|
+
for (const m of src.matchAll(/function\s*[A-Za-z_$]*\s*\(([^)]*)\)/g)) {
|
|
95
|
+
for (const raw of m[1].split(',')) {
|
|
96
|
+
const name = raw.trim().replace(/[={].*$/s, '').trim()
|
|
97
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) defined.add(name)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const m of src.matchAll(/\(([^()]*)\)\s*=>/g)) {
|
|
101
|
+
for (const raw of m[1].split(',')) {
|
|
102
|
+
const name = raw.trim().replace(/[={].*$/s, '').trim()
|
|
103
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) defined.add(name)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const m of src.matchAll(/([A-Za-z_$][\w$]*)\s*=>/g)) defined.add(m[1])
|
|
107
|
+
// JS/DSH globals that are legitimately free
|
|
108
|
+
const GLOBALS = new Set([
|
|
109
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'function', 'new', 'await', 'delete', 'void', 'do', 'else', 'try',
|
|
110
|
+
'Array', 'Object', 'JSON', 'Number', 'String', 'Boolean', 'Math', 'Date', 'Promise', 'Set', 'Map', 'WeakRef', 'WeakMap', 'Symbol', 'Error',
|
|
111
|
+
'isFinite', 'isNaN', 'parseInt', 'parseFloat', 'encodeURIComponent', 'decodeURIComponent', 'structuredClone',
|
|
112
|
+
'AbortSignal', 'console', 'require', 'import', 'super', 'this', 'of', 'in', 'instanceof',
|
|
113
|
+
'RegExp', 'Proxy', 'Reflect', 'AggregateError', 'TextEncoder', 'TextDecoder', 'URL', 'BigInt', 'Intl', 'Buffer', 'process',
|
|
114
|
+
])
|
|
115
|
+
const LOCAL_METHODS = new Set()
|
|
116
|
+
for (const m of src.matchAll(/[{,]\s*([A-Za-z_$][\w$]*)\s*[:(]/g)) LOCAL_METHODS.add(m[1])
|
|
117
|
+
for (const m of src.matchAll(/\.\s*([A-Za-z_$][\w$]*)\s*\(/g)) LOCAL_METHODS.add(m[1])
|
|
118
|
+
|
|
119
|
+
const called = new Map()
|
|
120
|
+
for (const m of src.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
121
|
+
const name = m[1]
|
|
122
|
+
if (GLOBALS.has(name) || LOCAL_METHODS.has(name)) continue
|
|
123
|
+
if (!called.has(name)) called.set(name, { n: 0, line: lineOf(m.index) })
|
|
124
|
+
called.get(name).n++
|
|
125
|
+
}
|
|
126
|
+
for (const [name, info] of called) {
|
|
127
|
+
if (!defined.has(name)) findings.push('line ' + info.line + ': called but never defined: ' + name + '() (' + info.n + ' call site(s))')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- 2. params keys ----------------------------------------------------
|
|
131
|
+
const dpBlock = /const DEFAULT_PARAMS = \{([\s\S]*?)\n \}/.exec(src)
|
|
132
|
+
if (!dpBlock) findings.push('could not locate DEFAULT_PARAMS')
|
|
133
|
+
else {
|
|
134
|
+
const declared = new Set()
|
|
135
|
+
for (const m of dpBlock[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)) declared.add(m[1])
|
|
136
|
+
const used = new Map()
|
|
137
|
+
for (const m of src.matchAll(/\bparams\.([A-Za-z_$][\w$]*)/g)) used.set(m[1], (used.get(m[1]) || 0) + 1)
|
|
138
|
+
for (const [k, n] of used) {
|
|
139
|
+
if (!declared.has(k)) findings.push('params.' + k + ' read but not declared in DEFAULT_PARAMS (' + n + ' use(s))')
|
|
140
|
+
}
|
|
141
|
+
notes.push('DEFAULT_PARAMS keys: ' + declared.size + '; params.* reads: ' + used.size)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- 3. session-API surface used by tool handlers -----------------------
|
|
145
|
+
// The returned API object is the last `return { ... }` inside makeSession.
|
|
146
|
+
const apiStart = src.lastIndexOf(' return {\n sessionId,')
|
|
147
|
+
if (apiStart === -1) findings.push('could not locate the session API return object')
|
|
148
|
+
else {
|
|
149
|
+
const apiEnd = src.indexOf('\n }\n }', apiStart)
|
|
150
|
+
const apiText = src.slice(apiStart, apiEnd === -1 ? apiStart + 4000 : apiEnd)
|
|
151
|
+
const apiKeys = new Set()
|
|
152
|
+
for (const m of apiText.matchAll(/(?:^|[\s{,])([A-Za-z_$][\w$]*)\s*:/g)) apiKeys.add(m[1])
|
|
153
|
+
for (const m of apiText.matchAll(/(?:^|[\s{,])([A-Za-z_$][\w$]*)\s*,/g)) apiKeys.add(m[1])
|
|
154
|
+
const usedOnS = new Map()
|
|
155
|
+
for (const m of src.matchAll(/(?<![\w$.])s\.([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
156
|
+
if (!usedOnS.has(m[1])) usedOnS.set(m[1], lineOf(m.index))
|
|
157
|
+
}
|
|
158
|
+
for (const [k, line] of usedOnS) {
|
|
159
|
+
if (!apiKeys.has(k)) findings.push('line ' + line + ': tool handler calls s.' + k + '() but the session API does not export it')
|
|
160
|
+
}
|
|
161
|
+
notes.push('session API keys: ' + apiKeys.size + '; s.*() called: ' + usedOnS.size)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---- 4. error codes ----------------------------------------------------
|
|
165
|
+
// Scan the RAW source: these patterns live inside string literals, which `src` strips.
|
|
166
|
+
const raised = new Set()
|
|
167
|
+
for (const m of raw.matchAll(/v5err\(\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
168
|
+
for (const m of raw.matchAll(/code:\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
169
|
+
const docs = readFileSync(new URL('../vibe-math-v5/实现方案.md', import.meta.url), 'utf8')
|
|
170
|
+
const documented = new Set()
|
|
171
|
+
for (const m of docs.matchAll(/`(V5_[A-Z_]+)`/g)) documented.add(m[1])
|
|
172
|
+
const onlyDocs = new Set()
|
|
173
|
+
for (const m of docs.matchAll(/(V5_[A-Z_]+)/g)) onlyDocs.add(m[1])
|
|
174
|
+
const raisedNotDocumented = [...raised].filter(c => !onlyDocs.has(c))
|
|
175
|
+
const documentedNotRaised = [...documented].filter(c => !raised.has(c))
|
|
176
|
+
if (raisedNotDocumented.length) findings.push('error codes raised but absent from 实现方案.md: ' + raisedNotDocumented.join(', '))
|
|
177
|
+
if (documentedNotRaised.length) notes.push('documented but not raised in code (ok if advisory): ' + documentedNotRaised.join(', '))
|
|
178
|
+
notes.push('error codes raised: ' + raised.size)
|
|
179
|
+
|
|
180
|
+
// ---- 5. leftover scaffolding -------------------------------------------
|
|
181
|
+
for (const bad of ['@@V5_SECTION@@', 'TODO', 'FIXME', 'XXX', 'PLACEHOLDER']) {
|
|
182
|
+
if (src.includes(bad)) findings.push('leftover development marker: ' + bad)
|
|
183
|
+
}
|
|
184
|
+
// every event type declared in EV must be appended somewhere
|
|
185
|
+
const evBlock = /const EV = \{([\s\S]*?)\n\}/.exec(raw)
|
|
186
|
+
if (evBlock) {
|
|
187
|
+
const keys = [...evBlock[1].matchAll(/([A-Za-z]+):\s*'(vibe5\/[a-z]+)'/g)]
|
|
188
|
+
for (const [, prop, literal] of keys) {
|
|
189
|
+
const uses = (raw.match(new RegExp("EV\\." + prop + "\\b", 'g')) || []).length
|
|
190
|
+
if (uses <= 1) findings.push('event type EV.' + prop + ' (' + literal + ') is declared but never committed')
|
|
191
|
+
}
|
|
192
|
+
notes.push('event types declared: ' + keys.length)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---- 6. composition sanity --------------------------------------------
|
|
196
|
+
// The v5 preset is a composition, and a typo in a row `name` is a mounting failure that
|
|
197
|
+
// no unit test of the plugin can catch (the mock calls apply() directly and never goes
|
|
198
|
+
// through the loader). v4 is a proven-good composition mounted on the same hosts, so any
|
|
199
|
+
// package row v5 names that v4 does not is either a genuine new dependency or a typo —
|
|
200
|
+
// and a new dependency must be justified, so surface it for review.
|
|
201
|
+
function packageRows(yaml) {
|
|
202
|
+
const rows = []
|
|
203
|
+
const lines = yaml.split('\n')
|
|
204
|
+
for (let i = 0; i < lines.length; i++) {
|
|
205
|
+
const idm = /^\s*-\s*id:\s*(\S+)\s*$/.exec(lines[i])
|
|
206
|
+
if (!idm) continue
|
|
207
|
+
const row = { id: idm[1], name: '', disabled: false, line: i + 1 }
|
|
208
|
+
for (let j = i + 1; j < lines.length && j < i + 12; j++) {
|
|
209
|
+
if (/^\s*-\s*id:\s*\S+\s*$/.test(lines[j])) break
|
|
210
|
+
const nm = /^\s*name:\s*'?([^'\n]+?)'?\s*$/.exec(lines[j])
|
|
211
|
+
if (nm && !row.name) row.name = nm[1].trim()
|
|
212
|
+
if (/^\s*disabled:\s*true/.test(lines[j])) row.disabled = true
|
|
213
|
+
}
|
|
214
|
+
if (row.name) rows.push(row)
|
|
215
|
+
}
|
|
216
|
+
return rows
|
|
217
|
+
}
|
|
218
|
+
const v5yaml = readFileSync(new URL('../vibe-math-v5/agent.cordis.yml', import.meta.url), 'utf8')
|
|
219
|
+
const v4yaml = readFileSync(new URL('../vibe-math-v4/agent.cordis.yml', import.meta.url), 'utf8')
|
|
220
|
+
const v5rows = packageRows(v5yaml)
|
|
221
|
+
const v4names = new Set(packageRows(v4yaml).map(r => r.name))
|
|
222
|
+
const RELATIVE_OK = new Set(['./vibe-math-v5.js'])
|
|
223
|
+
if (!v5rows.length) findings.push('composition: no rows parsed out of agent.cordis.yml (is the file still YAML?)')
|
|
224
|
+
for (const r of v5rows) {
|
|
225
|
+
if (r.name === 'cordis:group') continue
|
|
226
|
+
if (r.name.startsWith('./')) {
|
|
227
|
+
if (!RELATIVE_OK.has(r.name)) findings.push('composition line ' + r.line + ': relative row name "' + r.name + '" has no matching file in the preset directory')
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
if (!v4names.has(r.name)) findings.push('composition line ' + r.line + ': row "' + r.id + '" names "' + r.name + '", which v4 does not — verify the package/name is correct')
|
|
231
|
+
}
|
|
232
|
+
// the preset must reference its own plugin row, and that file must exist
|
|
233
|
+
if (!v5rows.some(r => r.name === './vibe-math-v5.js')) findings.push('composition: the preset never mounts ./vibe-math-v5.js')
|
|
234
|
+
// prefix AND text are required on the persona row for the DSH schema and for back-compat
|
|
235
|
+
const personaBlock = /- id:\s*persona[\s\S]*?(?=\n-\s*id:)/.exec(v5yaml)
|
|
236
|
+
if (!personaBlock) findings.push('composition: no persona row found')
|
|
237
|
+
else {
|
|
238
|
+
if (!/^\s*prefix:\s*\|/m.test(personaBlock[0])) findings.push('composition: persona row lacks the required `prefix` key')
|
|
239
|
+
if (!/^\s*text:\s*\|/m.test(personaBlock[0])) findings.push('composition: persona row lacks the legacy `text` key')
|
|
240
|
+
}
|
|
241
|
+
notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5rows.filter(r => r.name !== 'cordis:group' && !r.name.startsWith('./') && !v4names.has(r.name)).length)
|
|
242
|
+
|
|
243
|
+
// ---- 7. requirements traceability against the plan ---------------------
|
|
244
|
+
// "No missing logic" is only checkable mechanically if the SPEC is machine-readable.
|
|
245
|
+
// The plan's §15 names every tool and §16 every parameter, so compare those sets
|
|
246
|
+
// against the implementation: a name in the plan but not the code is an unimplemented
|
|
247
|
+
// requirement, and a name in the code but not the plan is undocumented surface.
|
|
248
|
+
{
|
|
249
|
+
const plan = readFileSync(new URL('../vibe-math-v5/实现方案.md', import.meta.url), 'utf8')
|
|
250
|
+
const planTools = new Set()
|
|
251
|
+
for (const m of plan.matchAll(/\bvibe_v5_[a-z_]+/g)) planTools.add(m[0])
|
|
252
|
+
const codeTools = new Set()
|
|
253
|
+
for (const m of raw.matchAll(/registerTool\(\s*'(vibe_v5_[a-z_]+)'/g)) codeTools.add(m[1])
|
|
254
|
+
// documented-but-wildcarded placeholders are not real tools
|
|
255
|
+
const IGNORE = new Set(['vibe_v5_', 'vibe_v5_record_', 'vibe_v5_lean_', 'vibe_v5_task_'])
|
|
256
|
+
for (const t of planTools) {
|
|
257
|
+
if (IGNORE.has(t)) continue
|
|
258
|
+
if (!codeTools.has(t)) findings.push('plan names tool ' + t + ' but the plugin never registers it')
|
|
259
|
+
}
|
|
260
|
+
const undocumented = [...codeTools].filter(t => !planTools.has(t))
|
|
261
|
+
if (undocumented.length) notes.push('tools registered but not named in the plan: ' + undocumented.join(', '))
|
|
262
|
+
notes.push('plan tools: ' + planTools.size + '; registered tools: ' + codeTools.size)
|
|
263
|
+
|
|
264
|
+
// Parameters: only the §16 default table, NOT the §15 tool tables (whose rows also
|
|
265
|
+
// begin with a backticked name).
|
|
266
|
+
const declared2 = new Set()
|
|
267
|
+
if (dpBlock) for (const m of dpBlock[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)) declared2.add(m[1])
|
|
268
|
+
const sec16 = /##\s*16\.[\s\S]*?(?=\n##\s*17\.)/.exec(plan)
|
|
269
|
+
const paramRows = new Set()
|
|
270
|
+
if (sec16) {
|
|
271
|
+
for (const m of sec16[0].matchAll(/^\|\s*`([A-Za-z][\w]*)`(?:\s*\/\s*`([A-Za-z][\w]*)`)?\s*\|/gm)) {
|
|
272
|
+
paramRows.add(m[1])
|
|
273
|
+
if (m[2]) paramRows.add(m[2])
|
|
274
|
+
}
|
|
275
|
+
} else findings.push('could not locate the plan\'s §16 parameter table')
|
|
276
|
+
for (const p of paramRows) {
|
|
277
|
+
if (!declared2.has(p)) findings.push('plan documents parameter `' + p + '` but DEFAULT_PARAMS does not declare it')
|
|
278
|
+
}
|
|
279
|
+
notes.push('plan §16 parameter rows: ' + paramRows.size)
|
|
280
|
+
|
|
281
|
+
// The plan's philosophy is enforced by concrete gates; assert the load-bearing ones
|
|
282
|
+
// still exist so a future edit cannot quietly drop a guard.
|
|
283
|
+
const GATES = [
|
|
284
|
+
['temp workers cannot vote', "if (m.kind === 'temp') return // no vote"],
|
|
285
|
+
['temp workers cannot hire', "if (caller.kind === 'temp') return { ok: false, code: 'V5_NOT_VOTER'"],
|
|
286
|
+
['only the academician assigns', "code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks'"],
|
|
287
|
+
['only the academician sets priorities', "code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can set priorities'"],
|
|
288
|
+
['permanent-staff changes need the office', "code: 'V5_NOT_ACADEMICIAN', message: '解聘常驻研究员只能向所办提议,由所办批准(成员不能直接执行)'"],
|
|
289
|
+
['assignment objections are broadcast', 'if (p.reject_assign && typeof p.reject_assign === \'object\' && params.memberMayRejectAssign)'],
|
|
290
|
+
['the academician has no extra vote weight', 'const E = voters().map((m) => m.id)'],
|
|
291
|
+
['members may reject an assignment', "memberMayRejectAssign: true,"],
|
|
292
|
+
['the charter states the progress definition', "' · Progress/<你>/progress.md —— **你的研究日志**(叙述体,可追加)。'"],
|
|
293
|
+
['the charter describes the academician as organizer', "' 【四、你的组织职责与边界(院士)】'"],
|
|
294
|
+
['the framework never assigns on its own', "agenda: '本所较长时间没有新进展。请你们自行讨论:现在最该推进的是什么?谁来做?是否需要发起验证?'"],
|
|
295
|
+
|
|
296
|
+
// ── PROMPT / INTERACTION CORRECTNESS GATES ─────────────────────────────
|
|
297
|
+
// The 2026-09 field test shipped a framework whose every member brief named the WRONG
|
|
298
|
+
// member. These gates keep the structural fixes in place, and the companion
|
|
299
|
+
// prompt-v5-integrity.test.mjs asserts the TEXT those fixes produce.
|
|
300
|
+
['the status block takes the member it describes', 'function briefBlock(member) {'],
|
|
301
|
+
['an unknown identity fails loudly instead of being guessed', "throw v5err('V5_INTERNAL', 'briefBlock: a member is required"],
|
|
302
|
+
['the status block is built from that member, never a global', 'function stateBlock(member) {\n return briefBlock(member)\n }'],
|
|
303
|
+
['the joiner is committed to the roster BEFORE its brief is built', "member.phase = 'active'\n member.childId = ''\n await putMember(member)"],
|
|
304
|
+
['the charter is frozen at hire and reused on resume', 'const persona = member.persona || memberPersona(member)'],
|
|
305
|
+
['a rebuilt session is framed as a rebuild', "const resume = mode === 'resume'"],
|
|
306
|
+
['leadership text follows the live roster', 'function academicianId() {'],
|
|
307
|
+
['no charter invents a leader when there is none', "本所当前**没有在册院士**"],
|
|
308
|
+
['the office resolves as the office, never as a guessed member', "try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }"],
|
|
309
|
+
['the mailbox is acked BEFORE the round prompt is built', 'if (pending.length) await ackPending(pending)\n const base = typeof baseFn === \'function\' ? baseFn() : baseFn'],
|
|
310
|
+
['framework feedback has its own sender (never a self-message)', "return await say('framework', { to: memberId, kind: 'notice', text: String(text) })"],
|
|
311
|
+
['an assignment is framed by its true origin', "if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text"],
|
|
312
|
+
['a nudge is framed as supervision, not as an assignment', "to, kind: 'nudge',"],
|
|
313
|
+
['a voters-only broadcast is framed as such', "if (m.kind === 'voters') return '【研究所·致全体表决者 from ' + m.from + '】' + m.text"],
|
|
314
|
+
['relayed messages carry their true sender', "await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })"],
|
|
315
|
+
['a member that failed to provision stays visible', "b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))"],
|
|
316
|
+
['the objection channel is documented in the reply spec', '"reject_assign": {"task_id":"t-3"'],
|
|
317
|
+
['the task-done channel is documented in the reply spec', '"task_done": "t-3",'],
|
|
318
|
+
['the meeting input channel is documented in the reply spec', '"input": "本轮会议/辩论的发言正文'],
|
|
319
|
+
['the work push is paced, not an unbounded loop', 'if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue'],
|
|
320
|
+
|
|
321
|
+
// ── LEAN FORMAL VERIFICATION GATES (docs/formal-verification.md) ─────────
|
|
322
|
+
['the formal-verification mode defaults to off', "formalVerify: 'off',"],
|
|
323
|
+
['an unknown mode degrades to off, never to a stronger mode', "indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'"],
|
|
324
|
+
['the mode is read at prompt-build time (never frozen into the charter)', 'const formalOn = () => formalMode()'],
|
|
325
|
+
['a passing run — not merely an archived file — is what makes an object passed', 'const formalGateOk = (rec) => !!rec && (rec.status === \'passed\' || rec.status === \'blocked\')'],
|
|
326
|
+
['a passing Lean run switches the review subject to fidelity', '你的任务是**忠实性审查**'],
|
|
327
|
+
['the voting prompt tells voters not to re-derive once a proof exists', '**你不需要重新检查推导**'],
|
|
328
|
+
['require mode withholds a verdict without a formal record', 'if (formalMode() === \'require\' && !formalGateOk(rec)) {'],
|
|
329
|
+
['a withheld verdict is recorded as undecided with a machine-readable reason', 'formal-required:尚未取得 Lean 形式化通过'],
|
|
330
|
+
['the withheld object goes on a formalization TODO', "'Formal/TODO.md'"],
|
|
331
|
+
['a blocker record must carry a reason', "if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)"],
|
|
332
|
+
['the proof of an object is archived under Verified/Lean/', "await writeTextRel('Verified/Lean/' + target + '.lean', body)"],
|
|
333
|
+
['reusable definitions live in the GLOBAL cross-project library', 'const okWrite = await writeTextAbs(instRootless(rel), body)'],
|
|
334
|
+
['the Lean path guard normalises .. (no string-prefix traversal hole)', 'const norm = normalizeAbsPath(abs)'],
|
|
335
|
+
['a missing toolchain degrades to a readable result, not a crash', "code: 'LEAN_NOT_FOUND'"],
|
|
336
|
+
['the formal record is durable projection state', "formal: 'vibe5/formal',"],
|
|
337
|
+
['the state block advertises the formalization counts', "b.push('[形式化] ' + (formalMode()"],
|
|
338
|
+
['the reply contract carries the difficulty judgement', "L.push(' \"formal\": {"],
|
|
339
|
+
]
|
|
340
|
+
for (const [label, needle] of GATES) {
|
|
341
|
+
if (!raw.includes(needle)) findings.push('philosophy gate missing from the implementation: ' + label)
|
|
342
|
+
}
|
|
343
|
+
notes.push('philosophy gates checked: ' + GATES.length)
|
|
344
|
+
|
|
345
|
+
// The prompt corpus is a SHIPPED deliverable, not a build artifact: a human must be able
|
|
346
|
+
// to read the exact text every member receives without decoding session logs.
|
|
347
|
+
const REPO_ROOT = new URL('../', import.meta.url)
|
|
348
|
+
const needFiles = [
|
|
349
|
+
['the prompt-integrity suite is shipped', 'tests/prompt-v5-integrity.test.mjs'],
|
|
350
|
+
['the machine-readable prompt corpus is shipped', 'prompt-corpus-v5/prompt-corpus-v5.json'],
|
|
351
|
+
['the human-readable prompt corpus is shipped', 'prompt-corpus-v5/prompt-corpus-v5.md'],
|
|
352
|
+
['the persona-surface suite is shipped', 'tests/audit-persona-surface.test.mjs'],
|
|
353
|
+
['the four-preset persona corpus is shipped (machine-readable)', 'prompt-corpus-persona/persona-corpus.json'],
|
|
354
|
+
['the four-preset persona corpus is shipped (human-readable)', 'prompt-corpus-persona/persona-corpus.md'],
|
|
355
|
+
]
|
|
356
|
+
for (const [label, rel] of needFiles) {
|
|
357
|
+
let ok = false
|
|
358
|
+
try { ok = existsSync(new URL(rel, REPO_ROOT)) } catch (e) { ok = false }
|
|
359
|
+
if (!ok) findings.push('missing shipped prompt-correctness artifact: ' + label + ' (' + rel + ')')
|
|
360
|
+
}
|
|
361
|
+
notes.push('prompt-correctness artifacts checked: ' + needFiles.length)
|
|
362
|
+
|
|
363
|
+
// The corpus must actually contain the interactions a reviewer needs to see, and must
|
|
364
|
+
// never contain a wrong-identity brief.
|
|
365
|
+
try {
|
|
366
|
+
const corpusPath = new URL('prompt-corpus-v5/prompt-corpus-v5.json', REPO_ROOT)
|
|
367
|
+
const c = JSON.parse(readFileSync(corpusPath, 'utf8'))
|
|
368
|
+
const kinds = new Set((c.prompts || []).map(p => p.kind))
|
|
369
|
+
const need = ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
|
|
370
|
+
'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
|
|
371
|
+
'inbox-office', 'inbox-assign', 'inbox-nudge', 'inbox-office-assign', 'inbox-office-nudge',
|
|
372
|
+
'notice', 'notice-claim', 'after-failure',
|
|
373
|
+
// the Lean formal-verification interaction must be reviewable by a human too — including
|
|
374
|
+
// the `require` gate wording and the state a member sees AFTER a fidelity defect withdrew
|
|
375
|
+
// a proof (both were missing from the first corpus, so nobody could read them).
|
|
376
|
+
'lean-work', 'lean-verify', 'lean-fidelity', 'lean-require', 'lean-after-defect']
|
|
377
|
+
for (const k of need) if (!kinds.has(k)) findings.push('the prompt corpus is missing a ' + k + ' prompt')
|
|
378
|
+
const all = (c.prompts || []).map(p => p.prompt + '\n' + (p.charter || '')).join('\n')
|
|
379
|
+
if (/你是 \?/.test(all)) findings.push('the prompt corpus contains a wrong-identity "你是 ?" brief')
|
|
380
|
+
if (/\[状态\][^\n]*你是\s+(\S+?)[^\n]*\n/.test(all)) {
|
|
381
|
+
// every [状态] line must name a real member id, never a placeholder
|
|
382
|
+
for (const m of all.match(/\[状态\][^\n]*/g) || []) {
|
|
383
|
+
const id = (/\[状态\]\s*你是\s+(\S+?)(/.exec(m) || [])[1]
|
|
384
|
+
if (!id || id === '?' || id === 'undefined') findings.push('the prompt corpus has a bad identity line: ' + m.slice(0, 60))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
notes.push('prompt corpus: ' + (c.total || 0) + ' prompts, ' + kinds.size + ' kinds')
|
|
388
|
+
} catch (e) {
|
|
389
|
+
findings.push('the prompt corpus could not be read/parsed: ' + String((e && e.message) || e))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// The persona surface of ALL FOUR presets is a shipped prompt-correctness artifact too:
|
|
393
|
+
// it is the only prompt the MAIN agent receives, and no e2e suite loads the YAML, so a
|
|
394
|
+
// human reviewer needs the corpus on disk (generated by audit-persona-surface.test.mjs).
|
|
395
|
+
try {
|
|
396
|
+
const pc = JSON.parse(readFileSync(new URL('prompt-corpus-persona/persona-corpus.json', REPO_ROOT), 'utf8'))
|
|
397
|
+
for (const d of ['vibe-math-v2', 'vibe-math-v3', 'vibe-math-v4', 'vibe-math-v5']) {
|
|
398
|
+
if (!(pc.presets || []).some((p) => p.preset === d)) findings.push('the persona corpus is missing preset ' + d)
|
|
399
|
+
}
|
|
400
|
+
const emptyBlocks = (pc.presets || []).filter((p) => !p.prefix || !p.text).map((p) => p.preset)
|
|
401
|
+
if (emptyBlocks.length) findings.push('the persona corpus has an empty persona block: ' + emptyBlocks.join(', '))
|
|
402
|
+
notes.push('persona corpus: ' + (pc.presets || []).length + ' presets')
|
|
403
|
+
} catch (e) {
|
|
404
|
+
findings.push('the persona corpus could not be read/parsed: ' + String((e && e.message) || e))
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---- scanner self-check -------------------------------------------------
|
|
409
|
+
// The whole audit rests on stripNoise(); a scanner that mis-lexes silently BLINDS it (that is how the
|
|
410
|
+
// regex-with-a-quote bug lived here). Two checks, both cheap and both measured to be sensitive:
|
|
411
|
+
// · the stripped source must still PARSE (the pre-fix scanner produced a SyntaxError);
|
|
412
|
+
// · a fixture with a quoted character class + a following comment must survive intact.
|
|
413
|
+
{
|
|
414
|
+
const tmp = mkdtempSync(join(tmpdir(), 'v5-integrity-strip-'))
|
|
415
|
+
try {
|
|
416
|
+
const f = join(tmp, 'v5.mjs')
|
|
417
|
+
writeFileSync(f, src)
|
|
418
|
+
const r = spawnSync(process.execPath, ['--check', f], { encoding: 'utf8' })
|
|
419
|
+
if (r.status !== 0) {
|
|
420
|
+
findings.push('stripNoise() corrupted the source it scans (the stripped text does not parse) — every identifier check below is unreliable: ' +
|
|
421
|
+
String(r.stderr || '').split('\n').filter((l) => l.trim()).slice(-2).join(' ').slice(0, 160))
|
|
422
|
+
}
|
|
423
|
+
const fixture = [
|
|
424
|
+
'function f(s) { return s.replace(/["\']/g, "-") }',
|
|
425
|
+
'// phantom_call( must not survive as a call site',
|
|
426
|
+
'const div = 6 / 2',
|
|
427
|
+
].join('\n')
|
|
428
|
+
const stripped = stripNoise(fixture)
|
|
429
|
+
if (stripped.includes('phantom_call')) findings.push('stripNoise() left a comment in the code stream (a phantom call site would be reported)')
|
|
430
|
+
if (!/'/.test(stripped)) findings.push('stripNoise() dropped a value placeholder')
|
|
431
|
+
notes.push('scanner self-check: stripped output parses=' + (r.status === 0) + '; quoted-class fixture=' + (!stripped.includes('phantom_call')))
|
|
432
|
+
} finally {
|
|
433
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---- report ------------------------------------------------------------
|
|
438
|
+
console.log('-- V5 integrity audit --')
|
|
439
|
+
for (const n of notes) console.log(' note: ' + n)
|
|
440
|
+
console.log('')
|
|
441
|
+
if (findings.length) {
|
|
442
|
+
for (const f of findings) console.error(' FINDING: ' + f)
|
|
443
|
+
console.error('')
|
|
444
|
+
console.error(findings.length + ' finding(s)')
|
|
445
|
+
process.exit(1)
|
|
446
|
+
}
|
|
447
|
+
console.log('clean: no undefined calls, no undeclared params keys, no missing session API, no leftover markers')
|
|
448
|
+
process.exit(0)
|