dsh-vibe-math 2.7.2 → 2.7.3
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.en.md +11 -10
- package/README.md +13 -12
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +229 -3
- package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
- package/docs/final-paper.md +2 -2
- package/docs/math-computation.md +13 -6
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +1 -0
- package/docs/release-notes/README.md +1 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/TEMPLATE.md +6 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +8 -5
- package/installer.js +27 -17
- package/package.json +250 -203
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +295 -115
- package/prompt-corpus-v2/formal-verify-v2.md +2287 -283
- package/prompt-corpus-v3/formal-verify-v3.json +105 -105
- package/prompt-corpus-v3/formal-verify-v3.md +213 -143
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +451 -207
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +52 -4
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +64 -8
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +4 -2
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +14 -0
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +63 -22
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +50 -0
- package/tests/audit-prompt-invariants.mjs +43 -5
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +300 -13
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +12 -7
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +458 -13
- package/tests/formal-verify-v2.test.mjs +91 -43
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +85 -39
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +541 -9
- package/tests/formal-verify-v5.test.mjs +68 -9
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +274 -5
- package/tests/math-computation-v2.test.mjs +767 -0
- package/tests/math-computation-v3.test.mjs +759 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +13 -2
- package/tests/run-tests.mjs +139 -9
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +53 -1
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/v2-fix-probes.mutants.mjs +100 -0
- package/tests/v2-fix-probes.test.mjs +1039 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v3-fix-probes.mutants.mjs +95 -0
- package/tests/v3-fix-probes.test.mjs +891 -0
- package/tests/v5-institute-fixes.mutants.mjs +190 -0
- package/vibe-math-v2/agent.cordis.yml +62 -6
- package/vibe-math-v2/math-computation.js +130 -41
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/vibe-math-v2.js +388 -113
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
- package/vibe-math-v3/agent.cordis.yml +72 -6
- package/vibe-math-v3/math-computation.js +130 -41
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/vibe-math-v3.js +422 -126
- package/vibe-math-v4/agent.cordis.yml +44 -8
- package/vibe-math-v4/math-computation.js +130 -41
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/vibe-math-v4.js +196 -43
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +130 -41
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1173 -218
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +80 -12
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +2 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mutant harness for tests/audit-status-report-fields.mjs (ships: `tests/*.mutants.mjs`).
|
|
4
|
+
*
|
|
5
|
+
* ONE MUTANT PER DIRECTION (the doc↔source comparison must bite both ways):
|
|
6
|
+
* A) doc→source: delete one v2 row from a COPY of docs/status-report-fields.md -> the "every source
|
|
7
|
+
* field has a doc row" assertion reddens by name
|
|
8
|
+
* B) source→doc: add a field to a COPY of vibe-math-v2.js's `fieldScopes.session` -> the "every doc row
|
|
9
|
+
* names a real source field" / missing-row assertion reddens by name
|
|
10
|
+
*
|
|
11
|
+
* Run: node tests/audit-status-report-fields.mutants.mjs
|
|
12
|
+
*/
|
|
13
|
+
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { spawnSync } from 'node:child_process'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
20
|
+
const REPO = join(HERE, '..')
|
|
21
|
+
const GUARD = join(HERE, 'audit-status-report-fields.mjs')
|
|
22
|
+
let passed = 0, failed = 0
|
|
23
|
+
const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
|
|
24
|
+
const run = (env) => {
|
|
25
|
+
const r = spawnSync(process.execPath, [GUARD], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, env || {}) })
|
|
26
|
+
const out = String(r.stdout || '') + String(r.stderr || '')
|
|
27
|
+
return { status: r.status, failLines: out.split('\n').filter((l) => /^ - /.test(l)), tail: out.trim().split('\n').filter(Boolean).slice(-1)[0] || '' }
|
|
28
|
+
}
|
|
29
|
+
const baseline = run(null)
|
|
30
|
+
ok(baseline.status === 0, 'baseline: the field-table guard is green', baseline.tail.slice(0, 80))
|
|
31
|
+
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), 'fieldtable-mut-'))
|
|
33
|
+
// A) doc -> source
|
|
34
|
+
const docPath = join(REPO, 'docs/status-report-fields.md')
|
|
35
|
+
const docLines = readFileSync(docPath, 'utf8').split(/\r?\n/)
|
|
36
|
+
const v2Head = docLines.findIndex((l) => l.startsWith('### v2 的字段与作用域'))
|
|
37
|
+
const firstRow = docLines.findIndex((l, i) => i > v2Head && /^\|\s*`[^`]+`\s*\|/.test(l))
|
|
38
|
+
if (v2Head === -1 || firstRow === -1) ok(false, 'A: the v2 section and its first row exist (harness and guard agree on the shape)', 'ANCHOR MISS')
|
|
39
|
+
else {
|
|
40
|
+
const removed = docLines[firstRow]
|
|
41
|
+
const tampered = docLines.slice(0, firstRow).concat(docLines.slice(firstRow + 1)).join('\n')
|
|
42
|
+
writeFileSync(join(dir, 'status-report-fields.md'), tampered)
|
|
43
|
+
const a = run({ FIELD_DOC: join(dir, 'status-report-fields.md') })
|
|
44
|
+
const named = a.failLines.some((l) => l.indexOf('v2: every source field has a doc row') !== -1)
|
|
45
|
+
ok(a.status !== 0 && named, '★ A (doc→source): deleting a v2 row reddens the guard by name', 'removed=' + removed.trim().slice(0, 60) + ' exit=' + a.status)
|
|
46
|
+
if (!named) for (const l of a.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
|
|
47
|
+
}
|
|
48
|
+
// B) source -> doc
|
|
49
|
+
const v2Path = join(REPO, 'vibe-math-v2/vibe-math-v2.js')
|
|
50
|
+
const v2src = readFileSync(v2Path, 'utf8')
|
|
51
|
+
const m = /(\n\s*session:\s*\[)([\s\S]*?)(\])/.exec(v2src)
|
|
52
|
+
if (!m) ok(false, 'B: the v2 fieldScopes session array exists (anchor check)', 'ANCHOR MISS')
|
|
53
|
+
else {
|
|
54
|
+
const injected = m[1] + m[2].replace(/\s*$/, '') + ", 'ghostFieldFromMutant'" + m[3]
|
|
55
|
+
writeFileSync(join(dir, 'vibe-math-v2.js'), v2src.replace(m[0], injected))
|
|
56
|
+
const b = run({ FIELD_V2_JS: join(dir, 'vibe-math-v2.js') })
|
|
57
|
+
const named = b.failLines.some((l) => l.indexOf('v2: every source field has a doc row') !== -1 || l.indexOf('v2: every doc row names a real source field') !== -1)
|
|
58
|
+
ok(b.status !== 0 && named, '★ B (source→doc): adding a field to the source reddens the guard by name', 'exit=' + b.status)
|
|
59
|
+
if (!named) for (const l of b.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
|
|
60
|
+
}
|
|
61
|
+
rmSync(dir, { recursive: true, force: true })
|
|
62
|
+
console.log('')
|
|
63
|
+
console.log('=== STATUS FIELD TABLE MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
64
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe for prompt-audit M3 (v3): the two registerTool paths must not drift.
|
|
3
|
+
*
|
|
4
|
+
* v3 declares each tool twice — the session handler table inside makeSession()
|
|
5
|
+
* (registerTool(name, description, parameters, executeFn)) and the real
|
|
6
|
+
* `tools.register` at apply scope (registerTool(name, description, parameters,
|
|
7
|
+
* handlerName)). The parameter schemas are guarded by audit-prompt-invariants
|
|
8
|
+
* (I13/I14); the DESCRIPTIONS were not, and five of them had already drifted.
|
|
9
|
+
*
|
|
10
|
+
* Checks per tool (33 of them):
|
|
11
|
+
* - MISSING : registered on only one path
|
|
12
|
+
* - DESC-DRIFT : the description argument text differs between the two paths
|
|
13
|
+
* - SCHEMA-DRIFT : the parameter-schema argument text differs
|
|
14
|
+
* - NO-SOURCE : a description is not read from the shared module-level table
|
|
15
|
+
* (the "one source of truth" claim would be nominal only)
|
|
16
|
+
* - BAD-REF : a TOOL_DESC.<name> reference has no matching entry in the table
|
|
17
|
+
*
|
|
18
|
+
* Read-only: parses the source text, never executes the plugin.
|
|
19
|
+
*
|
|
20
|
+
* Sensitivity probe (the suite must be falsifiable):
|
|
21
|
+
* node tests/audit-v3-registration-parity.mjs --self-probe '["<from>","<to>"]'
|
|
22
|
+
* The pair is applied to the source text, so a first-occurrence mutation makes the
|
|
23
|
+
* second path disagree and the probe must exit 1.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
let SRC = readFileSync(join(HERE, '..', 'vibe-math-v3', 'vibe-math-v3.js'), 'utf8');
|
|
31
|
+
|
|
32
|
+
const SELF_PROBE = process.argv.indexOf('--self-probe');
|
|
33
|
+
if (SELF_PROBE !== -1 && process.argv[SELF_PROBE + 1]) {
|
|
34
|
+
const [from, to] = JSON.parse(process.argv[SELF_PROBE + 1]);
|
|
35
|
+
const before = SRC;
|
|
36
|
+
SRC = SRC.replace(from, to);
|
|
37
|
+
if (SRC === before) { console.log('MUTATION DID NOT APPLY: ' + from); process.exit(2); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Skip a string literal (any quote) starting at i; returns the index after it. */
|
|
41
|
+
function skipString(s, i) {
|
|
42
|
+
const q = s[i];
|
|
43
|
+
i++;
|
|
44
|
+
while (i < s.length) {
|
|
45
|
+
if (s[i] === '\\') { i += 2; continue; }
|
|
46
|
+
if (s[i] === q) return i + 1;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
return i;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Normalise line endings: this file is checked out with CRLF on Windows, and a stray \r
|
|
53
|
+
// between an entry's `,` and its key would make the table parser under-count (it did).
|
|
54
|
+
SRC = SRC.replace(/\r/g, '');
|
|
55
|
+
|
|
56
|
+
/** Collect a top-level-comma-separated argument list starting at `open` (an index of `(`). */
|
|
57
|
+
function argsOf(s, open) {
|
|
58
|
+
let depth = 0;
|
|
59
|
+
const args = [];
|
|
60
|
+
let cur = '';
|
|
61
|
+
for (let i = open; i < s.length; i++) {
|
|
62
|
+
const c = s[i];
|
|
63
|
+
if (c === '"' || c === "'" || c === '`') { const j = skipString(s, i); cur += s.slice(i, j); i = j - 1; continue; }
|
|
64
|
+
if (c === '(' || c === '[' || c === '{') { depth++; cur += c; continue; }
|
|
65
|
+
if (c === ')' || c === ']' || c === '}') {
|
|
66
|
+
depth--;
|
|
67
|
+
if (depth === 0) { args.push(cur.trim()); return args; }
|
|
68
|
+
cur += c; continue;
|
|
69
|
+
}
|
|
70
|
+
if (c === ',' && depth === 1) { args.push(cur.trim()); cur = ''; continue; }
|
|
71
|
+
if (depth >= 1) cur += c;
|
|
72
|
+
}
|
|
73
|
+
return args;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const calls = [];
|
|
77
|
+
for (const m of SRC.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)) {
|
|
78
|
+
const open = SRC.indexOf('(', m.index + 'registerTool'.length);
|
|
79
|
+
calls.push({ name: m[1], at: m.index, args: argsOf(SRC, open) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const byName = new Map();
|
|
83
|
+
for (const c of calls) {
|
|
84
|
+
if (!byName.has(c.name)) byName.set(c.name, []);
|
|
85
|
+
byName.get(c.name).push(c);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The shared description table (module-level `const TOOL_DESC = { … }`).
|
|
89
|
+
//
|
|
90
|
+
// Keys are collected with an ANCHORED PER-LINE regex inside the table's own text slice —
|
|
91
|
+
// string values are blanked out first (keeping every newline) so the `{`/`}` braces inside
|
|
92
|
+
// the sync_meta documentation text cannot shift a brace-depth scanner (that mis-parse is
|
|
93
|
+
// exactly how an earlier version of this probe silently saw 15 of 33 entries).
|
|
94
|
+
const tableAt = SRC.search(/const\s+TOOL_DESC\s*=\s*\{/);
|
|
95
|
+
const tableKeys = new Set();
|
|
96
|
+
if (tableAt !== -1) {
|
|
97
|
+
const open = SRC.indexOf('{', tableAt);
|
|
98
|
+
// slice to the matching close brace with strings blanked (newlines preserved)
|
|
99
|
+
let depth = 0;
|
|
100
|
+
let end = -1;
|
|
101
|
+
let blanked = '';
|
|
102
|
+
for (let i = open; i < SRC.length; i++) {
|
|
103
|
+
const c = SRC[i];
|
|
104
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
105
|
+
const stop = skipString(SRC, i);
|
|
106
|
+
for (let k = i; k < stop; k++) blanked += (SRC[k] === '\n' ? '\n' : ' ');
|
|
107
|
+
i = stop - 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
blanked += c;
|
|
111
|
+
if (c === '{') depth++;
|
|
112
|
+
else if (c === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
113
|
+
}
|
|
114
|
+
const tableText = blanked.slice(0, end === -1 ? blanked.length : end - open + 1);
|
|
115
|
+
for (const m of tableText.matchAll(/^[ \t]*([A-Za-z_$][\w$]*)\s*:/gm)) tableKeys.add(m[1]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let descDrift = 0;
|
|
119
|
+
let schemaDrift = 0;
|
|
120
|
+
let noSource = 0;
|
|
121
|
+
let badRef = 0;
|
|
122
|
+
const missing = [];
|
|
123
|
+
for (const [name, list] of byName) {
|
|
124
|
+
if (list.length !== 2) { missing.push(name + ' (' + list.length + ' registration(s))'); continue; }
|
|
125
|
+
const [a, b] = list.map((c) => ({ desc: c.args[1], schema: c.args[2] }));
|
|
126
|
+
if (a.desc !== b.desc) {
|
|
127
|
+
descDrift++;
|
|
128
|
+
console.log('DESC-DRIFT ' + name);
|
|
129
|
+
console.log(' path#1: ' + String(a.desc).slice(0, 140));
|
|
130
|
+
console.log(' path#2: ' + String(b.desc).slice(0, 140));
|
|
131
|
+
}
|
|
132
|
+
if (a.schema !== b.schema) {
|
|
133
|
+
schemaDrift++;
|
|
134
|
+
console.log('SCHEMA-DRIFT ' + name);
|
|
135
|
+
console.log(' path#1: ' + String(a.schema).slice(0, 140));
|
|
136
|
+
console.log(' path#2: ' + String(b.schema).slice(0, 140));
|
|
137
|
+
}
|
|
138
|
+
for (const d of [a.desc, b.desc]) {
|
|
139
|
+
const want = 'TOOL_DESC.' + name;
|
|
140
|
+
if (d === want) continue;
|
|
141
|
+
if (/^TOOL_DESC\./.test(d)) { badRef++; console.log('BAD-REF ' + name + ' → ' + d); continue; }
|
|
142
|
+
noSource++;
|
|
143
|
+
console.log('NO-SOURCE ' + name + ' → ' + String(d).slice(0, 140));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('shared TOOL_DESC entries: ' + tableKeys.size);
|
|
148
|
+
console.log('tools registered twice: ' + [...byName.values()].filter((v) => v.length === 2).length + ' / ' + byName.size);
|
|
149
|
+
console.log('DESC-DRIFT count: ' + descDrift);
|
|
150
|
+
console.log('SCHEMA-DRIFT count: ' + schemaDrift);
|
|
151
|
+
console.log('descriptions not from the shared table: ' + noSource);
|
|
152
|
+
console.log('bad TOOL_DESC references: ' + badRef);
|
|
153
|
+
console.log('one-path-only tools: ' + (missing.length ? missing.join(', ') : 'none'));
|
|
154
|
+
const ok = descDrift === 0 && schemaDrift === 0 && noSource === 0 && badRef === 0 && missing.length === 0 && tableKeys.size === byName.size;
|
|
155
|
+
console.log(ok ? '=== DESC PARITY OK ===' : '=== DESC PARITY FAILED ===');
|
|
156
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -9,13 +9,125 @@
|
|
|
9
9
|
// 5. leftover development markers / TODO scaffolding
|
|
10
10
|
// Run: node tests/audit-v5-integrity.mjs (exit 1 on any finding)
|
|
11
11
|
// ============================================================
|
|
12
|
-
import { readFileSync, existsSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
12
|
+
import { readFileSync, existsSync, mkdtempSync, writeFileSync, rmSync, readdirSync } from 'node:fs'
|
|
13
13
|
import { spawnSync } from 'node:child_process'
|
|
14
14
|
import { tmpdir } from 'node:os'
|
|
15
15
|
import { join } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const HERE = new URL('../', import.meta.url)
|
|
19
|
+
/**
|
|
20
|
+
* `V5_INTEGRITY_MUTATE` carries a JSON `[rel, from, to]` triple: that ONE repo-relative file is mutated
|
|
21
|
+
* IN MEMORY for one child run. Combined with `--self-probe` this makes every guard below reproducible
|
|
22
|
+
* from OUTSIDE — a verifier no longer has to copy this audit and rewrite its URLs to point it at a
|
|
23
|
+
* mutated copy (R16's request). Same shape as `audit-prompt-invariants.mjs` and the
|
|
24
|
+
* `math-computation-shared --self-probe` pattern.
|
|
25
|
+
*
|
|
26
|
+
* SEAM COVERAGE (R17 measured, both directions): the seam reaches a **plugin** path (a plugin mutation
|
|
27
|
+
* produces 1 finding) AND the **corpus** path (a corpus mutation produces 2 findings) — i.e. it is not
|
|
28
|
+
* limited to the plugin file, and a corpus-only drift is observable from outside too. The corpus is read
|
|
29
|
+
* through the same `readRaw()`, so any new corpus check inherits the seam.
|
|
30
|
+
*/
|
|
31
|
+
const MUT = process.env.V5_INTEGRITY_MUTATE
|
|
32
|
+
function readRaw(rel) {
|
|
33
|
+
let text = readFileSync(new URL(rel, HERE), 'utf8')
|
|
34
|
+
if (MUT) {
|
|
35
|
+
try {
|
|
36
|
+
const [r, from, to] = JSON.parse(MUT)
|
|
37
|
+
if (r === rel && from) text = text.replace(from, to)
|
|
38
|
+
} catch (e) { /* a malformed mutation is a harness error, not an audit failure */ }
|
|
39
|
+
}
|
|
40
|
+
return text
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Each mutation is a REAL defect shape for one of this audit's guards. `expect` is a substring that MUST
|
|
45
|
+
* appear in the failing run; `control: true` marks the unmutated run, which must stay green.
|
|
46
|
+
*/
|
|
47
|
+
const SELF_PROBE_MUTATIONS = [
|
|
48
|
+
{ name: 'control (no mutation)', rel: '', from: '', to: '', expect: '', control: true },
|
|
49
|
+
{
|
|
50
|
+
name: 'F3: a status() top-level key is renamed (the frozen field surface must redden)',
|
|
51
|
+
rel: 'vibe-math-v5/vibe-math-v5.js',
|
|
52
|
+
from: 'leanNoticesScope: ',
|
|
53
|
+
to: 'leanNoticesScopeRenamed: ',
|
|
54
|
+
expect: 'status() field surface changed',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'F1: the README resume claim loses its in-instance case (the pairing gate must redden)',
|
|
58
|
+
rel: 'README.md',
|
|
59
|
+
from: '**同实例重建**(宿主丢了子会话、插件实例还在)**继续**原编号',
|
|
60
|
+
to: '**同实例重建**从 1 重新开始',
|
|
61
|
+
expect: 'README resume-claim side missing',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'F1: a code anchor the README cites is removed (its philosophy gate must redden)',
|
|
65
|
+
rel: 'vibe-math-v5/vibe-math-v5.js',
|
|
66
|
+
from: 'rounds.set(member.id, startRound)',
|
|
67
|
+
to: 'void 0 /* anchor removed (self-probe) */',
|
|
68
|
+
expect: 'philosophy gate missing from the implementation: the founding round is applied only AFTER a successful start',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'F2/G1: the diagram is mutated back to the pre-G1 inbox order (the pairing gate must redden)',
|
|
72
|
+
rel: 'vibe-math-v5/架构图.md',
|
|
73
|
+
from: '唯一的推进驱动',
|
|
74
|
+
to: '唯一的推进驱动(先 ack 再构造)',
|
|
75
|
+
expect: '架构图 still states the pre-G1 order',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'F2/G1: the plan is mutated back to the explicit pre-G1 inbox phrasing (the pairing gate must redden)',
|
|
79
|
+
rel: 'vibe-math-v5/实现方案.md',
|
|
80
|
+
from: '**未 ack ⇒ 仍可重投**',
|
|
81
|
+
to: '**先 ack 邮件、再构造**;**未 ack ⇒ 仍可重投**',
|
|
82
|
+
expect: '实现方案.md still states the pre-G1 order',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'F2/G1: a docs file is mutated back to the named pre-G1 phrasing (the docs-wide gate must redden)',
|
|
86
|
+
rel: 'docs/AUDIT-CHECKLIST.md',
|
|
87
|
+
from: '身份一律显式传递,绝不猜测。',
|
|
88
|
+
to: '身份一律显式传递,绝不猜测。先 ack 再构造',
|
|
89
|
+
expect: 'still states the pre-G1 order',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'F2/G1: the plan is mutated to re-introduce the pre-G1 re-send contract (the named check must redden)',
|
|
93
|
+
rel: 'vibe-math-v5/实现方案.md',
|
|
94
|
+
from: '`makeFileBackend`',
|
|
95
|
+
to: '`makeFileBackend`(queued 永不重发)',
|
|
96
|
+
expect: 'still states the pre-G1 re-send contract',
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
if (process.argv.includes('--self-probe')) {
|
|
101
|
+
const bad = []
|
|
102
|
+
for (const mut of SELF_PROBE_MUTATIONS) {
|
|
103
|
+
const r = spawnSync(process.execPath, [fileURLToPath(import.meta.url), '--json'], {
|
|
104
|
+
cwd: fileURLToPath(HERE),
|
|
105
|
+
env: Object.assign({}, process.env, mut.control ? {} : { V5_INTEGRITY_MUTATE: JSON.stringify([mut.rel, mut.from, mut.to]) }),
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
})
|
|
108
|
+
let parsed = null
|
|
109
|
+
try { parsed = JSON.parse(r.stdout) } catch (e) { /* fall through to the diagnostic below */ }
|
|
110
|
+
const text = (r.stdout || '') + (r.stderr || '')
|
|
111
|
+
if (mut.control) {
|
|
112
|
+
const ok = r.status === 0 && parsed && parsed.failed === 0
|
|
113
|
+
console.log((ok ? 'PASS ' : 'FAIL ') + 'control: the unmutated run stays green (' + (parsed ? parsed.failed + ' findings, ' + parsed.gates + ' gates' : 'unparsable output') + ')')
|
|
114
|
+
if (!ok) bad.push('control run was not green')
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
const hit = text.includes(mut.expect)
|
|
118
|
+
const red = r.status === 1 && parsed && parsed.failed > 0
|
|
119
|
+
const ok = red && hit
|
|
120
|
+
console.log((ok ? 'PASS ' : 'FAIL ') + mut.name + (ok ? '' : ' → exit=' + r.status + ' hit=' + hit + ' failed=' + (parsed && parsed.failed)))
|
|
121
|
+
if (!ok) bad.push(mut.name + ' (exit ' + r.status + ', expected ' + JSON.stringify(mut.expect) + ')')
|
|
122
|
+
}
|
|
123
|
+
console.log('')
|
|
124
|
+
console.log('V5 INTEGRITY SELF-PROBE: ' + (SELF_PROBE_MUTATIONS.length - bad.length) + '/' + SELF_PROBE_MUTATIONS.length + ' as required')
|
|
125
|
+
for (const b of bad) console.error(' FAIL ' + b)
|
|
126
|
+
process.exit(bad.length === 0 ? 0 : 1)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const FILE_REL = 'vibe-math-v5/vibe-math-v5.js'
|
|
130
|
+
const raw = readRaw(FILE_REL)
|
|
19
131
|
// Strip comments, string literals AND regex literals before any identifier scan. Without this the
|
|
20
132
|
// heuristic matches English words inside comments that merely precede a '(' (e.g.
|
|
21
133
|
// "// per unit (" becomes a phantom call to unit()), drowning the real findings.
|
|
@@ -79,6 +191,8 @@ function stripNoise(s) {
|
|
|
79
191
|
const src = stripNoise(raw)
|
|
80
192
|
const findings = []
|
|
81
193
|
const notes = []
|
|
194
|
+
// How many philosophy gates the run checked (module scope so `--json` can report it).
|
|
195
|
+
let gateCount = 0
|
|
82
196
|
|
|
83
197
|
const lineOf = (idx) => src.slice(0, idx).split('\n').length
|
|
84
198
|
|
|
@@ -213,7 +327,7 @@ else {
|
|
|
213
327
|
const raised = new Set()
|
|
214
328
|
for (const m of raw.matchAll(/v5err\(\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
215
329
|
for (const m of raw.matchAll(/code:\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
216
|
-
const docs =
|
|
330
|
+
const docs = readRaw('vibe-math-v5/实现方案.md')
|
|
217
331
|
const documented = new Set()
|
|
218
332
|
for (const m of docs.matchAll(/`(V5_[A-Z_]+)`/g)) documented.add(m[1])
|
|
219
333
|
const onlyDocs = new Set()
|
|
@@ -262,8 +376,8 @@ function packageRows(yaml) {
|
|
|
262
376
|
}
|
|
263
377
|
return rows
|
|
264
378
|
}
|
|
265
|
-
const v5yaml =
|
|
266
|
-
const v4yaml =
|
|
379
|
+
const v5yaml = readRaw('vibe-math-v5/agent.cordis.yml')
|
|
380
|
+
const v4yaml = readRaw('vibe-math-v4/agent.cordis.yml')
|
|
267
381
|
const v5rows = packageRows(v5yaml)
|
|
268
382
|
const v4names = new Set(packageRows(v4yaml).map(r => r.name))
|
|
269
383
|
const RELATIVE_OK = new Set(['./vibe-math-v5.js'])
|
|
@@ -293,7 +407,7 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
|
|
|
293
407
|
// against the implementation: a name in the plan but not the code is an unimplemented
|
|
294
408
|
// requirement, and a name in the code but not the plan is undocumented surface.
|
|
295
409
|
{
|
|
296
|
-
const plan =
|
|
410
|
+
const plan = readRaw('vibe-math-v5/实现方案.md')
|
|
297
411
|
const planTools = new Set()
|
|
298
412
|
for (const m of plan.matchAll(/\bvibe_v5_[a-z_]+/g)) planTools.add(m[0])
|
|
299
413
|
const codeTools = new Set()
|
|
@@ -325,6 +439,158 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
|
|
|
325
439
|
}
|
|
326
440
|
notes.push('plan §16 parameter rows: ' + paramRows.size)
|
|
327
441
|
|
|
442
|
+
// F3 (docs-vs-behaviour sweep): `status()`'s machine surface. OWNERSHIP (agreed division, so the two
|
|
443
|
+
// guards never both claim the same job):
|
|
444
|
+
// • FIELD-LEVEL reconciliation (field ↔ doc row, BOTH directions) is owned by
|
|
445
|
+
// `tests/audit-status-report-fields.mjs`, which carries the table and prints the
|
|
446
|
+
// `frozen / doc-rows / exceptions` counts;
|
|
447
|
+
// • THIS audit keeps ONLY this frozen-key drift check: the extracted 43 keys below, the printed
|
|
448
|
+
// `captured: N`, and the `<30` anti-vacuity floor. Any addition/removal of a top-level key must be
|
|
449
|
+
// an explicit decision here (and in the owning guard's reconciliation).
|
|
450
|
+
// No "doc half" is promised or planned here — the note printed at the end points at the owner instead.
|
|
451
|
+
const stIdx = raw.indexOf('function status() {')
|
|
452
|
+
const statusKeys = []
|
|
453
|
+
if (stIdx === -1) findings.push('could not locate status() — the machine-surface extraction has no anchor')
|
|
454
|
+
else {
|
|
455
|
+
const open = raw.indexOf('return {', stIdx)
|
|
456
|
+
const close = raw.indexOf('\n }', open)
|
|
457
|
+
// Tokenise the returned literal: every key directly inside it — including SHORTHAND keys that share a
|
|
458
|
+
// line (`institute: instituteName, project, key, phase,`) — while SKIPPING each value (so an
|
|
459
|
+
// identifier used as a value can never be mistaken for a key).
|
|
460
|
+
const body = open === -1 ? '' : raw.slice(open + 'return {'.length, close === -1 ? open + 9000 : close)
|
|
461
|
+
// Comments must be skipped IN PLACE: the payload's comments contain prose with apostrophes, parens and
|
|
462
|
+
// commas (`…delivered ONCE…`, `phase:'failed'`, `(installer review)`), which otherwise corrupt both the
|
|
463
|
+
// depth tracking and the value skipping.
|
|
464
|
+
const skipComment = (s, at) => {
|
|
465
|
+
if (s[at + 1] === '/') { let j = at + 2; while (j < s.length && s[j] !== '\n') j += 1; return j }
|
|
466
|
+
if (s[at + 1] === '*') { const j = s.indexOf('*/', at + 2); return j === -1 ? s.length : j + 2 }
|
|
467
|
+
return -1
|
|
468
|
+
}
|
|
469
|
+
let i = 0, depth = 0
|
|
470
|
+
while (i < body.length) {
|
|
471
|
+
const ch = body[i]
|
|
472
|
+
if (ch === '/') { const j = skipComment(body, i); if (j !== -1) { i = j; continue } }
|
|
473
|
+
if (ch === '"' || ch === "'") { const q = ch; i += 1; while (i < body.length && body[i] !== q) { if (body[i] === '\\') i += 1; i += 1 } i += 1; continue }
|
|
474
|
+
if (ch === '{' || ch === '[' || ch === '(') { depth += 1; i += 1; continue }
|
|
475
|
+
if (ch === '}' || ch === ']' || ch === ')') { if (depth === 0) break; depth -= 1; i += 1; continue }
|
|
476
|
+
if (depth === 0 && /[A-Za-z_$]/.test(ch)) {
|
|
477
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(body.slice(i))
|
|
478
|
+
const after = body.slice(i + m[0].length).replace(/^[\s]+/, '')
|
|
479
|
+
if (after[0] === ':') {
|
|
480
|
+
statusKeys.push(m[0])
|
|
481
|
+
i += m[0].length
|
|
482
|
+
let d = 0
|
|
483
|
+
while (i < body.length) { // skip the VALUE up to the next top-level comma
|
|
484
|
+
const c = body[i]
|
|
485
|
+
if (c === '/') { const j = skipComment(body, i); if (j !== -1) { i = j; continue } }
|
|
486
|
+
if (c === '"' || c === "'") { const q = c; i += 1; while (i < body.length && body[i] !== q) { if (body[i] === '\\') i += 1; i += 1 } i += 1; continue }
|
|
487
|
+
if (c === '{' || c === '[' || c === '(') { d += 1; i += 1; continue }
|
|
488
|
+
if (c === '}' || c === ']' || c === ')') { if (d === 0) break; d -= 1; i += 1; continue }
|
|
489
|
+
if (c === ',' && d === 0) { i += 1; break }
|
|
490
|
+
i += 1
|
|
491
|
+
}
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
if (after[0] === ',') { statusKeys.push(m[0]); i += m[0].length; continue }
|
|
495
|
+
i += m[0].length; continue
|
|
496
|
+
}
|
|
497
|
+
i += 1
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
notes.push('status() top-level keys captured: ' + statusKeys.length)
|
|
501
|
+
if (statusKeys.length < 30) findings.push('status() key extraction captured only ' + statusKeys.length + ' keys (anchor or payload broke — this check must never pass vacuously)')
|
|
502
|
+
for (const must of ['phase', 'members', 'quorum', 'chat', 'persistence']) {
|
|
503
|
+
if (statusKeys.indexOf(must) === -1) findings.push('status() no longer exposes `' + must + '` (either the extraction anchor or the payload broke)')
|
|
504
|
+
}
|
|
505
|
+
const EXPECTED_STATUS_KEYS = ['ok', 'institute', 'project', 'key', 'phase', 'running', 'autoDone', 'runId', 'leanNotices', 'leanNoticesScope', 'fieldScopes', 'backend', 'diagnostics', 'debug', 'quorum', 'members', 'tasks', 'failedMembers', 'failedMembersNote', 'pendingSpawns', 'pendingSpawnsNote', 'chat', 'chatScope', 'officeRequests', 'officeRequestsShown', 'officeRequestsCap', 'officeRequestsDropped', 'officeRequestsTruncated', 'persistence', 'meeting', 'parkedMeeting', 'verify', 'verifyQueue', 'verified', 'verifiedTrue', 'concludedFalse', 'verifiedNote', 'undecided', 'solveVotes', 'formal', 'paper', 'lastProgressAt', 'params']
|
|
506
|
+
const statusMissing = EXPECTED_STATUS_KEYS.filter((k) => statusKeys.indexOf(k) === -1)
|
|
507
|
+
const statusAdded = statusKeys.filter((k) => EXPECTED_STATUS_KEYS.indexOf(k) === -1)
|
|
508
|
+
if (statusMissing.length || statusAdded.length) {
|
|
509
|
+
findings.push('status() field surface changed — missing=' + JSON.stringify(statusMissing) + ' added=' + JSON.stringify(statusAdded) +
|
|
510
|
+
': record the decision in this freeze AND let the field-level owner reconcile the documents (tests/audit-status-report-fields.mjs)')
|
|
511
|
+
}
|
|
512
|
+
notes.push('status() field-surface drift: frozen=' + statusKeys.length + ' keys (this audit owns ONLY this freeze + the <30 anti-vacuity floor); ' +
|
|
513
|
+
'field-level reconciliation (field ↔ doc row, both directions) is OWNED BY tests/audit-status-report-fields.mjs, which prints frozen / doc-rows / exceptions')
|
|
514
|
+
|
|
515
|
+
// F1 (docs-vs-behaviour): the README's TWO-CASE `resume` claim must stay paired with the behavioural
|
|
516
|
+
// anchor that pins the in-instance case (the two CODE anchors are gates in the GATES table below). Only
|
|
517
|
+
// the prose side is checked here, and every side reports how much it matched, so this cannot pass vacuously.
|
|
518
|
+
{
|
|
519
|
+
const readme = readRaw('README.md')
|
|
520
|
+
const suite = readRaw('tests/e2e-v5-round2.test.mjs')
|
|
521
|
+
const readmeClaims = [
|
|
522
|
+
['the two-case framing', /`resume`\s*后轮次计数的两种情形/],
|
|
523
|
+
['case 1 = a cross-process reload counts from 1', /跨进程重载[^\n]*从\s*\*\*1\*\*\s*重新计/],
|
|
524
|
+
['case 2 = an in-instance rebuild CONTINUES the numbering (轮次 2)', /同实例重建[^\n]*继续[^\n]*轮次 2/],
|
|
525
|
+
['a failed/invalid start no longer erases the existing count', /失败\/无效的启动\*\*不再抹掉\*\*已有计数/],
|
|
526
|
+
['the claim is pinned to a revision', /口径复核于\s*`?[0-9a-f]{7,40}`?/],
|
|
527
|
+
]
|
|
528
|
+
let readmeHits = 0
|
|
529
|
+
for (const [label, re] of readmeClaims) {
|
|
530
|
+
if (re.test(readme)) readmeHits += 1
|
|
531
|
+
else findings.push('README resume-claim side missing: ' + label)
|
|
532
|
+
}
|
|
533
|
+
const suiteClaims = [
|
|
534
|
+
['the in-instance rebuild is asserted as 轮次 2', /a FAILED resume must not move the round number backwards/],
|
|
535
|
+
['the founding round is asserted as 轮次 1', /the founding prompt starts the member at 轮次 1/],
|
|
536
|
+
]
|
|
537
|
+
let suiteHits = 0
|
|
538
|
+
for (const [label, re] of suiteClaims) {
|
|
539
|
+
if (re.test(suite)) suiteHits += 1
|
|
540
|
+
else findings.push('README resume-claim has no behavioural anchor: ' + label)
|
|
541
|
+
}
|
|
542
|
+
// The README cites exact code/test lines PINNED to a revision ("口径复核于 <sha>"). Those drift by
|
|
543
|
+
// design, so the drift is a measured NOTE carrying the current lines — never a failure.
|
|
544
|
+
const lineOf = (needle) => { const at = raw.indexOf(needle); return at === -1 ? -1 : raw.slice(0, at).split('\n').length }
|
|
545
|
+
const startLine = lineOf('const startRound = (rounds.get(member.id) || 0) + 1')
|
|
546
|
+
const applyLine = lineOf('rounds.set(member.id, startRound)')
|
|
547
|
+
if (startLine === -1 || applyLine === -1) findings.push('the spawnMember round-count anchors are gone (startRound=' + startLine + ', applied=' + applyLine + ')')
|
|
548
|
+
notes.push('F1 resume-claim pairing: README ' + readmeHits + '/' + readmeClaims.length + ' claims, behavioural anchors ' + suiteHits + '/' + suiteClaims.length + '; cited-line drift (README pins a revision): startRound now :' + startLine + ', applied now :' + applyLine)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// F2/G1 pairing (prose ↔ code): the v5 diagram must not state the PRE-G1 order, and must describe the
|
|
552
|
+
// implemented one. Going through `readRaw` gives this block the same `V5_INTEGRITY_MUTATE` seam as every
|
|
553
|
+
// other file, which is what the `--self-probe` case below relies on.
|
|
554
|
+
{
|
|
555
|
+
const diagram = readRaw('vibe-math-v5/架构图.md')
|
|
556
|
+
const FORBID = /先\s*ack[^\n]{0,8}再构造/
|
|
557
|
+
const CLAUSES = /构造[^\n]{0,12}(后|再)[^\n]{0,12}ack|ack[^\n]{0,12}(仅|只)[^\n]{0,12}(成功|送达)/
|
|
558
|
+
if (FORBID.test(diagram)) findings.push('架构图 still states the pre-G1 order: ack before the prompt is built (F2/G1)')
|
|
559
|
+
if (!CLAUSES.test(diagram)) findings.push('架构图 does not describe the implemented inbox order (construct first / ack only on success)')
|
|
560
|
+
if (!raw.includes('if (ok && prompt.pending.length) await ackPending(')) findings.push('the ack is no longer gated by a successful wake (F2/G1 anchor missing)')
|
|
561
|
+
notes.push('架构图 inbox pairing: forbid-hits=' + (FORBID.test(diagram) ? 1 : 0) + ', clauses=' + (CLAUSES.test(diagram) ? 1 : 0))
|
|
562
|
+
// The PLAN uses the explicit pre-G1 phrases only: a sentence that legitimately QUOTES or FORBIDS the old
|
|
563
|
+
// order must not trip this, so the pattern names the phrases instead of the bare `先 ack`.
|
|
564
|
+
const planDoc = readRaw('vibe-math-v5/实现方案.md')
|
|
565
|
+
const PLAN_FORBID = /先\s*ack\s*邮件[、,,]?\s*再构造|先\s*ack[、,,]?\s*后构造/
|
|
566
|
+
if (PLAN_FORBID.test(planDoc)) findings.push('实现方案.md still states the pre-G1 order: ack the mail before building the prompt (F2/G1)')
|
|
567
|
+
notes.push('实现方案.md inbox pairing: forbid-hits=' + (PLAN_FORBID.test(planDoc) ? 1 : 0))
|
|
568
|
+
// The SAME class produced five instances, the last one inside the guard checklist itself, so the scan
|
|
569
|
+
// covers every top-level docs/*.md. The phrase is NAMED (never the bare `先 ack`) so a sentence that
|
|
570
|
+
// legitimately QUOTES or FORBIDS the old order cannot trip it. NOTE (applier): `HERE` is the REPO ROOT
|
|
571
|
+
// (see `new URL(rel, HERE)` above), so the docs directory is `new URL('docs/', HERE)` — NOT `../docs/`.
|
|
572
|
+
const DOC_FORBID = /先\s*ack[^\n]{0,8}再构造/
|
|
573
|
+
let docHits = 0, docFiles = 0
|
|
574
|
+
for (const f of readdirSync(new URL('docs/', HERE)).filter((n) => n.endsWith('.md'))) {
|
|
575
|
+
docFiles += 1
|
|
576
|
+
if (DOC_FORBID.test(readRaw('docs/' + f))) { docHits += 1; findings.push('docs/' + f + ' still states the pre-G1 order: ack the mail before building the prompt (F2/G1)') }
|
|
577
|
+
}
|
|
578
|
+
// The scan covers LIVE docs only. `docs/release-notes/**` is a FROZEN published record: the pre-G1
|
|
579
|
+
// wording there is history, not a stale claim, so it must stay OUT of the scan — and the exemption is
|
|
580
|
+
// ASSERTED (a missing/empty directory fails) so a future change to recursive scanning cannot silently
|
|
581
|
+
// start scanning frozen history. Same convention as audit-readme-counts.mjs's frozen TOTAL-65 notes.
|
|
582
|
+
let frozenNotes = []
|
|
583
|
+
try { frozenNotes = readdirSync(new URL('docs/release-notes/', HERE)).filter((n) => n.endsWith('.md')) } catch (e) { frozenNotes = [] }
|
|
584
|
+
if (!frozenNotes.length) findings.push('the frozen release-notes exemption is EMPTY or missing (docs/release-notes/*.md) — re-decide the scan scope explicitly, never widen it silently')
|
|
585
|
+
notes.push('docs/** inbox pairing: files=' + docFiles + ', forbid-hits=' + docHits + '; frozen docs/release-notes/*.md exempted=' + frozenNotes.length)
|
|
586
|
+
// The plan ALSO carried the pre-G1 re-send contract ("queued 不重发/永不重发") in two sketch lines while
|
|
587
|
+
// its own M2 row already said "未 ack ⇒ 仍可重投". NAMED phrase only, scoped to the plan file, so a
|
|
588
|
+
// sentence that legitimately quotes or forbids the old wording cannot trip it.
|
|
589
|
+
const QUEUED_FORBID = /queued\s*(永不|不)重发/
|
|
590
|
+
if (QUEUED_FORBID.test(planDoc)) findings.push('实现方案.md still states the pre-G1 re-send contract (queued 不重发/永不重发): unacked mail is re-deliverable (F2/G1)')
|
|
591
|
+
notes.push('实现方案.md queued-no-resend phrasing: hits=' + (QUEUED_FORBID.test(planDoc) ? 1 : 0))
|
|
592
|
+
}
|
|
593
|
+
|
|
328
594
|
// The plan's philosophy is enforced by concrete gates; assert the load-bearing ones
|
|
329
595
|
// still exist so a future edit cannot quietly drop a guard.
|
|
330
596
|
const GATES = [
|
|
@@ -336,24 +602,36 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
|
|
|
336
602
|
['assignment objections are broadcast', 'if (p.reject_assign && typeof p.reject_assign === \'object\' && params.memberMayRejectAssign)'],
|
|
337
603
|
['the academician has no extra vote weight', 'const E = voters().map((m) => m.id)'],
|
|
338
604
|
['members may reject an assignment', "memberMayRejectAssign: true,"],
|
|
339
|
-
['the charter states the progress definition', "' · Progress
|
|
605
|
+
['the charter states the progress definition', "' · Members/<你>/Progress/progress.md —— **你的研究日志**(叙述体,可追加)。'"],
|
|
340
606
|
['the charter describes the academician as organizer', "' 【四、你的组织职责与边界(院士)】'"],
|
|
341
607
|
['the framework never assigns on its own', "agenda: '本所较长时间没有新进展。请你们自行讨论:现在最该推进的是什么?谁来做?是否需要发起验证?'"],
|
|
342
608
|
|
|
343
609
|
// ── PROMPT / INTERACTION CORRECTNESS GATES ─────────────────────────────
|
|
610
|
+
// F1 (README two-case resume claim): the two code anchors the prose cites.
|
|
611
|
+
['the founding round is computed from the stored count (never reset)', 'const startRound = (rounds.get(member.id) || 0) + 1'],
|
|
612
|
+
['the founding round is applied only AFTER a successful start', 'rounds.set(member.id, startRound)'],
|
|
344
613
|
// The 2026-09 field test shipped a framework whose every member brief named the WRONG
|
|
345
614
|
// member. These gates keep the structural fixes in place, and the companion
|
|
346
615
|
// prompt-v5-integrity.test.mjs asserts the TEXT those fixes produce.
|
|
347
|
-
['the status block takes the member it describes', 'function briefBlock(member) {'],
|
|
616
|
+
['the status block takes the member it describes', 'function briefBlock(member, roundNo) {'],
|
|
348
617
|
['an unknown identity fails loudly instead of being guessed', "throw v5err('V5_INTERNAL', 'briefBlock: a member is required"],
|
|
349
|
-
['the status block is built from that member, never a global', 'function stateBlock(member) {\n return briefBlock(member)\n }'],
|
|
618
|
+
['the status block is built from that member, never a global', 'function stateBlock(member, roundNo) {\n return briefBlock(member, roundNo)\n }'],
|
|
619
|
+
['the founding prompt is told the round it is starting (never a stored 0)', 'const prompt = initialPrompt(member, initialTask, mode, startRound)'],
|
|
350
620
|
['the joiner is committed to the roster BEFORE its brief is built', "member.phase = 'active'\n member.childId = ''\n await putMember(member)"],
|
|
351
621
|
['the charter is frozen at hire and reused on resume', 'const persona = member.persona || memberPersona(member)'],
|
|
352
622
|
['a rebuilt session is framed as a rebuild', "const resume = mode === 'resume'"],
|
|
353
623
|
['leadership text follows the live roster', 'function academicianId() {'],
|
|
354
624
|
['no charter invents a leader when there is none', "本所当前**没有在册院士**"],
|
|
355
625
|
['the office resolves as the office, never as a guessed member', "try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }"],
|
|
356
|
-
['the mailbox is acked
|
|
626
|
+
['the mailbox is acked only AFTER the wake actually succeeded', "if (ok && prompt.pending.length) await ackPending(prompt.pending)"],
|
|
627
|
+
// F2: the three REAL mailbox invariants, each at its own source anchor. `inFlightMessages` is a DSH
|
|
628
|
+
// mailbox internal that v5 does NOT have (dedupe is `delivered` + the per-round injection marks), so
|
|
629
|
+
// no gate may reference it.
|
|
630
|
+
['the acknowledgement is gated by the wake result AND a non-empty pending list', 'const ok = await wakeMember(member, prompt.text, kind)'],
|
|
631
|
+
['the delivered ledger is CAPPED (oldest evicted, never unbounded)', 'const capped = delivered.length > DELIVERED_CAP ? delivered.slice(delivered.length - DELIVERED_CAP) : delivered'],
|
|
632
|
+
['a prompt prepends only messages not already prepended in this round', 'const fresh = pending.filter((p) => !seen.has(p.id))'],
|
|
633
|
+
['the per-round injection mark is cleared with every concluded attempt', 'inboxInjected.delete(member.id)'],
|
|
634
|
+
['a prepended inbox suppresses the base block (exactly one inbox section per prompt)', "const pending = inboxSuppressed.has(member.id) ? [] : pendingFor(member.id)"],
|
|
357
635
|
['framework feedback has its own sender (never a self-message)', "return await say('framework', { to: memberId, kind: 'notice', text: String(text) })"],
|
|
358
636
|
['an assignment is framed by its true origin', "if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text"],
|
|
359
637
|
['a nudge is framed as supervision, not as an assignment', "to, kind: 'nudge',"],
|
|
@@ -388,6 +666,7 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
|
|
|
388
666
|
if (!raw.includes(needle)) findings.push('philosophy gate missing from the implementation: ' + label)
|
|
389
667
|
}
|
|
390
668
|
notes.push('philosophy gates checked: ' + GATES.length)
|
|
669
|
+
gateCount = GATES.length
|
|
391
670
|
|
|
392
671
|
// The prompt corpus is a SHIPPED deliverable, not a build artifact: a human must be able
|
|
393
672
|
// to read the exact text every member receives without decoding session logs.
|
|
@@ -411,7 +690,7 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
|
|
|
411
690
|
// never contain a wrong-identity brief.
|
|
412
691
|
try {
|
|
413
692
|
const corpusPath = new URL('prompt-corpus-v5/prompt-corpus-v5.json', REPO_ROOT)
|
|
414
|
-
const c = JSON.parse(
|
|
693
|
+
const c = JSON.parse(readRaw('prompt-corpus-v5/prompt-corpus-v5.json'))
|
|
415
694
|
const kinds = new Set((c.prompts || []).map(p => p.kind))
|
|
416
695
|
const need = ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
|
|
417
696
|
'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
|
|
@@ -440,7 +719,7 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
|
|
|
440
719
|
// it is the only prompt the MAIN agent receives, and no e2e suite loads the YAML, so a
|
|
441
720
|
// human reviewer needs the corpus on disk (generated by audit-persona-surface.test.mjs).
|
|
442
721
|
try {
|
|
443
|
-
const pc = JSON.parse(
|
|
722
|
+
const pc = JSON.parse(readRaw('prompt-corpus-persona/persona-corpus.json'))
|
|
444
723
|
for (const d of ['vibe-math-v2', 'vibe-math-v3', 'vibe-math-v4', 'vibe-math-v5']) {
|
|
445
724
|
if (!(pc.presets || []).some((p) => p.preset === d)) findings.push('the persona corpus is missing preset ' + d)
|
|
446
725
|
}
|
|
@@ -495,6 +774,14 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
|
|
|
495
774
|
}
|
|
496
775
|
|
|
497
776
|
// ---- report ------------------------------------------------------------
|
|
777
|
+
// `--json` is the machine-readable contract the `--self-probe` children parse (same shape as
|
|
778
|
+
// audit-prompt-invariants.mjs): { gates, failed, findings, notes }. It is emitted BEFORE the human
|
|
779
|
+
// output so a child run's stdout is parseable in one piece.
|
|
780
|
+
if (process.argv.includes('--json')) {
|
|
781
|
+
console.log(JSON.stringify({ gates: gateCount, failed: findings.length, findings, notes }))
|
|
782
|
+
for (const f of findings) console.error(' FINDING: ' + f)
|
|
783
|
+
process.exit(findings.length ? 1 : 0)
|
|
784
|
+
}
|
|
498
785
|
console.log('-- V5 integrity audit --')
|
|
499
786
|
for (const n of notes) console.log(' note: ' + n)
|
|
500
787
|
console.log('')
|