dsh-vibe-math 2.7.1 → 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 +682 -690
- package/README.md +19 -27
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +744 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +588 -588
- package/docs/math-computation.md +194 -163
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +49 -0
- package/docs/release-notes/README.md +48 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
- 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 +126 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +157 -154
- package/installer.js +27 -17
- package/package.json +55 -3
- 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 +305 -113
- package/prompt-corpus-v2/formal-verify-v2.md +2399 -279
- package/prompt-corpus-v3/formal-verify-v3.json +134 -110
- package/prompt-corpus-v3/formal-verify-v3.md +409 -141
- 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 +513 -259
- 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 +107 -9
- 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 +6 -4
- 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 +15 -8
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +566 -12
- package/tests/formal-verify-v2.test.mjs +91 -5
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +85 -6
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +628 -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 +396 -2
- 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 +229 -29
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/vibe-math-v2.js +458 -119
- 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 +229 -29
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/vibe-math-v3.js +482 -139
- package/vibe-math-v4/agent.cordis.yml +44 -8
- package/vibe-math-v4/math-computation.js +229 -29
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/vibe-math-v4.js +245 -52
- 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 +229 -29
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1307 -243
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1655
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -1,4 +1,96 @@
|
|
|
1
1
|
// ============================================================
|
|
2
|
+
// Defect class: F-6 (durability surfacing). A swallowed durable write / index rebuild / compaction
|
|
3
|
+
// failure must be NAMED once, with its consequence, instead of disappearing silently.
|
|
4
|
+
// Run: node tests/formal-verify-v4.test.mjs
|
|
5
|
+
//
|
|
6
|
+
// RECORDED BOUNDARY - all four product warnings EXIST (commit d02bdbf: warnedWriteFormalTodo,
|
|
7
|
+
// warnedDeferFormalTodo, warnedIndexRebuild, warnedCompaction; each once-per-process and each stating
|
|
8
|
+
// its consequence) but are NOT captured automatically by this suite. NOT REPRODUCED != NOT
|
|
9
|
+
// IMPLEMENTED: the reasons below are measured on the mock host, not assumed.
|
|
10
|
+
// site 1 closeVerify - guarded by `if(formalOn() && formalTodos.some(id===target))` (unreachable
|
|
11
|
+
// in off mode); the close is finalised by finalizeVerify's ASYNC chain
|
|
12
|
+
// AFTER proposeAndVote() returns, so a deterministic assertion would need
|
|
13
|
+
// a product-side fault hook (rejected: it would become a new
|
|
14
|
+
// agent-settable parameter) or dedicated verify orchestration.
|
|
15
|
+
// site 2 deferForFormal - KNOWN COVERAGE HOLE (measured). Payload-anchored injection (fail the
|
|
16
|
+
// Formal/TODO.md write whose payload mentions the target) is CONSUMED by an
|
|
17
|
+
// earlier TODO write in require mode (recording the proposition also writes
|
|
18
|
+
// that file and names the target), so the induced failure lands OUTSIDE
|
|
19
|
+
// deferForFormal's try: the run is derailed (Formal/TODO.md never created,
|
|
20
|
+
// section 9's own TODO assertions fail) and NO warning is emitted
|
|
21
|
+
// (`matched=0 of 0`). Anchoring by State/formal.json as a precursor was also
|
|
22
|
+
// measured imprecise (armed=9, failed=1, no warning). The warning itself
|
|
23
|
+
// exists and is exercised by the F-2-style unit surface at
|
|
24
|
+
// tests/audit-f1-compact-fix.test.mjs; only the in-suite capture is missing.
|
|
25
|
+
// site 2 (old note) - same async-finalisation problem (require mode only).
|
|
26
|
+
// site 3 settleLeanJob - the injected index-write failure is consumed by whichever
|
|
27
|
+
// rebuildLeanLibIndexes() caller fires first (measured: armed=3,
|
|
28
|
+
// failed=1, warning not emitted), so the mock cannot discriminate the
|
|
29
|
+
// caller that this site guards. Caller-discriminating injection was
|
|
30
|
+
// attempted (run every other rebuild caller first, then arm): the settle
|
|
31
|
+
// path's index write was still never reached (measured failed=0) -
|
|
32
|
+
// caller-discriminating injection attempted, still not reached.
|
|
33
|
+
// AUTHOR-MEASURED, NOT INDEPENDENTLY REPRODUCED: R17 could not reproduce the
|
|
34
|
+
// `armed=3 failed=1` / `failed=0` instrumentation numbers (it marked them
|
|
35
|
+
// still-unsure). It DID verify the STRUCTURE this boundary rests on: the
|
|
36
|
+
// single-line best-effort wrapper, all six callers (settleLeanJob,
|
|
37
|
+
// writeLeanLibIndexesSafe, the three leanArchive paths, the lean_lib tool)
|
|
38
|
+
// now covered by the one-shot naming inside rebuildLeanLibIndexes, and every
|
|
39
|
+
// later rebuild rewriting the index => self-healing. The boundary stands on
|
|
40
|
+
// that structural evidence; the two raw numbers above are author-measured only.
|
|
41
|
+
// site 4 realCompact - SURFACED (section N15): the mock resolves the child agent and exposes a
|
|
42
|
+
// THROWING compaction service only under an injection switch, so a completed
|
|
43
|
+
// resident turn reaches realCompact and its catch is asserted to NAME the
|
|
44
|
+
// failure exactly once with its consequence (mutant: the warning guard
|
|
45
|
+
// disabled => `460/2`, red on N15/4).
|
|
46
|
+
// site 4 (old note) - realCompact returns early unless liveAgentOf(r.childId) resolves; this
|
|
47
|
+
// mock's agents.get() maps only sess-A, so a resident turn never reaches
|
|
48
|
+
// the compaction call (probe: the service is never invoked).
|
|
49
|
+
// F1 COVERAGE HOLE (window) - measured, not assumed. The consensus-membership fix (predicate + roster
|
|
50
|
+
// guard) is covered by the SOURCE-LEVEL guards in section N13 (both single-site mutants redden by name:
|
|
51
|
+
// count predicate => 457/2; `_inRoster=true` => 458/1). The behavioural WINDOW the v2/v3 owner proposed
|
|
52
|
+
// (a removed, still-in-flight frozen voter's late ballot) was driven inside section D4 and measured
|
|
53
|
+
// unobservable through the public API: fixed => `before=0/2 after=0/1 card=false`; guard deleted (M2) =>
|
|
54
|
+
// the SAME `0/1`; count predicate (M1) => the SAME `0/1`. The late ballot therefore never reaches the
|
|
55
|
+
// recording site in this composition, so a windowed assertion could not be reddened by M2 and would be
|
|
56
|
+
// vacuous - it was NOT landed. Regression coverage for F1 is N13 (source level).
|
|
57
|
+
|
|
58
|
+
// F-6 (:786) FIXED, not merely bounded: a failed build-context comparison now records
|
|
59
|
+
// `contextChanged=null` (unknown != unchanged) and emits a one-time named warning (warnedContextProbe,
|
|
60
|
+
// wording states the consequence). It carries NO automated assertion here: the branch is UNREACHABLE
|
|
61
|
+
// from this host mock - `leanBuildContext()` is non-throwing by construction (its only host call,
|
|
62
|
+
// resolveExecutable, is wrapped and degrades to the configured name) and every other step is a
|
|
63
|
+
// defensive coercion over agent-settable params (leanSearchPaths/leanArgs are Array.isArray-guarded and
|
|
64
|
+
// String()-coerced). A mutant without an assertion would prove nothing, so none is claimed.
|
|
65
|
+
// Consumer check (repo-wide grep of `contextChanged`): only 4 references, all in v4's settle paths -
|
|
66
|
+
// the field is write-only for the JSON reader, so `null` cannot be mis-read as `false`.
|
|
67
|
+
// Sites 3/4 carry test-side injection switches in the F-6 work branch (_oneoff/tw-patch.mjs) which
|
|
68
|
+
// can be re-landed together with a host-side caller discriminator.
|
|
69
|
+
// ---------------------------------------------------------------
|
|
70
|
+
// SHIPPED ASSERTION RULES (the rules behind N13-N17; the authoring ledger lives outside the package,
|
|
71
|
+
// so the essentials ship HERE, next to the guards they govern).
|
|
72
|
+
// (A) LIVENESS BEFORE SILENCE (governs N17/F-5, especially case 1c): every "no warning / no write /
|
|
73
|
+
// no re-anchor" assertion must ALSO prove that the code under test RAN, and the proof must be
|
|
74
|
+
// taken at the step CLOSEST to that branch. Measured counter-examples on this mock: "the policy
|
|
75
|
+
// service was queried" is NOT proof (`sandboxPolicyOf()` is read by BOTH workspaceRoot() and
|
|
76
|
+
// getPolicy()), and "the write succeeded" is NOT proof either (with an unknown cwd the same call
|
|
77
|
+
// returns ok:false for an UNRELATED path reason). The workable instrument is the resolve() call
|
|
78
|
+
// count - `LIVENESS: resolve calls=11` - because resolve({session}) runs immediately before
|
|
79
|
+
// fenceRootDriftNote(). Mutant "unknown cwd => warn" reddens `F-5/1c ... stays SILENT (drift=1)`.
|
|
80
|
+
// (B) A SYMBOL'S EXISTENCE IS NOT ITS USE (governs the F-3/F-4c verdicts recorded in-product at
|
|
81
|
+
// vibe-math-v4.js:3553): a count is a LEAD, never a verdict - the verdict must be read at the
|
|
82
|
+
// SEMANTIC UNIT. Measured: `registerTool(` appears 36 times but there is exactly ONE registration
|
|
83
|
+
// unit (the ctx.effect inside the registerTool helper), so no call site needs its own wrapper;
|
|
84
|
+
// and a failed mkdir is named ONCE INSIDE ensureDirs (warnedMkdir), which covers every caller -
|
|
85
|
+
// including the bare `await ensureDirs()` - so no caller-side return check is required.
|
|
86
|
+
// (C) G-6 HOST CONCURRENCY CAP, assertion level: when the host's live-child cap refuses a spawn, the
|
|
87
|
+
// assertion must require the cap to be NAMED together with the knob that raises it, the durable
|
|
88
|
+
// marker to survive (hostChildLimit / failReason), and the freed capacity to be spent on exactly
|
|
89
|
+
// ONE deferred member. Field-level semantics: docs/status-report-fields.md (pendingSpawns).
|
|
90
|
+
// (D) A2 WAKE/WATCHDOG, assertion level: ack only AFTER a successful delivery, and advance the round
|
|
91
|
+
// counter only AFTER a successful send - asserted under INJECTED send failures, never on the
|
|
92
|
+
// happy path (where build == send == success hides the defect). Shipped families: v5 G1/G2.
|
|
93
|
+
// ---------------------------------------------------------------
|
|
2
94
|
// V4 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
|
|
3
95
|
//
|
|
4
96
|
// Asserts the whole contract of the `formalVerify` knob for the v4 preset:
|
|
@@ -43,6 +135,7 @@ function pluginUrl() {
|
|
|
43
135
|
return pathToFileURL(fileURLToPath(new URL('file:///' + s.replace(/\\/g, '/'))))
|
|
44
136
|
}
|
|
45
137
|
const PLUGIN = pluginUrl()
|
|
138
|
+
function PLUGIN_SRC() { return readFileSync(process.env.V4_PLUGIN ? String(process.env.V4_PLUGIN) : fileURLToPath(new URL('../vibe-math-v4/vibe-math-v4.js', import.meta.url)), 'utf8') } // protocol 搂9: ONE seam-aware resolution for source-text guards
|
|
46
139
|
// docs §10 item 10: the Lean prompt corpus is shipped under prompt-corpus-v4/ so a HUMAN can read
|
|
47
140
|
// the exact text the framework sends. V4_CORPUS_DIR overrides the destination (same convention v3
|
|
48
141
|
// and v5 use for V3_CORPUS_DIR / V5_CORPUS_DIR).
|
|
@@ -70,6 +163,13 @@ let subprocessAvailable = true
|
|
|
70
163
|
// Used by §13 to prove the withdrawal is not a best-effort delete: the framework must confirm the
|
|
71
164
|
// file is gone through the fs service and fall back to overwriting it with a withdrawal notice.
|
|
72
165
|
let shellDeletesFiles = true
|
|
166
|
+
let shellMkdirExit = 0
|
|
167
|
+
let fsWriteFailPred = null // F-6/N18: (targetKey) => boolean; the first matching write FAILS (payload/path anchored)
|
|
168
|
+
let fsWriteFailOnce = false
|
|
169
|
+
let sandboxDriftRoot = null // F-5: when set, the mock policy resolves THIS root (drift/containment harness)
|
|
170
|
+
let sandboxNoRoot = false // F-5: when set, the mock policy exposes NO interpretable root (no-op path)
|
|
171
|
+
let sandboxUnknownCwd = false // F-5/1c: the captured root agent has NO cwd (unknown => `.` guard)
|
|
172
|
+
let compactionFails = false // F-6 site 4: expose a THROWING compaction service + resolve child agents
|
|
73
173
|
const leanRuns = [] // every spawn the framework made, for cwd/argv assertions
|
|
74
174
|
const terminated = [] // files whose handle the framework actively terminate()d (docs §7)
|
|
75
175
|
const shellCalls = [] // every platform-shell script (mkdir at mount, Remove-Item on defect)
|
|
@@ -95,7 +195,7 @@ function makeSubprocess() {
|
|
|
95
195
|
if (shellDeletesFiles && /Remove-Item/.test(script) && m) rmSync(m[1].replace(/''/g, "'"), { force: true })
|
|
96
196
|
if (shellDeletesFiles && /^rm -f /.test(script)) for (const q of script.slice(6).match(/'[^']*'/g) || []) rmSync(q.slice(1, -1), { force: true })
|
|
97
197
|
return {
|
|
98
|
-
done: Promise.resolve({ exitCode: 0, signal: null }),
|
|
198
|
+
done: Promise.resolve({ exitCode: (/New-Item|^mkdir -p /.test(script) ? shellMkdirExit : 0), signal: null }),
|
|
99
199
|
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
100
200
|
terminate() {},
|
|
101
201
|
}
|
|
@@ -135,9 +235,21 @@ function makeHost() {
|
|
|
135
235
|
const WS = mkdtempSync(join(tmpdir(), 'vibe-v4-lean-'))
|
|
136
236
|
const listeners = {}, toolRegs = [], spawns = [], followups = [], cmdRegs = []
|
|
137
237
|
const subprocess = makeSubprocess()
|
|
238
|
+
let sandboxPolicyQueries = 0 // F-5/1c: how many times the policy service was queried
|
|
239
|
+
let sandboxResolveCalls = 0 // F-5/1c: resolve({session}) calls = fenceRootDriftNote invocations
|
|
138
240
|
let ROOT
|
|
139
241
|
const ctx = {
|
|
140
|
-
get(name) {
|
|
242
|
+
get(name) {
|
|
243
|
+
if (name === 'subprocess' && subprocessAvailable) return subprocess
|
|
244
|
+
if (name === 'sandboxPolicy') sandboxPolicyQueries += 1 // F-5/1c liveness: the policy path really ran
|
|
245
|
+
if (name === 'sandboxPolicy' && sandboxUnknownCwd) return { resolve: () => { sandboxResolveCalls += 1; return { workspaceRoot: WS + '/fenced-elsewhere' } } } // NO top-level root: workspaceRoot() falls back to '.'
|
|
246
|
+
if (name === 'sandboxPolicy' && (sandboxDriftRoot || sandboxNoRoot)) return sandboxNoRoot ? { allow: ['x'], deny: [], resolve: () => ({ allow: ['x'], deny: [] }) } : { workspaceRoot: sandboxDriftRoot, resolve: () => ({ workspaceRoot: sandboxDriftRoot }) }
|
|
247
|
+
if (name === 'compaction' && compactionFails) return {
|
|
248
|
+
async compactIfNeeded() { throw new Error('COMPACTION_FAIL (injected)') },
|
|
249
|
+
async compactNow() { throw new Error('COMPACTION_FAIL (injected)') },
|
|
250
|
+
}
|
|
251
|
+
return undefined
|
|
252
|
+
},
|
|
141
253
|
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
142
254
|
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
143
255
|
logger: { info() {}, warn() {}, error() {} },
|
|
@@ -151,21 +263,28 @@ function makeHost() {
|
|
|
151
263
|
async sendMessage(parent, childId, blocks) { followups.push({ childId, blocks }) },
|
|
152
264
|
interrupt() {},
|
|
153
265
|
},
|
|
154
|
-
agents: { roots() { return [] }, get(id) {
|
|
266
|
+
agents: { roots() { return [] }, get(id) {
|
|
267
|
+
if (compactionFails && id && id !== 'sess-A') return { id, session: { id, header: { cwd: WS, parentSession: 'sess-A' } } }
|
|
268
|
+
return id === 'sess-A' ? ROOT : undefined
|
|
269
|
+
} },
|
|
155
270
|
fs: {
|
|
156
271
|
async resolve(rel, opts) { const b = (opts && opts.cwd) || WS; const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/')); return { targetKey: p, displayPath: p } },
|
|
157
272
|
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
158
273
|
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
159
|
-
async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
274
|
+
async writeText(t, c) { if (fsWriteFailOnce && fsWriteFailPred && fsWriteFailPred(String(t.targetKey), String(c))) { fsWriteFailOnce = false; throw new Error('FS_WRITE_FAIL (injected): ' + t.targetKey) } mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
160
275
|
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
161
276
|
},
|
|
162
277
|
}
|
|
163
278
|
ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session: { id: 'sess-A', header: { cwd: WS, parentSession: undefined } }, followup() {}, ctx: undefined }
|
|
279
|
+
if (sandboxUnknownCwd) ROOT.session.header.cwd = '' // F-5/1c: unknown session cwd
|
|
164
280
|
const projectRoot = join(WS, 'VibeMath', 'Projects', 'default')
|
|
165
281
|
const vibeRoot = join(WS, 'VibeMath')
|
|
166
282
|
let consumed = 0
|
|
167
283
|
const h = {
|
|
168
284
|
WS, ctx, toolRegs, spawns, followups, ROOT, projectRoot, vibeRoot, cmdRegs,
|
|
285
|
+
policyQueries: () => sandboxPolicyQueries,
|
|
286
|
+
resolveCalls: () => sandboxResolveCalls,
|
|
287
|
+
resetResolveCalls: () => { sandboxResolveCalls = 0 },
|
|
169
288
|
/** Drive the registered `/v4` slash command — the OTHER parameter surface the spec §B names. */
|
|
170
289
|
async cmd(rawInput) { const c = cmdRegs.find(x => x.name === 'v4'); if (!c) throw new Error('no /v4 command'); return await c.handler({ agent: ROOT, rawInput }) },
|
|
171
290
|
async callTool(n, a, agent) {
|
|
@@ -270,6 +389,24 @@ async function proposeAndVote(h, target, opts = {}) {
|
|
|
270
389
|
// The verification may only have been QUEUED by the proposal round; finish it.
|
|
271
390
|
return await settleVerify(h, opts.plan || (() => (opts.verdict === undefined ? 1 : opts.verdict)))
|
|
272
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Start a verification and KEEP IT IN FLIGHT (the first half of `proposeAndVote`): record a card, have
|
|
394
|
+
* r-1 propose verifying it, and stop driving as soon as the verify view is ACTIVE. Note the trap:
|
|
395
|
+
* `verifyInProgress` is already true while the verification is merely QUEUED, so `consensus.kind`
|
|
396
|
+
* is the only reliable "it is really running" signal.
|
|
397
|
+
*/
|
|
398
|
+
async function startVerifyOnly(h, target) {
|
|
399
|
+
const card = { id: target, title: target, statement: '待验证对象 ' + target, prob: 0.8, value: 0.6, motivation: 'm' }
|
|
400
|
+
await h.callTool('vibe_v4_record_proposition', card, h.resAgent(h.childOf('r-1')))
|
|
401
|
+
const before = h.followups.length
|
|
402
|
+
await h.callTool('vibe_v4_message', { to: 'r-1', content: '请处理本轮工作。' })
|
|
403
|
+
for (let i = 0; i < 300 && h.followups.length === before; i++) await sleep(10)
|
|
404
|
+
await drive(h, () => ({ summary: '提议验证 ' + target + '。', solved: false, propose_verify: target, contextPct: 20 }), async () => {
|
|
405
|
+
const s = await h.callTool('vibe_v4_status', {})
|
|
406
|
+
return !!(s.consensus && s.consensus.kind === 'verify')
|
|
407
|
+
})
|
|
408
|
+
return await h.callTool('vibe_v4_status', {})
|
|
409
|
+
}
|
|
273
410
|
/** Wake exactly the given resident with an ordinary work round and return that wake. */
|
|
274
411
|
async function workWake(h, rId) {
|
|
275
412
|
const before = h.followups.length
|
|
@@ -572,8 +709,8 @@ assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under Vibe
|
|
|
572
709
|
assert(/ZMod5/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the new definition')
|
|
573
710
|
assert(/sq_odd/.test(readIf(join(provedPath, 'Index.md'))), 'Proved/Index.md lists the new lemma')
|
|
574
711
|
// `from` copies an existing workspace .lean file instead of inline content
|
|
575
|
-
writeFileSync(join(D.projectRoot, 'Formal', '
|
|
576
|
-
const fromRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Copied', from: 'Formal/
|
|
712
|
+
writeFileSync(join(D.projectRoot, 'Formal', 'PLUGIN_SRC().lean'), 'def copied := 7\n', 'utf8')
|
|
713
|
+
const fromRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Copied', from: 'Formal/PLUGIN_SRC().lean' }, r1)
|
|
577
714
|
assert(fromRes.ok === true && readIf(join(libPath, 'Copied.lean')) === 'def copied := 7\n', "kind='def' accepts from=<existing .lean file>")
|
|
578
715
|
const fromEsc = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Evil', from: '../../../../etc/passwd' }, r1)
|
|
579
716
|
assert(fromEsc.ok === false && fromEsc.code === 'V4_INVALID_ARGUMENT', "★ kind='def' refuses a `from` outside the VibeMath root")
|
|
@@ -1158,7 +1295,7 @@ const K = await establish()
|
|
|
1158
1295
|
const later = byLabel('meeting/later speaker (others relayed)')
|
|
1159
1296
|
assert(!!first && !!later, '★ the corpus carries the MEETING prompt (two perspectives: no prior speech / others already relayed)')
|
|
1160
1297
|
assert(/meeting is in progress/.test(first.prompt) && /meeting is in progress/.test(later.prompt), 'the meeting entries are built by the real meeting prompt builder')
|
|
1161
|
-
assert(
|
|
1298
|
+
assert(/并行独立发言/.test(first.prompt), 'the first-speaker entry states the parallel-statements semantics (nobody has spoken yet)')
|
|
1162
1299
|
assert(/已有发言(他人 input,已转发给你)/.test(later.prompt) && /我建议先验证 p-corpus/.test(later.prompt), '★ the later-speaker entry carries a teammate contribution VERBATIM (the group-chat relay is reviewable)')
|
|
1163
1300
|
assert(/\[r-1\]/.test(later.prompt) && !/\[r-2\]/.test(later.prompt), 'the relayed-speech list excludes the reader and names the real sender id')
|
|
1164
1301
|
assert(/voteSolved/.test(first.prompt) && /停止表决必须是绝对票/.test(first.prompt), '★ the meeting prompt states the stop-vote contract the code enforces (every speaker must answer true)')
|
|
@@ -1306,12 +1443,22 @@ section('20 crash recovery: queued re-driven, changed content never verified, se
|
|
|
1306
1443
|
await N.callTool('vibe_v4_start', { problem: '恢复扫描', residentCount: 1 })
|
|
1307
1444
|
const a = await N.callTool('vibe_v4_lean_job', { jobId: 'p-rec-aaaaaaaaaaaa' })
|
|
1308
1445
|
const b = await N.callTool('vibe_v4_lean_job', { jobId: 'p-stale-bbbbbbbbbbbb' })
|
|
1309
|
-
assert(a.state === 'queued' || a.state === 'running' || a.state === 'settled', '
|
|
1446
|
+
assert(a.attempts >= 2 && (a.state === 'queued' || a.state === 'running' || a.state === 'settled'), '* a hash-matching interrupted job is RE-DRIVEN (never adopted): attempts 1 -> ' + a.attempts + ', state=' + a.state)
|
|
1447
|
+
// (cold-state liveness, code path recoverLeanJobs:911): with NO explicit followup the armed heartbeat
|
|
1448
|
+
// must drain the re-queued job on a later tick - poll with the diagnostic triple in the message.
|
|
1449
|
+
{
|
|
1450
|
+
const t0 = Date.now()
|
|
1451
|
+
let cur = a, tries = 0
|
|
1452
|
+
while (tries < 60 && cur.state === 'queued') { tries++; await new Promise((r) => setTimeout(r, 100)); cur = await N.callTool('vibe_v4_lean_job', { jobId: 'p-rec-aaaaaaaaaaaa' }) }
|
|
1453
|
+
assert(cur.state !== 'queued', '* the next tick DRAINS the re-queued job without any explicit followup (state=' + cur.state + ', window=6000ms, waited=' + (Date.now() - t0) + 'ms, attempts=' + tries + ')')
|
|
1454
|
+
const stLive = await N.callTool('vibe_v4_status', {})
|
|
1455
|
+
assert(stLive.running === true, '* the institute clock keeps running after a stale-only recovery: recoverLeanJobs:911 arms ONE idle pass even when nothing is drainable (documented behaviour - if a future change stops arming on stale-only, THIS reddens)')
|
|
1456
|
+
}
|
|
1310
1457
|
assert(b.state === 'interrupted', '★ a `running` job whose file CHANGED is interrupted, not verified (state=' + b.state + ')')
|
|
1311
1458
|
const st = await N.callTool('vibe_v4_status', {})
|
|
1312
1459
|
assert(!st.formal.passed.includes('p-stale'), '★ the changed-content job never reaches `passed`')
|
|
1313
1460
|
assert(!st.formal.passed.includes('p-done') && !existsSync(join(N.projectRoot, 'Verified', 'Lean', 'p-done.lean')), '★ a fresh run does NOT adopt a previous run’s `settled` record (no stale passed, no minted proof)')
|
|
1314
|
-
assert(N.followups.
|
|
1461
|
+
assert(N.followups.length === 0, '* the stale-job recovery path wakes NO new resident (followups=' + N.followups.length + ') - the wake decision belongs to the watchdog; this line is falsifiable, so a future change of that contract reddens HERE')
|
|
1315
1462
|
}
|
|
1316
1463
|
|
|
1317
1464
|
// ===============================================================
|
|
@@ -1480,7 +1627,479 @@ section('18 every daily-line injection site (coreRules/normal/heartbeat) obeys t
|
|
|
1480
1627
|
}
|
|
1481
1628
|
|
|
1482
1629
|
// ===============================================================
|
|
1630
|
+
// 22. cross-preset audit P0 (v4 station): the member-visible library text must state that the
|
|
1631
|
+
// residents' file tools resolve against the SESSION CWD, so the listed sub-paths must be used
|
|
1632
|
+
// with the ABSOLUTE prefix (and computation artifacts via receipt.scriptAbs / receipt.cwd).
|
|
1633
|
+
// ===============================================================
|
|
1634
|
+
section('22 audit P0: member-facing paths carry the session-cwd / absolute-prefix instruction')
|
|
1635
|
+
{
|
|
1636
|
+
const N = await establish()
|
|
1637
|
+
const texts = [await N.prompts('brainstorm', 'r-1'), await N.prompts('coreRules'), await N.prompts('normal', 'r-1')]
|
|
1638
|
+
const ok = texts.filter((t) => t.indexOf('会话 cwd') !== -1 && t.indexOf('绝对前缀') !== -1)
|
|
1639
|
+
assert(ok.length >= 1, '★ the member-visible library text says file tools resolve against the SESSION CWD and that listed paths need the ABSOLUTE prefix (found in ' + ok.length + '/3 variants)')
|
|
1640
|
+
assert(ok.some((t) => t.indexOf('receipt.scriptAbs') !== -1 || t.indexOf('receipt.cwd') !== -1), '★ …and names the absolute receipt field (receipt.scriptAbs / receipt.cwd) for computation artifacts')
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// ===============================================================
|
|
1644
|
+
// 23. cross-preset audit P1 (v4 stations): a member-facing not-found must be diagnosable — code +
|
|
1645
|
+
// live roster status + an actionable next step — including `removeMember` (previously a bare
|
|
1646
|
+
// {ok:false}) and the not-yet-started case.
|
|
1647
|
+
// ===============================================================
|
|
1648
|
+
section('23 audit P1: member-facing not-found carries code + status + next')
|
|
1649
|
+
{
|
|
1650
|
+
const N = await establish()
|
|
1651
|
+
const wrong = await N.callTool('vibe_v4_send_message', { to: 'r-404', content: 'hi' }, N.resAgent(N.childOf('r-1')))
|
|
1652
|
+
assert(wrong.ok === false && wrong.code === 'V4_NO_SUCH_RESIDENT' && !!wrong.next && !!wrong.next.tool, '★ wrong resident id ⇒ code V4_NO_SUCH_RESIDENT + next{tool} (not a bare message)')
|
|
1653
|
+
assert(wrong.status && wrong.status.residentCount >= 1 && wrong.status.running === true, '★ …plus LIVE roster status, so "wrong id" is distinguishable from "not started"')
|
|
1654
|
+
const kicked = await N.callTool('vibe_v4_remove_member', { id: 'r-404' })
|
|
1655
|
+
assert(kicked.ok === false && kicked.code === 'V4_NO_SUCH_RESIDENT' && !!kicked.next && kicked.next.tool === 'vibe_v4_remove_member', '★ vibe_v4_remove_member {id:r-404} ⇒ code + next naming that tool (was a bare {ok:false})')
|
|
1656
|
+
const fresh = await mount()
|
|
1657
|
+
const notStarted = await fresh.callTool('vibe_v4_publish_progress', { content: 'x' })
|
|
1658
|
+
assert(notStarted.ok === false && notStarted.code === 'V4_NO_SUCH_RESIDENT' && !!notStarted.next && !!notStarted.status && notStarted.status.running === false, '★ run not started ⇒ code + next + status.running=false')
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// ---------- D4 cross-view: mid-verify hire/fire must not split the views ----------
|
|
1662
|
+
section('D4 cross-view: one frozen participant set, seen identically by every view')
|
|
1663
|
+
{
|
|
1664
|
+
const h = await establish()
|
|
1665
|
+
const st = await startVerifyOnly(h, 'p-d4')
|
|
1666
|
+
assert(st.consensus && st.consensus.kind === 'verify', 'precondition: an ACTIVE verification for the cross-view check (' + JSON.stringify({ c: st.consensus && st.consensus.kind, ip: st.verifyInProgress }) + ')')
|
|
1667
|
+
const c0 = st.consensus
|
|
1668
|
+
const rp0 = await h.callTool('vibe_v4_report', {})
|
|
1669
|
+
const rv0 = rp0.verify
|
|
1670
|
+
assert(Array.isArray(st.frozenParticipants) && rv0, 'the frozen participant set and the report() verify view are both exposed')
|
|
1671
|
+
assert(c0.expected === st.frozenParticipants.length, '★ consensus.expected === the frozen participant set size')
|
|
1672
|
+
assert(Number(String(rv0.voted).split('/')[1]) === c0.expected, '★ the two views agree on the DENOMINATOR (status().consensus.expected vs report().verify k/N)')
|
|
1673
|
+
assert(rv0.rosterVersion === c0.rosterVersion && c0.rosterVersion === st.rosterVersion, '★ both views (and status) report the SAME rosterVersion')
|
|
1674
|
+
// HIRE mid-verify: the newcomer must NOT join the frozen set (that is what freezing means).
|
|
1675
|
+
const hire = await h.callTool('vibe_v4_add_member', { direction: 'D4 cross-view assertion' })
|
|
1676
|
+
const st2 = await h.callTool('vibe_v4_status', {})
|
|
1677
|
+
const rv2 = (await h.callTool('vibe_v4_report', {})).verify
|
|
1678
|
+
assert(st2.rosterVersion > c0.rosterVersion, 'hiring mid-verify bumps rosterVersion (' + c0.rosterVersion + ' -> ' + st2.rosterVersion + ')')
|
|
1679
|
+
assert(st2.consensus.expected === st2.frozenParticipants.length && st2.consensus.expected === c0.expected,
|
|
1680
|
+
'★ after a mid-verify HIRE the frozen set is UNCHANGED and both views still agree (newcomer did not join)')
|
|
1681
|
+
assert(typeof rv2.rosterVersion === 'number' && typeof st2.consensus.rosterVersion === 'number', 'non-vacuous: both frozen views EXPOSE a numeric rosterVersion (a relative === passes when both are absent)')
|
|
1682
|
+
assert(rv2.rosterVersion === st2.consensus.rosterVersion,
|
|
1683
|
+
'★ after the HIRE the two FROZEN views (status().consensus + report().verify) still report the SAME rosterVersion')
|
|
1684
|
+
assert(st2.rosterVersion > rv2.rosterVersion,
|
|
1685
|
+
'★ …while status().rosterVersion is the LIVE counter and has moved past the snapshot (staleness is visible)')
|
|
1686
|
+
// FIRE mid-verify: the frozen set is pruned so the in-flight verification can still conclude.
|
|
1687
|
+
if (hire && hire.ok && hire.id) await h.callTool('vibe_v4_remove_member', { id: hire.id })
|
|
1688
|
+
const st3 = await h.callTool('vibe_v4_status', {})
|
|
1689
|
+
const rv3 = (await h.callTool('vibe_v4_report', {})).verify
|
|
1690
|
+
assert(st3.consensus.expected === st3.frozenParticipants.length && Number(String(rv3.voted).split('/')[1]) === st3.consensus.expected && rv3.rosterVersion === st3.consensus.rosterVersion,
|
|
1691
|
+
'★ after a mid-verify FIRE the two views STILL agree (the frozen set was pruned together)')
|
|
1692
|
+
assert(st3.frozenParticipants.indexOf(hire && hire.id) === -1, '★ the removed member is not in the frozen set any more (pruned on removal)')
|
|
1693
|
+
// …and the verification really does conclude after that churn (T26/T31 contract).
|
|
1694
|
+
await settleVerify(h, () => 1)
|
|
1695
|
+
const st4 = await h.callTool('vibe_v4_status', {})
|
|
1696
|
+
assert(st4.verifyInProgress === false, '★ the mid-verify hire/fire did NOT prevent the verification from concluding')
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
// ===============================================================
|
|
1702
|
+
// 24. round A F2 (design-preserving): a HIRE never extends the frozen set, so the live counter may
|
|
1703
|
+
// drift ahead of it (that drift IS the staleness signal). A FIRE prunes the frozen set, so the
|
|
1704
|
+
// snapshot must be re-stamped — otherwise the same version would denote two different sets.
|
|
1705
|
+
// ===============================================================
|
|
1706
|
+
section('24 round A F2: hire keeps the snapshot stale-able; fire re-stamps it (version ≡ set)')
|
|
1707
|
+
{
|
|
1708
|
+
const N = await establish()
|
|
1709
|
+
await N.callTool('vibe_v4_meeting', { agenda: 'round A 快照一致性' })
|
|
1710
|
+
const s1 = await N.callTool('vibe_v4_status', {})
|
|
1711
|
+
assert(!!s1.frozen && Array.isArray(s1.frozen.participants) && s1.frozen.participants.length >= 1, '★ an active consensus exposes frozen{version,participants} (additive)')
|
|
1712
|
+
assert(s1.frozen.version === s1.rosterVersion, 'at freeze time the snapshot version equals the live counter')
|
|
1713
|
+
assert(typeof s1.consensus.rosterVersion === 'number' && Array.isArray(s1.consensus.participants) && s1.consensus.rosterVersion === s1.frozen.version, '\u2605 F3 (absolute first, unguarded): status().consensus EXPOSES rosterVersion + participants and it equals the frozen snapshot version')
|
|
1714
|
+
const v1 = s1.rosterVersion
|
|
1715
|
+
await N.callTool('vibe_v4_add_member', { direction: 'round A 新增成员' })
|
|
1716
|
+
const s2 = await N.callTool('vibe_v4_status', {})
|
|
1717
|
+
assert(s2.rosterVersion > v1, 'hiring bumps the numeric rosterVersion (monotone)')
|
|
1718
|
+
assert(s2.frozen.version === v1, '★ a HIRE leaves the frozen snapshot version untouched (its set did not change)')
|
|
1719
|
+
assert(s2.frozen.participants.length === s1.frozen.participants.length, '★ …and the frozen requirement set is unchanged (frozen means frozen)')
|
|
1720
|
+
assert(s2.rosterVersion > s2.frozen.version, '★ the live-vs-frozen drift remains the visible staleness signal')
|
|
1721
|
+
const removed = s1.frozen.participants[0]
|
|
1722
|
+
await N.callTool('vibe_v4_remove_member', { id: removed })
|
|
1723
|
+
const s3 = await N.callTool('vibe_v4_status', {})
|
|
1724
|
+
assert(s3.frozen.participants.indexOf(removed) === -1, '★ a FIRE prunes the frozen set (the wait is released)')
|
|
1725
|
+
assert(s3.frozen.version === s3.rosterVersion && s3.frozen.version > v1, '★ F2: the pruned set carries a NEW version — no window where a changed set keeps the old version')
|
|
1726
|
+
}
|
|
1727
|
+
// ===============================================================
|
|
1728
|
+
// ===============================================================
|
|
1729
|
+
// N4. F-PROMPT: a documented 'true no-op' must be asserted on the DELIVERED prompt, not on the
|
|
1730
|
+
// config path. Under formalVerify:'off' the verify prompt must carry NO formalization clause
|
|
1731
|
+
// and NO forward reference to a segment that is not delivered.
|
|
1732
|
+
// ===============================================================
|
|
1733
|
+
section('N4 off: the DELIVERED verify prompt has no formalization content or dangling pointer')
|
|
1734
|
+
{
|
|
1735
|
+
const P = await establish()
|
|
1736
|
+
const off = await P.prompts('verify', 'r-1', { target: 'p-fprompt', stage: 'independent' })
|
|
1737
|
+
assert(off.length > 100 && /\u9a8c\u8bc1/.test(off), '* the delivered verify prompt is non-trivial (non-vacuity: a missing/empty prompt must not pass the negations below)')
|
|
1738
|
+
assert(!/\u5f62\u5f0f\u5316\u6bb5/.test(off), '* the off-mode verify prompt contains no forward reference to the formalization segment (\u5f62\u5f0f\u5316\u6bb5)')
|
|
1739
|
+
assert(!/formal/.test(off), '* and no formalization content at all (no `formal` receipt clause) - the documented true no-op')
|
|
1740
|
+
await P.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
1741
|
+
const enc = await P.prompts('verify', 'r-1', { target: 'p-fprompt', stage: 'independent' })
|
|
1742
|
+
assert(/Lean|formal/.test(enc), '* under encourage the delivered prompt DOES carry the formalization block (the gate is a gate, not a deletion)')
|
|
1743
|
+
}
|
|
1744
|
+
// ===============================================================
|
|
1745
|
+
// N5. v4 path basis - CORRECTED CONTRACT (the earlier comment here pinned the OLD, WRONG shape:
|
|
1746
|
+
// it demanded a member root. v4's writers/readers live at the PROJECT ROOT: publishProgress writes
|
|
1747
|
+
// Progress/<rId>/progress.md, the record tools write Propos|Methods|Subproblems/<rId>/<id>.md, and
|
|
1748
|
+
// countArtifacts() scans exactly those bases under frameworkRoot(). A Members/<placeholder>/... path
|
|
1749
|
+
// is therefore a path nothing scans - the realistic documented example is base + '/Propos/r-1/p-1.md'.
|
|
1750
|
+
// (Named in the report as the "comment pinning old behaviour" case.)
|
|
1751
|
+
// ===============================================================
|
|
1752
|
+
section('N5 v4 member-facing paths are PROJECT-ROOT (the shape the framework scans), and no Members/ shape')
|
|
1753
|
+
{
|
|
1754
|
+
const P = await establish()
|
|
1755
|
+
const text = (await P.prompts('brainstorm', 'r-1')) + '\n' + (await P.prompts('normal', 'r-1'))
|
|
1756
|
+
// INVERTED (critical correction): the framework's writer/reader live at the PROJECT ROOT
|
|
1757
|
+
// (publishProgress writes Progress/<rId>/progress.md; countArtifacts scans Propos|Methods|Subproblems
|
|
1758
|
+
// under frameworkRoot()). The delivered text must document THOSE paths, never Members/<x>/...
|
|
1759
|
+
const decls = (text.match(/(?:^|[^A-Za-z0-9/_.-])(?:Progress|Propos|Methods|Subproblems)\/<[^>]+>\//g) || [])
|
|
1760
|
+
assert(decls.length >= 4, '* every member-visible library declaration is PROJECT-ROOT relative (Progress/<you>/, Propos/<you>/, ...) - the shape the framework scans (found ' + decls.length + ')')
|
|
1761
|
+
const wrongMembers = (text.match(/Members\/<[^>]+>\//g) || [])
|
|
1762
|
+
assert(wrongMembers.length === 0, '* NO member-facing declaration uses the Members/<x>/ shape (the framework never scans it) - found ' + JSON.stringify(wrongMembers))
|
|
1763
|
+
assert(/Progress\/<你>\/progress\.md/.test(text), '* the documented progress path matches publishProgress (Progress/<rId>/progress.md)')
|
|
1764
|
+
assert(/Propos\/<你>\//.test(text) && /Methods\/<你>\//.test(text) && /Subproblems\/<你>\//.test(text), '* the documented card paths match the record tools (Propos|Methods|Subproblems/<rId>/<id>.md)')
|
|
1765
|
+
// CLASS GUARD (one level above v3's): what the prompt documents must be what the code reads/scans
|
|
1766
|
+
const src4 = readFileSync(fileURLToPath(new URL('../vibe-math-v4/vibe-math-v4.js', import.meta.url)), 'utf8')
|
|
1767
|
+
for (const base of ['Progress', 'Propos', 'Methods', 'Subproblems']) {
|
|
1768
|
+
assert(new RegExp("'" + base + "'").test(src4), '* class guard: the framework reads/scans ' + base + ' - every documented write path must map onto a scanned base')
|
|
1769
|
+
}
|
|
1770
|
+
assert(/['"]Members\/['"]\s*\+/.test(src4) === false, '* class guard: no code path (writer) builds a Members/<id>/... path in v4')
|
|
1771
|
+
}
|
|
1772
|
+
section('N6 four-preset path discipline (member root / prefix-note)')
|
|
1773
|
+
{
|
|
1774
|
+
const { readFileSync } = await import('node:fs')
|
|
1775
|
+
const { fileURLToPath } = await import('node:url')
|
|
1776
|
+
const pick = (env, rel) => process.env[env] ? process.env[env] : fileURLToPath(new URL(rel, import.meta.url))
|
|
1777
|
+
const files = {
|
|
1778
|
+
v2: pick('V2_PLUGIN', '../vibe-math-v2/vibe-math-v2.js'),
|
|
1779
|
+
v3: pick('V3_PLUGIN', '../vibe-math-v3/vibe-math-v3.js'),
|
|
1780
|
+
v4: pick('V4_PLUGIN', '../vibe-math-v4/vibe-math-v4.js'),
|
|
1781
|
+
v5: pick('V5_PLUGIN', '../vibe-math-v5/vibe-math-v5.js'),
|
|
1782
|
+
}
|
|
1783
|
+
const ROOT = /Members\/<[^>]+>\/(?:Progress|Propos|Methods|Subproblems)\//g
|
|
1784
|
+
const LEGACY = /(?:^|[^/>])(?:Progress|Propos|Methods|Subproblems)\/<[^>]+>\//g
|
|
1785
|
+
for (const k of ['v5']) { // v5 pending vm-v5's re-verification; v4 handled explicitly below
|
|
1786
|
+
const src = readFileSync(files[k], 'utf8')
|
|
1787
|
+
const root = (src.match(ROOT) || []).length
|
|
1788
|
+
const legacy = (src.match(LEGACY) || []).length
|
|
1789
|
+
assert(root >= 4, '* ' + k + ': every member-visible library declaration carries the member root (found ' + root + ')')
|
|
1790
|
+
assert(legacy === 0, '* ' + k + ': no declaration keeps the legacy project-root-relative shape (found ' + legacy + ')')
|
|
1791
|
+
}
|
|
1792
|
+
// v4 (critical correction): the framework writes/reads at the PROJECT ROOT - publishProgress writes
|
|
1793
|
+
// Progress/<rId>/progress.md and countArtifacts scans Propos|Methods|Subproblems under frameworkRoot(),
|
|
1794
|
+
// so a Members/<x>/... declaration is a path NOTHING scans.
|
|
1795
|
+
{
|
|
1796
|
+
const src4 = files.v4 ? readFileSync(files.v4, 'utf8') : ''
|
|
1797
|
+
const decl4 = (src4.match(/(?:^|[^A-Za-z0-9/_.-])(?:Progress|Propos|Methods|Subproblems)\/<[^>]+>\//g) || [])
|
|
1798
|
+
const bad4 = (src4.match(/Members\/<[^>]+>\//g) || [])
|
|
1799
|
+
assert(!/Members\/<[^>]+>\//.test(src4), '* v4: NO member-facing declaration/code claim uses the Members/<x>/ shape (found ' + bad4.length + ')')
|
|
1800
|
+
assert(/Progress|<rId>|progress\.md/.test(src4), '* v4: the writer/reader bases are the project-root Progress/Propos/Methods/Subproblems')
|
|
1801
|
+
}
|
|
1802
|
+
// v3 (v2/v3 deep review D1): the writer `applyAgentWrites` accepts ONLY Problems|Progress|Propos|Methods|Notes
|
|
1803
|
+
// at the PROJECT ROOT, so a `Members/<x>/...` declaration is a write it SILENTLY DISCARDS. v3's member-facing
|
|
1804
|
+
// text must therefore stay root-relative - the opposite of v4/v5, which really do have a member subtree.
|
|
1805
|
+
const v3 = readFileSync(files.v3, 'utf8')
|
|
1806
|
+
assert(!/Members\/<[^>]+>\//.test(v3), '* v3: no member-facing declaration keeps the Members/<x>/ shape (the writer would discard that write)')
|
|
1807
|
+
const v3decl = [...v3.matchAll(/(?:^|[^/>])([A-Za-z]+)\/<[^>]+>\//g)].map((m) => m[1])
|
|
1808
|
+
const WL = ['Problems', 'Progress', 'Propos', 'Methods', 'Notes']
|
|
1809
|
+
const v3ok = v3decl.filter((x) => WL.indexOf(x) !== -1)
|
|
1810
|
+
assert(v3ok.length >= 1, '* v3: its declarations use the project-root-relative sections the writer accepts (found ' + v3ok.length + ' of ' + JSON.stringify(v3decl) + ')')
|
|
1811
|
+
// CLASS GUARD (covers v2/v3/v4/v5): whatever sections the persona/prompts tell an agent to write must be
|
|
1812
|
+
// accepted by that preset's writer - this is the invariant that would have caught both D1 and the v4/v5 cases.
|
|
1813
|
+
assert(/applyAgentWrites[\s\S]{0,600}?(Problems|Progress|Propos|Methods|Notes)/.test(v3), '* class guard: applyAgentWrites whitelists the sections the member-facing text names (v3)')
|
|
1814
|
+
const v2 = readFileSync(files.v2, 'utf8')
|
|
1815
|
+
assert(!/Members\/<[^>]+>\//.test(v2), '* v2: no Members/<x>/ shape either (anti-vacuity for the class, without over-claiming v2 has no path vocabulary)')
|
|
1816
|
+
assert(/PAPER_PATH_NOTE/.test(v2) && /\u4f1a\u8bdd cwd/.test(v2) && /\u7edd\u5bf9\u524d\u7f00/.test(v2), '* v2 uses the OTHER mechanism: one PAPER_PATH_NOTE stating the session-cwd + absolute-prefix rule')
|
|
1817
|
+
const uses = (v2.match(/PAPER_PATH_NOTE/g) || []).length
|
|
1818
|
+
// (D3 fixed the duplicate: the note no longer belongs in the evidence ENTRY list.) Assert the
|
|
1819
|
+
// STRUCTURE, not an occurrence count: defined once, appended by each member-facing material text,
|
|
1820
|
+
// and never embedded in a bare data entry.
|
|
1821
|
+
const noteDef = (v2.match(/const PAPER_PATH_NOTE/g) || []).length
|
|
1822
|
+
assert(noteDef === 1, '* v2 defines PAPER_PATH_NOTE exactly once (found ' + noteDef + ')')
|
|
1823
|
+
const noteLines = v2.split(/\r?\n/).filter((l) => l.includes('PAPER_PATH_NOTE'))
|
|
1824
|
+
const embeds = noteLines.filter((l) => !/const PAPER_PATH_NOTE/.test(l) && !/[+]|push\(/.test(l))
|
|
1825
|
+
assert(embeds.length === 0, '* v2 never embeds the prose rule in a bare data entry (' + JSON.stringify(embeds).slice(0, 80) + ')')
|
|
1826
|
+
const bodyOf = (src, fn) => {
|
|
1827
|
+
const a = src.indexOf('function ' + fn)
|
|
1828
|
+
if (a < 0) return null
|
|
1829
|
+
const b = src.slice(a + 10).search(/\n {2}(?:async )?function /)
|
|
1830
|
+
if (b < 0) return null // STRICT bound: no fixed-length fallback (window bleed lesson)
|
|
1831
|
+
return src.slice(a, a + 10 + b)
|
|
1832
|
+
}
|
|
1833
|
+
// RULING (B): the index is a DATA LIST (prose-free); the delivered member-facing material that carries
|
|
1834
|
+
// the paths is the digest, and it must append the note. (The other consumer, paperEmitArtifacts ->
|
|
1835
|
+
// paperBuildMarkdown, produces the FINAL PAPER ARTIFACT for humans, not a member prompt.)
|
|
1836
|
+
assert(!/PAPER_PATH_NOTE/.test(bodyOf(v2, 'paperEvidenceIndex') || ''), '* v2: the evidence-index DATA LIST embeds no prose rule')
|
|
1837
|
+
assert(/PAPER_PATH_NOTE/.test(bodyOf(v2, 'buildPaperDigest') || ''), '* v2: the delivered digest material appends PAPER_PATH_NOTE')
|
|
1838
|
+
assert(bodyOf(v2, 'buildPaperDigest') !== null && bodyOf(v2, 'paperEvidenceIndex') !== null, '* v2: both windows are bounded strictly (no fixed-length fallback)')
|
|
1839
|
+
}
|
|
1840
|
+
section('N7 delivered prompts carry no labelled-but-empty slot (proposer/agenda/target)')
|
|
1841
|
+
{
|
|
1842
|
+
const P = await establish()
|
|
1843
|
+
const txt = await P.prompts('verify', 'r-1', { target: 'p-nopowner', stage: 'independent' })
|
|
1844
|
+
assert(txt.length > 100 && /verifying object p-nopowner/.test(txt), '* the delivered verify prompt is non-trivial and names the target (non-vacuity: an empty prompt must not pass below)')
|
|
1845
|
+
assert(!/\u63d0\u51fa\u8005\s*\uff09/.test(txt), '* no labelled-but-empty proposer slot in the delivered frame (\u63d0\u51fa\u8005 + ) )')
|
|
1846
|
+
assert(!/agenda:\s*[)\uff09]/.test(txt), '* no labelled-but-empty agenda slot in the delivered frame')
|
|
1847
|
+
// protocol 搂9 (path-alias family): resolve the plugin ONCE from the loader's own URL, read THAT, and
|
|
1848
|
+
// assert the read path equals the V4_PLUGIN seam when it is set - a guard must never print one path
|
|
1849
|
+
// and read another.
|
|
1850
|
+
const _pu = pluginUrl() // the loader's own resolution (seam-aware)
|
|
1851
|
+
const _href = typeof _pu === 'string' ? _pu : String(_pu && _pu.href ? _pu.href : _pu)
|
|
1852
|
+
const _loaderPath = _href.indexOf('file:') === 0 ? fileURLToPath(new URL(_href.split('?')[0])) : String(_href).split('?')[0]
|
|
1853
|
+
if (process.env.V4_PLUGIN) {
|
|
1854
|
+
const _a = _loaderPath.replace(/\\/g, '/').toLowerCase()
|
|
1855
|
+
const _b = String(process.env.V4_PLUGIN).replace(/\\/g, '/').toLowerCase()
|
|
1856
|
+
assert(_a === _b, '* N10 reads the SAME file the loader imports (readPath === V4_PLUGIN seam): ' + _a + ' vs ' + _b)
|
|
1857
|
+
}
|
|
1858
|
+
assert(/String\(vs\.targetOwner\|\|''\)\.trim\(\)\?/.test(PLUGIN_SRC()), '* the proposer segment is assembled ONLY when a value exists (source-level: the office is never invented)')
|
|
1859
|
+
}
|
|
1860
|
+
section('N8 the meeting frame\'s relay claim matches reality (no promise of speeches that do not exist)')
|
|
1861
|
+
{
|
|
1862
|
+
const P = await establish()
|
|
1863
|
+
const noPrior = await P.prompts('meeting', 'r-1')
|
|
1864
|
+
assert(noPrior.length > 100 && /A meeting is in progress/.test(noPrior), '* the meeting frame is non-trivial (non-vacuity)')
|
|
1865
|
+
assert(!/\u4e0b\u9762\u5df2\u6709\u4eba\u53d1\u8a00/.test(noPrior), '* with NO prior statements the frame does not claim that others have spoken (\u4e0b\u9762\u5df2\u6709\u4eba\u53d1\u8a00)')
|
|
1866
|
+
assert(/\u5e76\u884c\u72ec\u7acb\u53d1\u8a00/.test(noPrior), '* …instead it states the real semantics: this round is parallel independent statements')
|
|
1867
|
+
assert(/\(prior\?'\\n\u8fd9\u662f\u4e00\u573a\u771f\u5b9e\u8ba8\u8bba/.test(PLUGIN_SRC()), '* the relay claim is gated on prior existing (source-level)')
|
|
1868
|
+
assert(/\(prior\?\(\'\\n\\n### \u5df2\u6709\u53d1\u8a00/.test(PLUGIN_SRC()), '* the forwarded-speech block is still delivered whenever prior exists')
|
|
1869
|
+
}
|
|
1870
|
+
section('N9 behavioural: a write to the DOCUMENTED path is counted by countArtifacts (auto-meeting basis)')
|
|
1871
|
+
{
|
|
1872
|
+
const P = await establish()
|
|
1873
|
+
const st0 = await P.callTool('vibe_v4_status', {})
|
|
1874
|
+
assert(typeof st0.artifactCount === 'number', '* status exposes artifactCount (the auto-meeting basis) - keys: ' + JSON.stringify(Object.keys(st0).slice(0, 14)))
|
|
1875
|
+
const rec = await P.callTool('vibe_v4_record_proposition', { id: 'p-docpath', statement: '文档路径行为证明', value: 0.5, motive: 'm', p: 0.5 }, P.resAgent(P.childOf('r-1')))
|
|
1876
|
+
assert(rec.ok === true, '* a member card is recorded through the tool (returned ' + JSON.stringify(rec).slice(0, 120) + ')')
|
|
1877
|
+
const st1 = await P.callTool('vibe_v4_status', {})
|
|
1878
|
+
assert(st1.artifactCount > st0.artifactCount, '* the card written to the DOCUMENTED path (Propos/<id>/<id>.md at the project root) IS counted by countArtifacts (' + st0.artifactCount + ' -> ' + st1.artifactCount + ')')
|
|
1879
|
+
const files = P.host && P.host.fsWrites ? P.host.fsWrites.map((w) => String(w.path || '')).join(',') : ''
|
|
1880
|
+
assert(files.indexOf('Members/') === -1, '* no write landed under Members/ (the shape nothing scans) - writes: ' + files.slice(0, 160))
|
|
1881
|
+
}
|
|
1882
|
+
section('N10 G-7 class guard: agent-facing compaction takes the AGENT-LOCAL instance')
|
|
1883
|
+
{
|
|
1884
|
+
const { readFileSync } = await import('node:fs')
|
|
1885
|
+
const { fileURLToPath } = await import('node:url')
|
|
1886
|
+
assert(/function compactionForAgent\(/.test(PLUGIN_SRC()), '* v4 defines compactionForAgent (the agent-local accessor)')
|
|
1887
|
+
// F2 (HIGH): a named, BOUNDED anchor. The old window ran to the next two-space function (109,191 chars)
|
|
1888
|
+
// and swallowed unrelated code, which is why a line-located mutant escaped unnoticed.
|
|
1889
|
+
const CALL = 'const compaction = compactionForAgent(agent)'
|
|
1890
|
+
const at = PLUGIN_SRC().indexOf(CALL)
|
|
1891
|
+
assert(at > 0, '* the agent-facing compaction call exists verbatim (anchor: ' + CALL + ')')
|
|
1892
|
+
const _ls = PLUGIN_SRC().lastIndexOf('\n', at)
|
|
1893
|
+
const _le = PLUGIN_SRC().indexOf('\n', at)
|
|
1894
|
+
const win = PLUGIN_SRC().slice(_ls, _le > 0 ? _le : at + 200)
|
|
1895
|
+
assert(win.length <= 400, '* the inspected window is BOUNDED (the call line only; got ' + win.length + ')')
|
|
1896
|
+
assert(win.indexOf(CALL) !== -1, '* the window really contains the agent-facing call')
|
|
1897
|
+
if (process.env.V4_DEBUG) { const _lp = (function(){ try { const u = pluginUrl(); const h = typeof u === 'string' ? u : String(u && u.href ? u.href : u); return h.indexOf('file:') === 0 ? fileURLToPath(new URL(h.split('?')[0])) : h.split('?')[0] } catch (e) { return 'ERR:' + e.message } })(); console.log(' [N10-diag] read=' + _lp + ' | seam=' + String(process.env.V4_PLUGIN || '(none)') + ' | srcHasAnchor=' + (PLUGIN_SRC().indexOf(CALL) !== -1) + ' | win=' + win.length) }
|
|
1898
|
+
assert(!/compactionOf\(\)/.test(win), '* the agent-facing path does NOT call the host-root compactionOf() (a realm never falls back)')
|
|
1899
|
+
}
|
|
1483
1900
|
console.log('')
|
|
1901
|
+
section('N11 F-4c: a failed mkdir is NAMED exactly once (deferred/distorted diagnosis fixed)')
|
|
1902
|
+
{
|
|
1903
|
+
const h = await mount()
|
|
1904
|
+
const seen = []
|
|
1905
|
+
const realErr = console.error
|
|
1906
|
+
console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) } // protocol 搂6.4: capture AND still name a red
|
|
1907
|
+
shellMkdirExit = 1
|
|
1908
|
+
try {
|
|
1909
|
+
await h.callTool('vibe_v4_start', { problem: 'F-4c mkdir 失败可见', residentCount: 1 })
|
|
1910
|
+
const mk = shellCalls.filter(s => /New-Item|^mkdir -p /.test(String(s)))
|
|
1911
|
+
assert(mk.length >= 1, '* N11: the mkdir script was actually issued (shellCalls=' + shellCalls.length + ', mkdir=' + mk.length + ')')
|
|
1912
|
+
const warned = seen.filter(l => /ensureDirs: mkdir failed \(exit 1\) under/.test(l))
|
|
1913
|
+
assert(warned.length === 1, '* N11: the mkdir failure is NAMED exactly once (matched=' + warned.length + ' of ' + seen.length + ' captured stderr lines)')
|
|
1914
|
+
} finally { console.error = realErr; shellMkdirExit = 0 }
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
section('N13 F1 invariant guards: membership predicates + roster-only ballots (source level)')
|
|
1918
|
+
{
|
|
1919
|
+
const srcF1 = PLUGIN_SRC()
|
|
1920
|
+
const bodyOfF1 = (name) => { const a = srcF1.indexOf('function ' + name + '('); if (a < 0) return null; let d = 0; for (let i = a; i < srcF1.length; i++) { const ch = srcF1[i]; if (ch === '{') d++; else if (ch === '}') { d--; if (d === 0) return srcF1.slice(a, i + 1) } } return null }
|
|
1921
|
+
const cvrF1 = bodyOfF1('continueVerifyRound'), fvF1 = bodyOfF1('finalizeVerify')
|
|
1922
|
+
assert(!!cvrF1 && !!fvF1, '* F1/1 both consensus functions are found (bounded, brace-balanced windows)')
|
|
1923
|
+
assert(cvrF1.length < 20000 && fvF1.length < 20000, '* F1/1 the inspected windows are BOUNDED (cvr=' + cvrF1.length + ', fv=' + fvF1.length + ') - a runaway window is itself a failure')
|
|
1924
|
+
const MEMF1 = 'ids.every(id=>vs.verdicts[id]!==undefined)'
|
|
1925
|
+
assert(cvrF1.indexOf(MEMF1) !== -1, '* F1/1 continueVerifyRound requires MEMBERSHIP of the frozen ids')
|
|
1926
|
+
assert(fvF1.indexOf(MEMF1) !== -1, '* F1/1 finalizeVerify uses the SAME membership predicate (the two views cannot disagree)')
|
|
1927
|
+
assert(!/Object\.keys\(vs\.verdicts\)\.length>=/.test(fvF1), '* F1/1 finalizeVerify does NOT use the count-only predicate (that is the F1 regression: a non-roster ballot could satisfy it)')
|
|
1928
|
+
assert(/const vals=ids\.filter\(id=>vs\.verdicts\[id\]!==undefined\)\.map\(id=>vs\.verdicts\[id\]\)/.test(fvF1), '* F1/1 finalizeVerify takes `vals` ONLY from the frozen ids (never Object.values of the live map)')
|
|
1929
|
+
assert(/_inRoster\s*=\s*!Array\.isArray\(verifyState\.rosterSnapshot\)\s*\|\|\s*verifyState\.rosterSnapshot\.indexOf\(r\.rId\)!==-1/.test(srcF1), '* F1/2 the ballot recording site is guarded by the FROZEN roster (verdicts keys are a subset of rosterSnapshot)')
|
|
1930
|
+
const gF1 = srcF1.indexOf('if(!_inRoster)'), aF1 = srcF1.indexOf('verifyState.verdicts[r.rId]={prob:p')
|
|
1931
|
+
assert(gF1 !== -1 && aF1 > gF1 && (aF1 - gF1) < 400, '* F1/2 the verdict assignment sits INSIDE that guard (distance=' + (aF1 - gF1) + ' chars)')
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
section('N15 F-6 site 4: a failed compaction is NAMED exactly once (realCompact -> onResidentEnd)')
|
|
1935
|
+
{
|
|
1936
|
+
const h = await establish()
|
|
1937
|
+
const seen = []
|
|
1938
|
+
const realErr = console.error
|
|
1939
|
+
console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
|
|
1940
|
+
try {
|
|
1941
|
+
compactionFails = true
|
|
1942
|
+
// the turn end is only processed for a BUSY resident (`if(!busy.delete(r.rId)) return`), so
|
|
1943
|
+
// wake it first, exactly like the suite's workWake() helper does.
|
|
1944
|
+
const before = h.followups.length
|
|
1945
|
+
await h.callTool('vibe_v4_message', { to: 'r-1', content: '请继续推进。' })
|
|
1946
|
+
for (let i = 0; i < 300 && h.followups.length === before; i++) await sleep(10)
|
|
1947
|
+
const cid = h.followups[h.followups.length - 1].childId
|
|
1948
|
+
assert(!!cid, '* N15/4 the resident was woken so the turn end reaches onResidentEnd (childId=' + cid + ')')
|
|
1949
|
+
h.fireEnd(cid, { summary: '推进本轮。', solved: false, contextPct: 20 })
|
|
1950
|
+
for (let i = 0; i < 80 && !seen.some(l => /realCompact: the compaction call failed/.test(l)); i++) await sleep(10)
|
|
1951
|
+
const w = seen.filter(l => /realCompact: the compaction call failed/.test(l))
|
|
1952
|
+
assert(w.length === 1, '* N15/4 realCompact: a failed compaction is NAMED exactly once (matched=' + w.length + ' of ' + seen.length + ' stderr within 800ms)')
|
|
1953
|
+
assert(/NOT compacted/.test(w[0] || ''), '* N15/4 the warning STATES THE CONSEQUENCE (context pressure was NOT compacted)')
|
|
1954
|
+
} finally { console.error = realErr; compactionFails = false }
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
section('N14 F2 compaction policy: policy verb only, re-anchor only after a REAL compaction (source level)')
|
|
1958
|
+
{
|
|
1959
|
+
const srcC = PLUGIN_SRC()
|
|
1960
|
+
const aC = srcC.indexOf('async function realCompact(')
|
|
1961
|
+
assert(aC > 0, '* F2/a realCompact is found through the seam')
|
|
1962
|
+
let dC = 0, eC = aC
|
|
1963
|
+
for (let i = aC; i < srcC.length; i++) { const ch = srcC[i]; if (ch === '{') dC++; else if (ch === '}') { dC--; if (dC === 0) { eC = i; break } } }
|
|
1964
|
+
const bodyC = srcC.slice(aC, eC + 1)
|
|
1965
|
+
assert(bodyC.length > 100 && bodyC.length < 4000, '* F2/a the inspected window is BOUNDED (' + bodyC.length + ' chars) - a runaway window is itself a failure')
|
|
1966
|
+
assert(bodyC.indexOf('compactIfNeeded(agent, ') !== -1, '* F2/a realCompact calls the POLICY verb compactIfNeeded (threshold-driven)')
|
|
1967
|
+
assert(bodyC.indexOf('compactNow(') === -1, '* F2/a realCompact contains NO compactNow( call: a host offering the forcing verb must NOT be forced (user ruling)')
|
|
1968
|
+
assert(!/const force\s*=/.test(bodyC), '* F2/a the capability-based forcing branch is GONE, not merely deprioritised')
|
|
1969
|
+
assert(/const didCompact\s*=/.test(bodyC), '* F2/a the re-anchor is gated on didCompact (evidence that the policy call ACTUALLY compacted)')
|
|
1970
|
+
assert(/if\(didCompact\)\{/.test(bodyC), '* F2/a and the gate is the if(didCompact){ branch (mutant: if(true){ => this reddens)')
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
section('N17 F-5 fence-root drift: containment, no-op boundary, one-shot naming both roots')
|
|
1974
|
+
{
|
|
1975
|
+
const h = await establish()
|
|
1976
|
+
const seen = []
|
|
1977
|
+
const realErr = console.error
|
|
1978
|
+
console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
|
|
1979
|
+
const drift = () => seen.filter(l => /fence root does not CONTAIN this session workspace/.test(l))
|
|
1980
|
+
// NOTE: case (0) is 'no sandboxPolicy SERVICE'; case 1b is 'service present, root uninterpretable'.
|
|
1981
|
+
const ws = () => String(h.WS).replace(/\\/g, '/')
|
|
1982
|
+
try {
|
|
1983
|
+
const r1 = await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 1' }, h.resAgent(h.childOf('r-1')))
|
|
1984
|
+
await sleep(50)
|
|
1985
|
+
assert(drift().length === 0, '* F-5/i NO sandboxPolicy SERVICE at all => the policy path never runs and NO warning appears (drift=' + drift().length + '; the uninterpretable-root no-op is case 1b)')
|
|
1986
|
+
assert(r1 && r1.ok !== false, '* F-5/i and getPolicy() still returns a usable policy (the write went through)')
|
|
1987
|
+
// F-5/1b: a policy SERVICE that resolves to a shape with NO interpretable root ({allow,deny}) -
|
|
1988
|
+
// the helper IS invoked and must stay silent (uninterpretable => no-op, today's behaviour).
|
|
1989
|
+
seen.length = 0 // F-5: observe ONLY this case's window (no cascade from earlier cases)
|
|
1990
|
+
sandboxNoRoot = true
|
|
1991
|
+
const r1b = await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 1b' }, h.resAgent(h.childOf('r-1')))
|
|
1992
|
+
await sleep(50)
|
|
1993
|
+
assert(drift().length === 0, '* F-5/1b an UNINTERPRETABLE resolved policy ({allow,deny}) produces NO warning (documented no-op, not a dead branch) (' + drift().length + ')')
|
|
1994
|
+
const w1b = await h.callTool('vibe_v4_status', {})
|
|
1995
|
+
assert(r1b && r1b.ok !== false && !!w1b, '* F-5/1b getPolicy() still RESOLVES for that shape (the write succeeds, no throw path)')
|
|
1996
|
+
// F-5/1c: the session cwd is UNKNOWN (no cwd on the captured root agent) and the policy exposes no
|
|
1997
|
+
// top-level root, so workspaceRoot() is `'.'`; resolve({session}) still yields a root. The `.` clause
|
|
1998
|
+
// of the product guard is the ONLY reason this stays silent - a mutant that warns on an unknown cwd
|
|
1999
|
+
// reddens here by name.
|
|
2000
|
+
sandboxNoRoot = false
|
|
2001
|
+
sandboxDriftRoot = null
|
|
2002
|
+
sandboxUnknownCwd = true
|
|
2003
|
+
const seenC = []
|
|
2004
|
+
const realErrC = console.error
|
|
2005
|
+
console.error = (...a) => { seenC.push(a.map(String).join(' ')); realErrC(...a) }
|
|
2006
|
+
let hc = null
|
|
2007
|
+
try {
|
|
2008
|
+
// FOUNDING under an unknown cwd: the founding writes go through writeTextAbs -> getPolicy(),
|
|
2009
|
+
// so the window below both reaches the helper and proves it stayed silent
|
|
2010
|
+
hc = await mount()
|
|
2011
|
+
hc.resetResolveCalls()
|
|
2012
|
+
await hc.callTool('vibe_v4_start', { problem: 'F-5 case 1c', residentCount: 1 })
|
|
2013
|
+
await sleep(80)
|
|
2014
|
+
} finally { console.error = realErrC; sandboxUnknownCwd = false }
|
|
2015
|
+
const driftC = seenC.filter(l => /fence root does not CONTAIN this session workspace/.test(l))
|
|
2016
|
+
assert(driftC.length === 0, '* F-5/1c an UNKNOWN session cwd stays SILENT (the `.` guard: drift=' + driftC.length + ')')
|
|
2017
|
+
assert(hc.resolveCalls() >= 1, '* F-5/1c LIVENESS: the helper really ran with an unknown cwd (resolve calls=' + hc.resolveCalls() + ', policy queries=' + hc.policyQueries() + '), so the silence is not an unreached branch')
|
|
2018
|
+
sandboxNoRoot = false
|
|
2019
|
+
// (iv) agreeing roots => silence (anti-vacuity)
|
|
2020
|
+
seen.length = 0 // F-5: observe ONLY this case's window
|
|
2021
|
+
sandboxDriftRoot = ws()
|
|
2022
|
+
await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 4' }, h.resAgent(h.childOf('r-1')))
|
|
2023
|
+
await sleep(50)
|
|
2024
|
+
assert(drift().length === 0, '* F-5/iv an AGREEING resolved root produces NO warning (not vacuous)')
|
|
2025
|
+
// (iii) containment: an ANCESTOR root is legitimate => silence
|
|
2026
|
+
seen.length = 0 // F-5: observe ONLY this case's window
|
|
2027
|
+
sandboxDriftRoot = ws().replace(/\/[^/]+$/, '')
|
|
2028
|
+
await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 3' }, h.resAgent(h.childOf('r-1')))
|
|
2029
|
+
await sleep(50)
|
|
2030
|
+
assert(drift().length === 0, '* F-5/iii a root that CONTAINS the session workspace is accepted (no per-session false alarm)')
|
|
2031
|
+
// (ii) real drift => exactly one named warning naming BOTH sides
|
|
2032
|
+
seen.length = 0 // F-5: observe ONLY this case's window
|
|
2033
|
+
sandboxDriftRoot = ws() + '/somewhere-else'
|
|
2034
|
+
await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 2' }, h.resAgent(h.childOf('r-1')))
|
|
2035
|
+
await sleep(50)
|
|
2036
|
+
// a SECOND drift-triggering resolution: the one-shot must keep the warning at exactly one
|
|
2037
|
+
await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 2b' }, h.resAgent(h.childOf('r-1')))
|
|
2038
|
+
await sleep(50)
|
|
2039
|
+
const w = drift()
|
|
2040
|
+
assert(w.length === 1, '* F-5/ii the drift is NAMED exactly once (matched=' + w.length + ' of ' + seen.length + ' stderr lines)')
|
|
2041
|
+
assert(/resolved=/.test(w[0] || '') && /session=/.test(w[0] || ''), '* F-5/ii the warning names BOTH the resolved root and the session workspace')
|
|
2042
|
+
} finally { console.error = realErr; sandboxDriftRoot = null; sandboxNoRoot = false }
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
section('N18 F-6: a failed current-project marker write is NAMED exactly once')
|
|
2047
|
+
{
|
|
2048
|
+
// PRE-START: configure() refuses while a run is live and returns BEFORE writeCurrentProject
|
|
2049
|
+
// (measured with establish(): the injection never fired), so this section mounts without starting.
|
|
2050
|
+
const h = await mount()
|
|
2051
|
+
const seen = []
|
|
2052
|
+
const realErr = console.error
|
|
2053
|
+
console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
|
|
2054
|
+
const hits = () => seen.filter((l) => /current-project marker was NOT persisted/.test(l))
|
|
2055
|
+
try {
|
|
2056
|
+
// ONE injected failure, only for the `.current` marker write (path-anchored, existing hook)
|
|
2057
|
+
fsWriteFailPred = (p) => /[\\/]\.current$/.test(String(p))
|
|
2058
|
+
fsWriteFailOnce = true
|
|
2059
|
+
await h.callTool('vibe_v4_configure', { project: 'n18-proj' })
|
|
2060
|
+
await sleep(50)
|
|
2061
|
+
assert(hits().length === 1, '* N18 the failed `.current` write is NAMED exactly once (matched=' + hits().length + ' of ' + seen.length + ' stderr lines)')
|
|
2062
|
+
assert(/next session may load the previous project/.test(hits()[0] || ''), '* N18 the warning STATES THE CONSEQUENCE (the next session may load the previous project)')
|
|
2063
|
+
// anti-vacuity: a SUCCESSFUL marker write stays silent
|
|
2064
|
+
fsWriteFailPred = null
|
|
2065
|
+
fsWriteFailOnce = false
|
|
2066
|
+
seen.length = 0
|
|
2067
|
+
await h.callTool('vibe_v4_configure', { project: 'n18-proj-2' })
|
|
2068
|
+
await sleep(50)
|
|
2069
|
+
assert(hits().length === 0, '* N18 a SUCCESSFUL marker write is silent (the assertion is not vacuous)')
|
|
2070
|
+
} finally { console.error = realErr; fsWriteFailPred = null; fsWriteFailOnce = false }
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
|
|
2074
|
+
section('N20-3b rebuildLeanLibIndexes: a failed INDEX WRITE is NAMED once (covers all six callers)')
|
|
2075
|
+
{
|
|
2076
|
+
// ③ callsite enumeration (measured at this revision) and verdicts:
|
|
2077
|
+
// · settleLeanJob (:893) - own try/catch naming (in-suite capture = measured boundary)
|
|
2078
|
+
// · writeLeanLibIndexesSafe (:1006) - boundary: cold-recovery convenience, rebuilt again later
|
|
2079
|
+
// · leanArchive def/proof/blocked (:1394/:1438/:1452) - silent because the FUNCTION swallowed
|
|
2080
|
+
// · the lean_lib tool (:4139) - silent for the same reason (measured: ok:true + counts)
|
|
2081
|
+
// => ONE fix inside the function covers all six; this asserts the naming.
|
|
2082
|
+
const h = await mount()
|
|
2083
|
+
const seen = []
|
|
2084
|
+
const realErr = console.error
|
|
2085
|
+
console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
|
|
2086
|
+
const hits = () => seen.filter(l => /rebuildLeanLibIndexes: the Lean library index could NOT be written/.test(l))
|
|
2087
|
+
try {
|
|
2088
|
+
// (a) anti-vacuity: a healthy rebuild is SILENT
|
|
2089
|
+
await h.callTool('vibe_v4_lean_lib', {})
|
|
2090
|
+
await sleep(30)
|
|
2091
|
+
assert(hits().length === 0, '* N20-3b a healthy index rebuild is silent (the case is not vacuous)')
|
|
2092
|
+
// (b) one injected index-write failure => exactly one NAMED warning WITH its consequence
|
|
2093
|
+
fsWriteFailPred = (p) => /(?:Lib|Proved)[\\/]Index\.md$/i.test(String(p))
|
|
2094
|
+
fsWriteFailOnce = true
|
|
2095
|
+
const r = await h.callTool('vibe_v4_lean_lib', {})
|
|
2096
|
+
await sleep(30)
|
|
2097
|
+
assert(hits().length === 1, '* N20-3b a failed index write is NAMED exactly once (matched=' + hits().length + ' of ' + seen.length + ' stderr lines)')
|
|
2098
|
+
assert(/STALE until the next successful rebuild/.test(hits()[0] || ''), '* N20-3b the warning STATES THE CONSEQUENCE (the index is stale until the next successful rebuild)')
|
|
2099
|
+
assert(!!r && r.ok === true, '* N20-3b and the tool still answers with the scanned counts (the naming is additive, not a new failure mode)')
|
|
2100
|
+
} finally { console.error = realErr; fsWriteFailPred = null; fsWriteFailOnce = false }
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1484
2103
|
console.log('passed=' + passed + ' failed=' + failed)
|
|
1485
2104
|
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
1486
2105
|
console.log('ALL GREEN')
|