dsh-vibe-math 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT-CHECKLIST.md +78 -3
- package/README.md +33 -2
- package/RELEASE-NOTES-2.3.1.md +134 -0
- package/RELEASE-NOTES-2.3.2.md +145 -0
- package/audit-formal-sensitivity.mjs +134 -39
- package/audit-prompt-invariants.mjs +414 -0
- package/audit-spec-traceability.mjs +173 -0
- package/audit-v5-integrity.mjs +5 -3
- package/docs/formal-verification.md +122 -19
- package/docs/generate_framework_diagram_v5.mjs +2 -1
- package/docs/test-timing.md +101 -0
- package/formal-verify-v2.test.mjs +526 -7
- package/formal-verify-v3.test.mjs +389 -10
- package/formal-verify-v4.test.mjs +462 -4
- package/formal-verify-v5.test.mjs +163 -4
- package/installer.js +3 -1
- package/package.json +12 -2
- package/prompt-corpus-persona/persona-corpus.json +2 -2
- package/prompt-corpus-persona/persona-corpus.md +6 -2
- package/prompt-corpus-v2/formal-verify-v2.json +484 -0
- package/prompt-corpus-v2/formal-verify-v2.md +5239 -0
- package/prompt-corpus-v3/formal-verify-v3.json +274 -100
- package/prompt-corpus-v3/formal-verify-v3.md +2057 -335
- package/prompt-corpus-v4/formal-verify-v4.json +89 -0
- package/prompt-corpus-v4/formal-verify-v4.md +283 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +186 -219
- package/prompt-corpus-v5/prompt-corpus-v5.md +485 -700
- package/prompt-v5-integrity.test.mjs +1272 -1085
- package/run-tests.mjs +118 -0
- package/vibe-math-v2/vibe-math-v2.js +341 -45
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +129 -8
- package/vibe-math-v3/vibe-math-v3.js +162 -36
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +21 -3
- package/vibe-math-v4/vibe-math-v4.js +201 -30
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +54 -2
- package/vibe-math-v5/agent.cordis.yml +6 -2
- package/vibe-math-v5/vibe-math-v5.js +133 -28
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +55 -5
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +16 -2
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +6 -5
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// the object is Lean-passed or carries an explicit, reasoned blocker; then the
|
|
12
12
|
// same verdict DOES write the card, and the card records the formal status
|
|
13
13
|
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
14
|
+
// · the REPLY CHANNEL is real, not dead code (contract §4 / §6.3 / §10.8): a verifier's
|
|
15
|
+
// `formal:{decision:'blocked'|'defect', note}` reply is actually absorbed into the durable
|
|
16
|
+
// record for BOTH id spaces, a missing note is refused, and `defect` (a fidelity defect,
|
|
17
|
+
// i.e. the Lean code does not say what the proposition says) downgrades the proof, deletes
|
|
18
|
+
// the archived file, writes the TODO and defers the verdict — it is NEVER recorded as
|
|
19
|
+
// "the proposition is false" (contract §4.1)
|
|
14
20
|
//
|
|
15
21
|
// The Lean toolchain is mocked through the subprocess SERVICE (a fake Lean: exit 0 unless the
|
|
16
22
|
// file still contains `sorry` or the marker `-- FAIL`), so these tests exercise the REAL code
|
|
@@ -24,11 +30,20 @@
|
|
|
24
30
|
// ============================================================
|
|
25
31
|
import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, writeFileSync, statSync, mkdirSync } from 'node:fs'
|
|
26
32
|
import { tmpdir } from 'node:os'
|
|
27
|
-
import { join, dirname, isAbsolute } from 'node:path'
|
|
33
|
+
import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
28
35
|
|
|
29
36
|
const PLUGIN = process.env.V2_PLUGIN
|
|
30
37
|
? new URL('file:///' + String(process.env.V2_PLUGIN).replace(/\\/g, '/'))
|
|
31
38
|
: new URL('./vibe-math-v2/vibe-math-v2.js', import.meta.url)
|
|
39
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
40
|
+
// Human-reviewable corpus (contract §10.10). A sensitivity probe runs THIS suite against a
|
|
41
|
+
// MUTATED plugin copy: writing the repository corpus from such a run would replace the
|
|
42
|
+
// reviewed text with mutated text, so a mutated run goes to a scratch directory instead
|
|
43
|
+
// (and V2_CORPUS_DIR overrides both, exactly like the v3 suite's V3_CORPUS_DIR).
|
|
44
|
+
const CORPUS_DIR = process.env.V2_CORPUS_DIR
|
|
45
|
+
? pathResolve(process.env.V2_CORPUS_DIR)
|
|
46
|
+
: (process.env.V2_PLUGIN ? join(tmpdir(), 'vibe-v2-prompt-corpus') : join(HERE, 'prompt-corpus-v2'))
|
|
32
47
|
|
|
33
48
|
let passed = 0, failed = 0
|
|
34
49
|
const failures = []
|
|
@@ -41,6 +56,7 @@ const section = (t) => console.log('\n[' + t + ']')
|
|
|
41
56
|
// ---------------------------------------------------------------
|
|
42
57
|
let toolchainAvailable = true
|
|
43
58
|
const leanRuns = []
|
|
59
|
+
const terminations = []
|
|
44
60
|
const subprocess = {
|
|
45
61
|
async resolveExecutable(cmd) {
|
|
46
62
|
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
@@ -64,11 +80,48 @@ const subprocess = {
|
|
|
64
80
|
if (m) m[1].split(/\s+/).forEach((p) => { const q = p.trim().replace(/^'|'$/g, ''); if (q) mkdirSync(q, { recursive: true }) })
|
|
65
81
|
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
66
82
|
}
|
|
83
|
+
// v2 ALSO deletes files through this same service (`powershell … Remove-Item -LiteralPath 'x'`,
|
|
84
|
+
// POSIX `rm -f 'x'`). Without honouring it, the `defect` assertion "the archived proof is
|
|
85
|
+
// gone" would pass vacuously (nothing was ever deleted) instead of testing the real code path.
|
|
86
|
+
if (/Remove-Item/.test(script)) {
|
|
87
|
+
const m = /-LiteralPath\s+'((?:[^']|'')*)'/.exec(script)
|
|
88
|
+
if (m) { try { rmSync(m[1].replace(/''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
89
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
90
|
+
}
|
|
91
|
+
if (/^\s*rm -f /.test(script)) {
|
|
92
|
+
const re = /'((?:[^']|'\\'')*)'/g
|
|
93
|
+
let m
|
|
94
|
+
while ((m = re.exec(script)) !== null) { try { rmSync(m[1].replace(/'\\''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
95
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
96
|
+
}
|
|
67
97
|
const text = existsSync(last) ? readFileSync(last, 'utf8') : ''
|
|
68
98
|
const bad = /sorry|-- FAIL/.test(text)
|
|
69
99
|
leanRuns.push({ argv: spec.argv.slice(0, -1), file: last, cwd: spec.cwd, graceMs: spec.graceMs, stdio: spec.stdio })
|
|
70
100
|
const stdout = bad ? '' : 'ok\n'
|
|
71
101
|
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
102
|
+
if (/-- HANG/.test(text)) {
|
|
103
|
+
// A run that never finishes by itself. It settles ONLY when the plugin actively terminates it
|
|
104
|
+
// (contract §7: "对超时调用 handle.terminate()"), with a bounded 2.5 s fallback so a plugin
|
|
105
|
+
// that FORGETS to terminate FAILS the terminate/speed assertions instead of hanging this suite.
|
|
106
|
+
let settled = false
|
|
107
|
+
let finish
|
|
108
|
+
const done = new Promise((resolve) => { finish = resolve })
|
|
109
|
+
const fallback = REAL_SET_TIMEOUT(() => { if (!settled) { settled = true; finish({ exitCode: null, signal: 'SIGKILL' }) } }, 2500)
|
|
110
|
+
return {
|
|
111
|
+
done,
|
|
112
|
+
collected: {
|
|
113
|
+
stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) },
|
|
114
|
+
stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) },
|
|
115
|
+
},
|
|
116
|
+
terminate() {
|
|
117
|
+
if (settled) return
|
|
118
|
+
settled = true
|
|
119
|
+
REAL_CLEAR_TIMEOUT(fallback)
|
|
120
|
+
terminations.push({ file: last })
|
|
121
|
+
finish({ exitCode: null, signal: 'SIGTERM' })
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
72
125
|
return {
|
|
73
126
|
done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
|
|
74
127
|
collected: {
|
|
@@ -162,18 +215,52 @@ async function makeCase(label, opts = {}) {
|
|
|
162
215
|
// The scheduler only picks objects up while it is RUNNING (scheduleTick early-returns).
|
|
163
216
|
async function startScheduler(h) { await h.call('vibe_math_start', {}) }
|
|
164
217
|
|
|
218
|
+
// ── test speed: fast-forward the SCHEDULER POLL (test-only, no production impact) ─────────
|
|
219
|
+
// The plugin registers its scheduler poll as `setInterval(..., 1000)` at apply() time, so every
|
|
220
|
+
// suite-side `tick()` had to wait a full wall-clock second. With ~9 verification rounds × ~17
|
|
221
|
+
// ticks that alone accounted for ~99% of this suite's 186 s (its v3/v4/v5 siblings take seconds).
|
|
222
|
+
// Patching ONLY setInterval (the suite's own `sleep` uses setTimeout) makes a poll cost ~25 ms,
|
|
223
|
+
// while the plugin's due-ness logic still uses the real 200 ms `tickIntervalMs` floor — no
|
|
224
|
+
// scheduler behaviour depends on the poll period, and `tickTheScheduler` below stays above it.
|
|
225
|
+
const REAL_SET_INTERVAL = globalThis.setInterval
|
|
226
|
+
globalThis.setInterval = function (fn, ms, ...rest) {
|
|
227
|
+
return REAL_SET_INTERVAL(fn, Math.min(Number(ms) || 0, 25), ...rest)
|
|
228
|
+
}
|
|
229
|
+
// ── test speed / leak check: track OUTSTANDING setTimeout handles (test-only) ──────────────
|
|
230
|
+
// `leanRunFile` races `handle.done` against its own `cap`-ms timer; forgetting to clear that timer on
|
|
231
|
+
// the normal path leaves one pending multi-minute timer PER RUN — a leak no output assertion can see.
|
|
232
|
+
// The plugin resolves the global at call time, so wrapping it here covers it. The mock's own bounded
|
|
233
|
+
// fallback uses the REAL functions, so it never pollutes the count.
|
|
234
|
+
const REAL_SET_TIMEOUT = globalThis.setTimeout
|
|
235
|
+
const REAL_CLEAR_TIMEOUT = globalThis.clearTimeout
|
|
236
|
+
const liveTimers = new Set()
|
|
237
|
+
globalThis.setTimeout = function (fn, ms, ...rest) {
|
|
238
|
+
let h
|
|
239
|
+
h = REAL_SET_TIMEOUT(function (...a) { liveTimers.delete(h); return fn.apply(this, a) }, ms, ...rest)
|
|
240
|
+
liveTimers.add(h)
|
|
241
|
+
return h
|
|
242
|
+
}
|
|
243
|
+
globalThis.clearTimeout = function (h) { liveTimers.delete(h); return REAL_CLEAR_TIMEOUT(h) }
|
|
244
|
+
|
|
165
245
|
const projRoot = (h) => join(h.WS, 'VibeMath', 'Projects', 'proj')
|
|
166
246
|
const vibeRoot = (h) => join(h.WS, 'VibeMath')
|
|
167
247
|
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
168
248
|
const formalStateOf = (h) => JSON.parse(readIf(join(projRoot(h), 'VibeMath_State', 'formal.json')) || '{}')
|
|
169
|
-
// One scheduler pass.
|
|
170
|
-
//
|
|
171
|
-
const tick = (ms =
|
|
172
|
-
async function waitFor(pred, tries =
|
|
249
|
+
// One scheduler pass. With the poll fast-forwarded above, the poll fires every ~25 ms and a tick
|
|
250
|
+
// runs whenever the plugin's own 200 ms `tickIntervalMs` floor has elapsed, so 260 ms is one pass.
|
|
251
|
+
const tick = (ms = 260) => sleep(ms)
|
|
252
|
+
async function waitFor(pred, tries = 80, ms = 60) {
|
|
173
253
|
for (let i = 0; i < tries; i++) { const v = pred(); if (v) return v; await sleep(ms) }
|
|
174
254
|
return undefined
|
|
175
255
|
}
|
|
176
256
|
const verifiersOf = (h, rKind, exclude) => h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind) && !(exclude || []).some((o) => o.childId === s.childId))
|
|
257
|
+
/** A reply exactly as an agent would emit it (a fenced JSON block) — the framework's real input. */
|
|
258
|
+
const fence = (obj) => '```json\n' + JSON.stringify(obj) + '\n```'
|
|
259
|
+
/** Feed ONE agent reply (a fresh turn's end) to the framework through the real dispatch path. */
|
|
260
|
+
const replyFrom = (h, childId, obj) => h.fireEnd({
|
|
261
|
+
id: childId, runId: 'r-' + childId, provider: 'spawn', local: true, stopReason: 'completed',
|
|
262
|
+
lastAssistantMessage: [{ type: 'text', text: fence(obj) }],
|
|
263
|
+
})
|
|
177
264
|
const fireVerdicts = (h, kids, vale) => {
|
|
178
265
|
for (let i = 0; i < kids.length; i++) {
|
|
179
266
|
h.fireEnd({ id: kids[i].childId, runId: 'v' + i, provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n' + JSON.stringify({ Result: vale, Reason: 'review ' + i }) + '\n```' }] })
|
|
@@ -192,6 +279,12 @@ async function verifyWithDebate(h, rKind, vale, firstRounds = 1, settle = 1) {
|
|
|
192
279
|
// v2 re-wakes the SAME verifier children for every debate round (it does not spawn new
|
|
193
280
|
// ones), so a new round is detected by counting the followups written to those children.
|
|
194
281
|
const kids = new Set()
|
|
282
|
+
// Early exit: a settled object stops producing followups. Without this the loop always spent
|
|
283
|
+
// its FULL 16 passes (~21 s at the old 1.3 s/pass), which is what made this suite take 3
|
|
284
|
+
// minutes: the debate is over, but the helper kept ticking at nothing. Three consecutive
|
|
285
|
+
// passes with no new followup = the framework has nothing left to ask.
|
|
286
|
+
let lastN = -1
|
|
287
|
+
let quiet = 0
|
|
195
288
|
for (let i = 0; i < 16; i++) {
|
|
196
289
|
// The scheduler may need a whole pass before it notices the object and another before it
|
|
197
290
|
// fills the verifier quota, so each wait must span several timer ticks.
|
|
@@ -206,7 +299,8 @@ async function verifyWithDebate(h, rKind, vale, firstRounds = 1, settle = 1) {
|
|
|
206
299
|
// verifier set each pass is safe; the extra answers land before the next round's wakes.
|
|
207
300
|
fireVerdicts(h, cand, rounds <= firstRounds ? vale : settle)
|
|
208
301
|
await sleep(200)
|
|
209
|
-
await tick(
|
|
302
|
+
await tick()
|
|
303
|
+
if (h.followups.length === lastN) { if (++quiet >= 3) break } else { quiet = 0; lastN = h.followups.length }
|
|
210
304
|
}
|
|
211
305
|
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
212
306
|
return { first: h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind)), rounds: kids.size ? rounds : 0 }
|
|
@@ -226,6 +320,25 @@ section("1 'off' (default) is a true no-op")
|
|
|
226
320
|
assert(st.params.leanTimeoutMs === 120000, 'leanTimeoutMs defaults to 120000 (got ' + st.params.leanTimeoutMs + ')')
|
|
227
321
|
assert(!!h.toolRegs.find((t) => t.name === 'vibe_math_lean_run') && !!h.toolRegs.find((t) => t.name === 'vibe_math_lean_archive') && !!h.toolRegs.find((t) => t.name === 'vibe_math_lean_lib'),
|
|
228
322
|
'the three Lean tools are registered in every mode (registration is static)')
|
|
323
|
+
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
324
|
+
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
325
|
+
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
326
|
+
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
327
|
+
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
328
|
+
// could not be switched on at all through the tool interface.
|
|
329
|
+
{
|
|
330
|
+
const setSpec = h.toolRegs.find((t) => t.name === 'vibe_math_set_params')
|
|
331
|
+
assert(!!setSpec, "vibe_math_set_params is registered")
|
|
332
|
+
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
333
|
+
'★ vibe_math_set_params publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
334
|
+
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
335
|
+
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
336
|
+
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
337
|
+
}
|
|
338
|
+
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
339
|
+
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
340
|
+
}
|
|
341
|
+
|
|
229
342
|
assert(existsSync(join(vibeRoot(h), 'Formal', 'Lib')) && existsSync(join(vibeRoot(h), 'Formal', 'Proved')), 'the GLOBAL Formal/Lib + Formal/Proved dirs are created outside the project')
|
|
230
343
|
assert(existsSync(join(projRoot(h), 'Formal')) && existsSync(join(projRoot(h), 'Verified', 'Lean')), 'the project Formal/ and Verified/Lean/ dirs are created')
|
|
231
344
|
await h.call('vibe_math_add_problem', { id: 'q1', description: 'off 模式无操作测试' })
|
|
@@ -318,8 +431,14 @@ section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
|
318
431
|
assert(/【Lean 形式化验证(鼓励模式)】/.test(reviewText), '★ the REVIEW prompt carries the Lean section')
|
|
319
432
|
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(reviewText), 'the review prompt states that a passing Lean run shrinks the question to fidelity')
|
|
320
433
|
assert(/实现难度/.test(reviewText), 'the review prompt asks for the implementation-difficulty judgement')
|
|
434
|
+
// NOTE: this assertion is only the WORDING half. Which is exactly the trap the first version
|
|
435
|
+
// of this feature fell into (a green suite guarding a dead channel). The behaviour — the reply
|
|
436
|
+
// really landing in the durable record — is asserted in section 11.
|
|
321
437
|
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(reviewText), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
438
|
+
assert(/"decision":"used\|blocked\|defect"/.test(reviewText), '★ the review contract lists the real decision enum (incl. defect)')
|
|
322
439
|
assert(/vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)/.test(reviewText), 'the review prompt names the three v2 tools')
|
|
440
|
+
assert(/归档可复用定义\/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run)/.test(reviewText), '★ the review prompt says a reusable artifact must run green BEFORE it is archived')
|
|
441
|
+
assert(/LEAN_NOT_FOUND/.test(reviewText) && /宿主无 Lean 工具链/.test(reviewText), '★ the review prompt writes out the missing-toolchain escape hatch (a host without Lean must not deadlock the agent)')
|
|
323
442
|
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
324
443
|
assert(/DEBATE/.test(debate), 'the debate round actually happened (a followup with the debate prompt was issued)')
|
|
325
444
|
assert(/【Lean 形式化验证(鼓励模式)】/.test(debate), '★ the DEBATE prompt carries the Lean section too')
|
|
@@ -330,6 +449,12 @@ section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
|
330
449
|
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ the explorer work prompt carries the 顺手形式化 line')
|
|
331
450
|
assert(!!ex && /vibe_math_lean_archive kind='def'/.test(ex.prompt || ''), 'the work line points at the archive tool for reusable definitions')
|
|
332
451
|
assert(!!ex && /vibe_math_lean_lib 查重/.test(ex.prompt || ''), 'the work line tells members to check the reuse library first')
|
|
452
|
+
assert(!!ex && /归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(ex.prompt || ''), '★ the work line forbids archiving a definition that has not run green')
|
|
453
|
+
assert(!!ex && /"formal":\{"target":"<对象id>","decision":"used\|blocked\|defect"/.test(ex.prompt || ''), '★ the WORK-round contract advertises the formal reply field too (otherwise a working agent has nowhere to write its judgement)')
|
|
454
|
+
// Every agent-facing string must name the REGISTERED tools: an abbreviated `lean_archive`
|
|
455
|
+
// is not a tool that exists, and agents copy these literals verbatim (contract §6 hard req. 1).
|
|
456
|
+
const workText = (ex.prompt || '') + '\n' + reviewText + '\n' + debate
|
|
457
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(workText), '★ no injected prompt names an abbreviated tool (every occurrence is prefixed)')
|
|
333
458
|
const offHost = await makeCase('enc-off')
|
|
334
459
|
await offHost.call('vibe_math_add_problem', { id: 'qN', description: 'x' })
|
|
335
460
|
await startScheduler(offHost)
|
|
@@ -403,6 +528,33 @@ section('4 lean_run executes through the subprocess service and reports honestly
|
|
|
403
528
|
assert(noSubStatus.ok === true, 'the scheduler still answers status after that (nothing was thrown into the loop)')
|
|
404
529
|
}
|
|
405
530
|
|
|
531
|
+
// ---------- 4b. the timeout must STOP the process, not just report it ----------
|
|
532
|
+
// Contract §7: "必须给 cwd…,并对超时调用 handle.terminate()". Reporting LEAN_TIMEOUT while the Lean
|
|
533
|
+
// process keeps running is a silent resource leak, and the framework's own contract says otherwise.
|
|
534
|
+
section('4b lean_run ACTIVELY terminates on timeout (contract §7)')
|
|
535
|
+
{
|
|
536
|
+
const h = await makeCase('timeout')
|
|
537
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
538
|
+
const proj = projRoot(h)
|
|
539
|
+
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
540
|
+
writeFileSync(join(proj, 'Formal', 'hang.lean'), 'theorem t : 1 = 1 := rfl -- HANG\n', 'utf8')
|
|
541
|
+
writeFileSync(join(proj, 'Formal', 'fast.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
542
|
+
const before = terminations.length
|
|
543
|
+
const t0 = Date.now()
|
|
544
|
+
const run = await h.call('vibe_math_lean_run', { file: 'Formal/hang.lean', timeout_ms: 1000 })
|
|
545
|
+
const elapsed = Date.now() - t0
|
|
546
|
+
assert(run.ok === false && run.code === 'LEAN_TIMEOUT', '★ a run that outlives its timeout is reported as LEAN_TIMEOUT (got ' + run.code + ')')
|
|
547
|
+
assert(run.timedOut === true, 'the result carries timedOut=true')
|
|
548
|
+
assert(terminations.length === before + 1 && /hang\.lean/.test(terminations[terminations.length - 1].file),
|
|
549
|
+
'★★ the timeout ACTIVELY called handle.terminate() (graceMs alone does not stop a lingering Lean process)')
|
|
550
|
+
assert(elapsed < 2400, 'the call returned AT its timeout instead of waiting the process out (took ' + elapsed + 'ms)')
|
|
551
|
+
const fast = await h.call('vibe_math_lean_run', { file: 'Formal/fast.lean' })
|
|
552
|
+
assert(fast.ok === true && fast.timedOut === false, 'a normal run still reports success')
|
|
553
|
+
assert(terminations.length === before + 1, 'a normal run terminates nothing')
|
|
554
|
+
await sleep(60)
|
|
555
|
+
assert(liveTimers.size === 0, '★ no timeout timer is left pending after either run (it is cleared as soon as `done` wins)')
|
|
556
|
+
}
|
|
557
|
+
|
|
406
558
|
// ---------- 5. archive: def / lemma / proof / blocked ----------
|
|
407
559
|
section('5 lean_archive writes the contract paths and indexes')
|
|
408
560
|
{
|
|
@@ -427,6 +579,13 @@ section('5 lean_archive writes the contract paths and indexes')
|
|
|
427
579
|
writeFileSync(join(proj, 'Formal', 'src.lean'), 'def copied := 3\n', 'utf8')
|
|
428
580
|
const defFrom = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'copied', from: 'Formal/src.lean' })
|
|
429
581
|
assert(defFrom.ok === true && existsSync(join(libPath, 'copied.lean')), 'kind=def can archive from an existing .lean file')
|
|
582
|
+
// The tool RESULT is agent-facing text too: a definition whose run just failed must not be advertised
|
|
583
|
+
// as "directly importable" (contract §6 hard req. 3 / 实现方案 §9.4 — a red file must not be presented
|
|
584
|
+
// as usable, whatever the framework decides to do with the file itself).
|
|
585
|
+
const defRed = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'polluted', content: 'def polluted := 1 -- FAIL\n' })
|
|
586
|
+
assert(defRed.ok === true && !!defRed.run && defRed.run.ok === false, 'a definition that does not compile is reported as a red run')
|
|
587
|
+
assert(!/可直接 import 复用/.test(defRed.note || ''), '★ a red definition must NOT be advertised as directly reusable')
|
|
588
|
+
assert(/运行未通过/.test(defRed.note || ''), '…and the note says what to do instead')
|
|
430
589
|
const fromOutside = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'escape', from: '../../../../etc/passwd' })
|
|
431
590
|
assert(fromOutside.ok === false && fromOutside.code === 'V2_INVALID_ARGUMENT', 'from=<path outside the VibeMath root> is refused')
|
|
432
591
|
const noName = await h.call('vibe_math_lean_archive', { kind: 'def', content: 'def x := 1\n' })
|
|
@@ -493,10 +652,21 @@ section('6 a passing proof flips the review subject to fidelity')
|
|
|
493
652
|
assert(/忠实性审查/.test(vpText), '★ it tells reviewers the review subject is now fidelity')
|
|
494
653
|
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vpText), 'it enumerates exactly what fidelity means')
|
|
495
654
|
assert(/Verified\/Lean\/r-pFid\.lean/.test(vpText), 'it points at the archived proof')
|
|
496
|
-
assert(
|
|
655
|
+
assert(/一致 → Result = 1/.test(vpText), '★ the fidelity guidance names the REAL field (v2\'s contract field is Result)')
|
|
656
|
+
assert(!/verdict/.test(vpText), '★ the fidelity guidance never names a `verdict` field (that vote would be silently dropped)')
|
|
657
|
+
assert(/发现任何偏差,不要投 0/.test(vpText), '★ a fidelity defect is explicitly NOT to be voted as 0 (it is not a refutation)')
|
|
658
|
+
assert(/formal:\{decision:'defect'/.test(vpText), '★ the reviewers are given the defect reply channel that withdraws the proof')
|
|
659
|
+
assert(!/偏离 → 0/.test(vpText), '★ the "any deviation → 0" instruction is gone (it would fabricate a false conclusion)')
|
|
497
660
|
assert(!/请先判断该对象的\*\*实现难度\*\*/.test(vpText), 'the "judge the difficulty first" wording is gone when a proof already exists')
|
|
661
|
+
// The withdrawal sentence must match the MODE's real strength (contract §4.1 pt.3 / §6.1; this case is
|
|
662
|
+
// 'encourage'): encouraging mode has NO gate, so the framework cannot hold the verdict — promising a
|
|
663
|
+
// hold there is a lie the voter would rely on (AUDIT-CHECKLIST §1.7, "提示词承诺的强度档位").
|
|
664
|
+
assert(/本档没有门禁/.test(vpText), '★ encourage fidelity text says THIS MODE HAS NO GATE (the framework cannot hold the verdict)')
|
|
665
|
+
assert(!/本次裁定\*\*不定论\*\*/.test(vpText), '★ encourage must NOT promise a hold the framework cannot enforce')
|
|
498
666
|
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
499
667
|
assert(/你不需要重新检查推导/.test(debate), '★ the debate prompt for a Lean-passed object also asks for fidelity, not re-derivation')
|
|
668
|
+
assert(/发现任何偏差,不要投 0/.test(debate), '★ and it carries the same no-zero rule in the debate round')
|
|
669
|
+
assert(/本档没有门禁/.test(debate), '★ the debate round carries the same mode-qualified withdrawal wording')
|
|
500
670
|
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlk2', note: '涉及未形式化的分析学前置' })
|
|
501
671
|
await h.call('vibe_math_add_proposition', { id: 'pBlk2', 概述: '已记录阻塞的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
502
672
|
const vs2 = await verifyWithDebate(h, 'r-pBlk2', 0.5, 1)
|
|
@@ -525,6 +695,9 @@ section("7 'require' withholds a verdict until the formal record exists")
|
|
|
525
695
|
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
526
696
|
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
527
697
|
assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
|
|
698
|
+
assert(/vibe_math_lean_archive kind='blocked' note=… 或回执 formal\.note/.test(vp), '★ the require wording names BOTH blocking routes with the FULL tool name')
|
|
699
|
+
assert(/"formal":\{"target":"r-pGate","decision":"used\|blocked\|defect"/.test(vp), '★ the require review contract carries the formal reply field keyed by the verification id')
|
|
700
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(vp), '★ the require prompt contains no abbreviated tool name either')
|
|
528
701
|
fireVerdicts(h, vs, 1)
|
|
529
702
|
// The deferral writes Formal/TODO.md from INSIDE the settle path; waiting for that file is
|
|
530
703
|
// the observable proof that the round settled (the task is dropped right after).
|
|
@@ -662,6 +835,352 @@ section("10 'encourage' never gates (a verdict still lands with no Lean artifact
|
|
|
662
835
|
assert(!existsSync(join(proj, 'Formal', 'TODO.md')) || !/pFree/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'no formalization TODO is created in encourage mode')
|
|
663
836
|
}
|
|
664
837
|
|
|
838
|
+
// ---------- 11. the reply channel is REAL (contract §4 / §6.3 / §10.8) ----------
|
|
839
|
+
// The first version of this feature only WROTE "请在回执的 formal 字段写明难度判断" into the prompt
|
|
840
|
+
// and never parsed it: 177 green assertions guarded a dead channel (AUDIT-CHECKLIST §2.2). These
|
|
841
|
+
// cases feed a genuine agent reply through the framework's own dispatch path (subagent/end →
|
|
842
|
+
// handleVerifier → absorb) and assert the DURABLE record, not the wording.
|
|
843
|
+
section('11 the reply channel really lands in the record (blocked / note validation)')
|
|
844
|
+
{
|
|
845
|
+
const h = await makeCase('reply')
|
|
846
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
847
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
848
|
+
await startScheduler(h)
|
|
849
|
+
await h.call('vibe_math_add_proposition', { id: 'pReply', 概述: '用回执记录阻塞', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
850
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pReply'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
851
|
+
assert(!!vs, 'verifiers were spawned for the reply-channel proposition')
|
|
852
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pReply:0') || {}).prompt || ''
|
|
853
|
+
assert(/"formal":\{"target":"r-pReply","decision":"used\|blocked\|defect"/.test(vp), '★ the review contract itself advertises the formal field, keyed by the verification id')
|
|
854
|
+
if (vs) {
|
|
855
|
+
// ① `blocked` WITH a note, named by the VERIFICATION id → the OBJECT record must be synced too.
|
|
856
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '我判断形式化不划算', formal: { target: 'r-pReply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' } })
|
|
857
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pReply'] && r['r-pReply'].status === 'blocked') ? r : undefined }, 40, 150)
|
|
858
|
+
assert(!!rec, '★ a `formal.decision=blocked` reply is really absorbed — the channel is not dead code')
|
|
859
|
+
assert(!!rec && rec['r-pReply'].decision === 'blocked' && /实分析前置知识/.test(rec['r-pReply'].note || ''), 'the record keeps the decision AND the reason')
|
|
860
|
+
assert(!!rec && !!rec['pReply'] && rec['pReply'].status === 'blocked' && /实分析前置知识/.test(rec['pReply'].note || ''), '★ the OBJECT-id record is synced too (the two id spaces must not drift)')
|
|
861
|
+
assert(/实分析前置知识/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the reply-recorded blocker reaches Formal/Index.md')
|
|
862
|
+
// ② `blocked` WITHOUT a note (a different target) → refused with V2_INVALID_ARGUMENT, no record.
|
|
863
|
+
replyFrom(h, vs[1].childId, { Result: 0.5, Reason: '不想做', formal: { target: 'r-pNoNote', decision: 'blocked' } })
|
|
864
|
+
let acts = ''
|
|
865
|
+
for (let i = 0; i < 25; i++) {
|
|
866
|
+
const st = await h.call('vibe_math_status', {})
|
|
867
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
868
|
+
if (/V2_INVALID_ARGUMENT/.test(acts)) break
|
|
869
|
+
await sleep(120)
|
|
870
|
+
}
|
|
871
|
+
assert(/V2_INVALID_ARGUMENT/.test(acts), '★ a blocked/defect judgement without a note is REJECTED with V2_INVALID_ARGUMENT')
|
|
872
|
+
assert(/没有写明 note/.test(acts), 'the refusal says why (an explicit decision is required, never a silent skip)')
|
|
873
|
+
const recN = formalStateOf(h)
|
|
874
|
+
assert(!(recN.records || {})['r-pNoNote'] && !(recN.records || {})['pNoNote'], 'no record is invented for the refused judgement')
|
|
875
|
+
assert(!/pNoNote/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the refused judgement does not reach the index either')
|
|
876
|
+
// ③ a reply with NO target must not invent an object (safeId('') would fall back to 'anon').
|
|
877
|
+
// The 0.5/0.5 round has no consensus, so the debate round re-woke both children — that is the
|
|
878
|
+
// observable proof that these children are registered and can answer again.
|
|
879
|
+
const woke = await waitFor(() => (h.followups.filter((f) => /DEBATE/.test(f.prompt || '')).length >= 2 ? true : undefined), 40, 150)
|
|
880
|
+
assert(!!woke, 'the debate round re-woke the verifiers (round-2 replies reach the framework again)')
|
|
881
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '漏写 target', formal: { decision: 'blocked', note: '没有写 target' } })
|
|
882
|
+
let acts2 = ''
|
|
883
|
+
for (let i = 0; i < 25; i++) {
|
|
884
|
+
const st = await h.call('vibe_math_status', {})
|
|
885
|
+
acts2 = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
886
|
+
if (/没有 target/.test(acts2)) break
|
|
887
|
+
await sleep(120)
|
|
888
|
+
}
|
|
889
|
+
assert(/没有 target/.test(acts2), '★ a `formal` reply without a target is refused, not guessed')
|
|
890
|
+
const recT = formalStateOf(h)
|
|
891
|
+
assert(!(recT.records || {}).anon, '★ a `formal` reply without a target cannot invent a record (no "anon" object)')
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ---------- 12. `defect`: a fidelity defect is NOT "the proposition is false" (§4.1) ----------
|
|
896
|
+
section('12 a defect reply withdraws the proof, writes the TODO and defers the verdict')
|
|
897
|
+
{
|
|
898
|
+
const h = await makeCase('defect')
|
|
899
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
900
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
901
|
+
await startScheduler(h)
|
|
902
|
+
const proj = projRoot(h)
|
|
903
|
+
// A PASSING Lean proof exists for the object — archived under the OBJECT id, the realistic path.
|
|
904
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' })
|
|
905
|
+
assert(proof.ok === true && proof.passed === true, 'precondition: the object starts Lean-passed')
|
|
906
|
+
const proofFile = join(proj, 'Verified', 'Lean', 'pDefect.lean')
|
|
907
|
+
assert(existsSync(proofFile), 'the archived proof exists before the defect is reported')
|
|
908
|
+
await h.call('vibe_math_add_proposition', { id: 'pDefect', 概述: '形式化写窄了的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
909
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pDefect'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
910
|
+
assert(!!vs, 'verifiers were spawned for the Lean-passed object')
|
|
911
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pDefect:0') || {}).prompt || ''
|
|
912
|
+
assert(/忠实性审查/.test(vp) && /不要投 0/.test(vp), 'the reviewers were told to audit fidelity and NOT to vote 0 on a defect')
|
|
913
|
+
// In REQUIRE mode the framework really does hold the verdict, so THAT promise is the correct one here.
|
|
914
|
+
assert(/本次裁定\*\*不定论\*\*/.test(vp), '★ require fidelity text does promise the hold (the gate really enforces it)')
|
|
915
|
+
assert(!/本档没有门禁/.test(vp), 'require must not claim it has no gate')
|
|
916
|
+
const DEFECT = 'Lean 只证了 n>0 的情形,命题原文是 n≥0'
|
|
917
|
+
if (vs) {
|
|
918
|
+
// A fidelity defect: the voter ABSTAINS (0.3) and records it through the reply channel.
|
|
919
|
+
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '形式化写窄了(弃权)', formal: { target: 'pDefect', decision: 'defect', note: DEFECT } })
|
|
920
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pDefect'] && r['pDefect'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
921
|
+
assert(!!rec, '★ the defect reply is absorbed')
|
|
922
|
+
assert(!!rec && rec['pDefect'].status === 'attempted', '★ the object is DOWNGRADED to attempted (the "passed" status is withdrawn)')
|
|
923
|
+
assert(!!rec && rec['pDefect'].proof === '', '★ the proof pointer is cleared')
|
|
924
|
+
assert(!!rec && rec['pDefect'].note === DEFECT, 'the concrete deviation is recorded on the object record')
|
|
925
|
+
assert(!!rec && !!rec['r-pDefect'] && rec['r-pDefect'].status === 'attempted' && rec['r-pDefect'].decision === 'defect', '★ the verification-id record is downgraded too (both id spaces)')
|
|
926
|
+
assert(!existsSync(proofFile), '★ the archived proof Verified/Lean/pDefect.lean is DELETED')
|
|
927
|
+
assert(existsSync(join(proj, 'Formal', 'pDefect.lean')), 'the working file Formal/pDefect.lean is kept (the code is not lost)')
|
|
928
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
929
|
+
assert(/pDefect/.test(todo) && /defect/.test(todo), '★ the object is listed in Formal/TODO.md with the defect reason')
|
|
930
|
+
assert(/只证了 n>0 的情形/.test(todo), 'the TODO carries the concrete deviation, not just a flag')
|
|
931
|
+
let acts = ''
|
|
932
|
+
for (let i = 0; i < 25; i++) {
|
|
933
|
+
const st = await h.call('vibe_math_status', {})
|
|
934
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
935
|
+
if (/忠实性缺陷/.test(acts)) break
|
|
936
|
+
await sleep(120)
|
|
937
|
+
}
|
|
938
|
+
assert(/忠实性缺陷/.test(acts), 'the downgrade is announced on the v2-readable channel (activity log)')
|
|
939
|
+
assert(/撤回「已通过」状态/.test(acts), 'the announcement says the passed status was withdrawn')
|
|
940
|
+
assert(/本次裁定不定论/.test(acts), 'and that the verdict is undecided — a defect withdraws a proof, it does not refute the proposition')
|
|
941
|
+
// Now drive the same round to a UNANIMOUS "true": without the defect it would conclude.
|
|
942
|
+
const settled = await verifyWithDebate(h, 'r-pDefect', 1, 0)
|
|
943
|
+
assert(!!settled.first, 'the round was driven to a verdict after the defect')
|
|
944
|
+
const todo2 = await waitFor(() => { const t = readIf(join(proj, 'Formal', 'TODO.md')); return /formal-required/.test(t) ? t : undefined }, 40, 150)
|
|
945
|
+
assert(!!todo2 && /r-pDefect/.test(todo2), '★ require mode DEFERS after a defect (the object enters the formalization TODO as formal-required)')
|
|
946
|
+
await tick(400)
|
|
947
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
948
|
+
const p = props.find((x) => x.id === 'pDefect') || {}
|
|
949
|
+
assert(p.布尔估计 === 0.5, '★★ the proposition is NOT recorded as false — 布尔估计 unchanged (got ' + p.布尔估计 + ')')
|
|
950
|
+
assert(!(p.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was written')
|
|
951
|
+
assert(p.优先级 !== 'never', 'the priority was not pinned to never')
|
|
952
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card: the verdict is UNDECIDED, not "false"')
|
|
953
|
+
}
|
|
954
|
+
// The reverse direction: a defect named by the VERIFICATION id must still downgrade the OBJECT
|
|
955
|
+
// record that actually holds the proof (this is the pair that silently drifts when only one side
|
|
956
|
+
// is written — formalGateRecord would read `passed` from the other side).
|
|
957
|
+
const h2 = await makeCase('defect-rid')
|
|
958
|
+
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
959
|
+
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
960
|
+
await startScheduler(h2)
|
|
961
|
+
const proj2 = projRoot(h2)
|
|
962
|
+
await h2.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect2', content: 'theorem p_defect2 : 2 + 2 = 4 := by decide\n' })
|
|
963
|
+
await h2.call('vibe_math_add_proposition', { id: 'pDefect2', 概述: '回执用验证 id 命名的缺陷', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
964
|
+
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pDefect2'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
965
|
+
assert(!!vs2, 'verifiers were spawned for the reverse-direction case')
|
|
966
|
+
if (vs2) {
|
|
967
|
+
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '写宽了', formal: { target: 'r-pDefect2', decision: 'defect', note: 'Lean 版本没有假设 n≥1,比原文更宽' } })
|
|
968
|
+
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['r-pDefect2'] && r['r-pDefect2'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
969
|
+
assert(!!rec2, '★ a defect named by the verification id is absorbed')
|
|
970
|
+
assert(!!rec2 && !!rec2['pDefect2'] && rec2['pDefect2'].status === 'attempted', '★ and the OBJECT record that held `passed` is downgraded as well')
|
|
971
|
+
assert(!!rec2 && rec2['pDefect2'].proof === '', 'its proof pointer is cleared too')
|
|
972
|
+
assert(!existsSync(join(proj2, 'Verified', 'Lean', 'pDefect2.lean')), '★ the archived proof is deleted even though the reply named the other id')
|
|
973
|
+
assert(/pDefect2/.test(readIf(join(proj2, 'Formal', 'TODO.md'))), 'the object is on the formalization TODO')
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ---------- 13. the gate must read BOTH id spaces ----------
|
|
978
|
+
// v2 has two id spaces that name the same object: the verification id (rId, `r-pX`, `r-pX-s0`) that the
|
|
979
|
+
// verification prompts and the gate use, and the OBJECT id (`pX`) that the agent reads in the target
|
|
980
|
+
// block and normally archives under. `lean_archive` can only ADD a record for the id it was given, and
|
|
981
|
+
// `syncVerificationTarget` merely UPDATES aliases that already exist — so "archived under the object id,
|
|
982
|
+
// rId record does not exist yet" is a real first-time state. A gate that reads only its own side calls
|
|
983
|
+
// that object un-formalized (a false negative), and because the deferral itself materialises the rId
|
|
984
|
+
// record as `status:'none'`, the object can then NEVER conclude: every round re-defers and re-debates
|
|
985
|
+
// (AUDIT-CHECKLIST §3: "成对关系只做一半").
|
|
986
|
+
section('13 the require gate reads BOTH id spaces (archiving under the OBJECT id must not wedge)')
|
|
987
|
+
{
|
|
988
|
+
const h = await makeCase('gate-objid')
|
|
989
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
990
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
991
|
+
await startScheduler(h)
|
|
992
|
+
const proj = projRoot(h)
|
|
993
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pObjId', content: 'theorem p_objid : 2 + 2 = 4 := by decide\n' })
|
|
994
|
+
assert(proof.ok === true && proof.passed === true, 'precondition: the object is Lean-passed under its OBJECT id')
|
|
995
|
+
const rec0 = formalStateOf(h)
|
|
996
|
+
assert(!!rec0.records && !!rec0.records.pObjId && rec0.records.pObjId.status === 'passed', 'the passed record lives under the object id')
|
|
997
|
+
assert(!(rec0.records || {})['r-pObjId'], 'precondition: no rId record exists yet (this is the state that used to fool the gate)')
|
|
998
|
+
await h.call('vibe_math_add_proposition', { id: 'pObjId', 概述: '用对象 id 归档后必须能定论', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
999
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pObjId'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1000
|
+
assert(!!vs, 'verifiers were spawned for the object-id case')
|
|
1001
|
+
fireVerdicts(h, vs || [], 1)
|
|
1002
|
+
await tick(600)
|
|
1003
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
1004
|
+
const p = props.find((x) => x.id === 'pObjId') || {}
|
|
1005
|
+
assert(p.布尔估计 === 1, "★ a Lean-passed object DOES conclude when the proof was archived under the object id (got " + p.布尔估计 + ')')
|
|
1006
|
+
const cards = JSON.parse(readIf(join(proj, 'Verified', '数论_Verified.json')) || '[]')
|
|
1007
|
+
assert(!!cards.find((c) => c.id === 'pObjId'), 'the Verified card was written')
|
|
1008
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
1009
|
+
assert(!/pObjId/.test(todo), '★ the already-formalized object is NOT put on the formalization TODO')
|
|
1010
|
+
const todo2 = await h.call('vibe_math_status', {})
|
|
1011
|
+
assert(!(todo2.formal.todo || []).some((t) => String(t.id).indexOf('pObjId') !== -1), 'and the status TODO is empty for it too')
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ---------- 14. withdrawing a proof must reach EVERY archived copy ----------
|
|
1015
|
+
// Contract §4.1 requires the withdrawal, not a best-effort delete: after a `defect` the archived proof
|
|
1016
|
+
// must be gone from `Verified/Lean/` — the one place everyone looks for "the proof of this object".
|
|
1017
|
+
// Two independent ways that fails silently:
|
|
1018
|
+
// (1) the proof was archived under a DIFFERENT id than the one the reply names (`Verified/Lean/<rId>.lean`
|
|
1019
|
+
// while the reply names the object id) — scanning only the two named ids misses it, and the record's
|
|
1020
|
+
// `proof` pointer is cleared anyway, so nothing ever points at the orphan again;
|
|
1021
|
+
// (2) the host cannot delete at all (no subprocess service) — the delete fails and the stale proof stays.
|
|
1022
|
+
section('14 defect withdrawal covers every id alias AND a host that cannot delete')
|
|
1023
|
+
{
|
|
1024
|
+
// (a) archived under the VERIFICATION id, defect named by the OBJECT id.
|
|
1025
|
+
const h = await makeCase('defect-alias')
|
|
1026
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1027
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1028
|
+
await startScheduler(h)
|
|
1029
|
+
const proj = projRoot(h)
|
|
1030
|
+
const pr = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'r-pAlias', content: 'theorem p_alias : 2 + 2 = 4 := by decide\n' })
|
|
1031
|
+
assert(pr.ok === true && pr.passed === true, 'precondition: the proof is archived under the VERIFICATION id')
|
|
1032
|
+
const aliasProof = join(proj, 'Verified', 'Lean', 'r-pAlias.lean')
|
|
1033
|
+
assert(existsSync(aliasProof), 'the archived proof sits at Verified/Lean/<rId>.lean')
|
|
1034
|
+
await h.call('vibe_math_add_proposition', { id: 'pAlias', 概述: '归档写在验证 id 上', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1035
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pAlias'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1036
|
+
assert(!!vs, 'verifiers were spawned for the alias case')
|
|
1037
|
+
if (vs) {
|
|
1038
|
+
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '写宽了(弃权)', formal: { target: 'pAlias', decision: 'defect', note: '原文还有 n≥1 的假设' } })
|
|
1039
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pAlias'] && r['pAlias'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1040
|
+
assert(!!rec, '★ the defect named by the object id is absorbed')
|
|
1041
|
+
assert(!existsSync(aliasProof), '★★ the proof archived under the VERIFICATION id is withdrawn too (every id alias, not just the two named ones)')
|
|
1042
|
+
assert(!!rec && (!rec['r-pAlias'] || rec['r-pAlias'].proof === ''), 'the rId record no longer points at a proof')
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// (b) a host whose deletion cannot work: the archived path must not keep reading as the proof.
|
|
1046
|
+
const h2 = await makeCase('defect-nosub', { noSubprocess: true })
|
|
1047
|
+
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1048
|
+
const proj2 = projRoot(h2)
|
|
1049
|
+
mkdirSync(join(proj2, 'Verified', 'Lean'), { recursive: true })
|
|
1050
|
+
mkdirSync(join(proj2, 'Formal'), { recursive: true })
|
|
1051
|
+
const WORK = 'theorem p_stale : 2 + 2 = 4 := by decide\n'
|
|
1052
|
+
writeFileSync(join(proj2, 'Formal', 'pStale.lean'), WORK, 'utf8')
|
|
1053
|
+
writeFileSync(join(proj2, 'Verified', 'Lean', 'pStale.lean'), WORK, 'utf8')
|
|
1054
|
+
// The record is written directly (this host has no subprocess, so the LEAN tools cannot run at all):
|
|
1055
|
+
// what is under test is the WITHDRAWAL path, not the archiving path.
|
|
1056
|
+
writeFileSync(join(proj2, 'VibeMath_State', 'formal.json'), JSON.stringify({ records: { pStale: { status: 'passed', file: 'Formal/pStale.lean', proof: 'Verified/Lean/pStale.lean', decision: 'used', updatedAt: 1 } }, todo: [] }), 'utf8')
|
|
1057
|
+
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1058
|
+
await h2.call('vibe_math_add_proposition', { id: 'pStale', 概述: '宿主删不掉归档证明时的撤回', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1059
|
+
await startScheduler(h2)
|
|
1060
|
+
const st0 = await h2.call('vibe_math_status', {})
|
|
1061
|
+
assert(st0.formal.objects.some((o) => o.target === 'pStale' && o.status === 'passed'), 'precondition: the passed record with an archived proof survives resume on the no-subprocess host')
|
|
1062
|
+
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pStale'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1063
|
+
assert(!!vs2, 'verifiers were spawned on the no-subprocess host')
|
|
1064
|
+
if (vs2) {
|
|
1065
|
+
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '不忠实(弃权)', formal: { target: 'pStale', decision: 'defect', note: 'Lean 少了 n≥1' } })
|
|
1066
|
+
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['pStale'] && r['pStale'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1067
|
+
assert(!!rec2, '★ the defect is absorbed even though this host cannot run a delete')
|
|
1068
|
+
assert(!!rec2 && rec2['pStale'].status === 'attempted' && rec2['pStale'].proof === '', 'the record is downgraded and its proof pointer cleared')
|
|
1069
|
+
const stalePath = join(proj2, 'Verified', 'Lean', 'pStale.lean')
|
|
1070
|
+
assert(existsSync(stalePath), 'the archived file could NOT be deleted here (no subprocess) — so the fallback has to handle it')
|
|
1071
|
+
const body = readIf(stalePath)
|
|
1072
|
+
assert(!/theorem p_stale/.test(body), '★★ the original proof text is GONE from the archived path (it can no longer be read as the proof)')
|
|
1073
|
+
assert(/已撤回/.test(body), '★★ …and that path carries an explicit withdrawal notice instead')
|
|
1074
|
+
assert(/Formal\/pStale\.lean/.test(body), 'the notice points at the working file that is kept')
|
|
1075
|
+
const acts = (await h2.call('vibe_math_status', {})).recentActivity.map((a) => a.detail).join('\n')
|
|
1076
|
+
assert(/覆写/.test(acts), '★ the announcement says WHAT actually happened (overwritten, not deleted)')
|
|
1077
|
+
assert(/撤回「已通过」状态/.test(acts), 'the announcement still says the passed status was withdrawn')
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// ---------- 15. the「判断命题」transfer is a verdict on the SOURCE proposition ----------
|
|
1082
|
+
// When a solver's solution for a "判断下述命题是否成立:X" problem is verified, the result is TRANSFERRED
|
|
1083
|
+
// onto the source proposition: `布尔估计 = v`, `已验证 = true`, a probability-1 proof/refutation entry,
|
|
1084
|
+
// and `优先级 = 'never'` for a boolean v. That IS a boolean verdict on X, so require mode must gate it —
|
|
1085
|
+
// otherwise a proposition nobody formalized is silently concluded (and permanently de-scheduled).
|
|
1086
|
+
section('15 the require gate also covers the judge-problem transfer')
|
|
1087
|
+
{
|
|
1088
|
+
const h = await makeCase('judge-gate')
|
|
1089
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1090
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1091
|
+
const proj = projRoot(h)
|
|
1092
|
+
await h.call('vibe_math_add_proposition', { id: 'pJudgeSrc', 概述: '被判断的源命题(未形式化)', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1093
|
+
await h.call('vibe_math_add_problem', { id: 'qJudge', description: '判断下述命题是否成立:pJudgeSrc', priority: 1 })
|
|
1094
|
+
const qsFile = join(proj, 'qs', 'qs.json')
|
|
1095
|
+
const qs0 = JSON.parse(readIf(qsFile) || '[]')
|
|
1096
|
+
const qj = qs0.find((q) => q.id === 'qJudge')
|
|
1097
|
+
qj.判断命题 = 'pJudgeSrc'
|
|
1098
|
+
// A solver-produced solution carries no 来源列表 — that is exactly the branch that transfers to the source.
|
|
1099
|
+
qj.解法列表 = [{ 完整解法: '该命题不成立的论证', 正确概率: 0.8, 已验: false }]
|
|
1100
|
+
writeFileSync(qsFile, JSON.stringify(qs0, null, 2), 'utf8')
|
|
1101
|
+
await startScheduler(h)
|
|
1102
|
+
const sp = await waitFor(() => { const x = verifiersOf(h, 'r-qJudge-s0'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1103
|
+
assert(!!sp, 'verifiers were spawned for the judge problem solution')
|
|
1104
|
+
fireVerdicts(h, sp || [], 0)
|
|
1105
|
+
await tick(600)
|
|
1106
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
1107
|
+
const ap = props.find((x) => x.id === 'pJudgeSrc') || {}
|
|
1108
|
+
assert(ap.布尔估计 === 0.5, "★★ the source proposition's 布尔估计 is UNCHANGED (require mode must not conclude 假 through the transfer; got " + ap.布尔估计 + ')')
|
|
1109
|
+
assert(!(ap.证伪列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 refutation was written onto the source proposition')
|
|
1110
|
+
assert(ap.优先级 !== 'never', 'the source proposition was not pinned to never')
|
|
1111
|
+
assert(ap.已验证 !== true, '★ it stays re-verifiable (the ungated path also marked it 已验证, removing it from every future candidate set)')
|
|
1112
|
+
assert(/pJudgeSrc/.test(readIf(join(proj, 'Formal', 'TODO.md'))), '★ the deferred source proposition is on the formalization TODO')
|
|
1113
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), 'no Verified card for the source proposition')
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ---------- 16. the prompt corpus (contract §10.10) ----------
|
|
1117
|
+
// A HUMAN must be able to re-read every prompt the framework emitted, not just the assertions
|
|
1118
|
+
// about them. Paths are normalised so the dump is deterministic, diffable and machine-free.
|
|
1119
|
+
section('16 the captured prompt corpus is written for human review')
|
|
1120
|
+
{
|
|
1121
|
+
// Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
|
|
1122
|
+
// prompt between two runs and make the corpus non-deterministic.
|
|
1123
|
+
for (const h of hosts) { try { await h.call('vibe_math_pause', {}) } catch (e) { /* ignore */ } }
|
|
1124
|
+
const scrub = (h, s) => {
|
|
1125
|
+
const ws = String(h.WS)
|
|
1126
|
+
const slash = ws.replace(/\\/g, '/')
|
|
1127
|
+
return String(s == null ? '' : s)
|
|
1128
|
+
.split(slash + '/VibeMath').join('<VIBEMATH>')
|
|
1129
|
+
.split(ws + '\\VibeMath').join('<VIBEMATH>')
|
|
1130
|
+
.split(slash).join('<WS>')
|
|
1131
|
+
.split(ws).join('<WS>')
|
|
1132
|
+
}
|
|
1133
|
+
const entries = []
|
|
1134
|
+
for (const h of hosts) {
|
|
1135
|
+
for (const s of h.spawns) entries.push({ kind: 'spawn', case: h.label, label: s.label, prompt: scrub(h, s.prompt) })
|
|
1136
|
+
for (const f of h.followups) {
|
|
1137
|
+
const owner = h.spawns.find((s) => s.childId === f.childId)
|
|
1138
|
+
entries.push({ kind: 'wake', case: h.label, label: owner ? owner.label : f.childId, prompt: scrub(h, f.prompt) })
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
1142
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.json'), JSON.stringify({ entries: entries }, null, 2), 'utf8')
|
|
1143
|
+
const md = ['# V2 形式化验证交互语料(prompt corpus)', '',
|
|
1144
|
+
'> 由 `formal-verify-v2.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。',
|
|
1145
|
+
'> 工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,因此可 diff、不泄露本机路径。',
|
|
1146
|
+
'> 覆盖:off 档(无任何 Lean 文字)、encourage 与 require 的表决初评/辩论、passed 后的忠实性分支、',
|
|
1147
|
+
'> 以及平时工作轮的「顺手形式化」段落与 formal 回执契约。', '']
|
|
1148
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1149
|
+
const e = entries[i]
|
|
1150
|
+
md.push('## [' + i + '] ' + e.kind + ' · ' + e.label + ' · case=' + e.case)
|
|
1151
|
+
md.push('')
|
|
1152
|
+
md.push('```text')
|
|
1153
|
+
md.push(e.prompt)
|
|
1154
|
+
md.push('```')
|
|
1155
|
+
md.push('')
|
|
1156
|
+
}
|
|
1157
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.md'), md.join('\n'), 'utf8')
|
|
1158
|
+
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v2.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v2.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
1159
|
+
assert(entries.length >= 15, 'the corpus covers the whole run (' + entries.length + ' prompts)')
|
|
1160
|
+
assert(entries.some((e) => /【Lean 形式化验证(鼓励模式)】/.test(e.prompt)), 'the corpus contains the encourage verify prompt')
|
|
1161
|
+
assert(entries.some((e) => /【Lean 形式化验证(强制模式)】/.test(e.prompt)), '★ the corpus contains the REQUIRE verify prompt')
|
|
1162
|
+
assert(entries.some((e) => /你不需要重新检查推导/.test(e.prompt) && /一致 → Result = 1/.test(e.prompt)), 'the corpus contains the passed/fidelity prompt')
|
|
1163
|
+
assert(entries.some((e) => /【顺手形式化(鼓励)】/.test(e.prompt)), 'the corpus contains the work-round 顺手形式化 prompt')
|
|
1164
|
+
assert(entries.some((e) => /【顺手形式化/.test(e.prompt) && /"formal":\{"target":"<对象id>","decision":"used\|blocked\|defect"/.test(e.prompt)), 'the corpus contains the formal reply contract line')
|
|
1165
|
+
assert(entries.some((e) => !/Lean|形式化/.test(e.prompt)), 'the corpus contains off-mode prompts with no Lean text at all')
|
|
1166
|
+
assert(entries.some((e) => e.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
|
|
1167
|
+
// Generic sweeps over EVERY captured prompt, not spot checks (AUDIT §2.1).
|
|
1168
|
+
const joined = entries.map((e) => e.prompt).join('\n')
|
|
1169
|
+
const bare = entries.filter((e) => /(^|[^a-z_])lean_(run|archive|lib)/.test(e.prompt))
|
|
1170
|
+
assert(bare.length === 0, '★ no captured prompt names an abbreviated tool (' + bare.map((b) => b.label).join(',') + ')')
|
|
1171
|
+
const zero = entries.filter((e) => /偏离\s*→\s*0/.test(e.prompt))
|
|
1172
|
+
assert(zero.length === 0, '★ no captured prompt turns a fidelity defect into a 0 vote (' + zero.map((z) => z.label).join(',') + ')')
|
|
1173
|
+
const wrongField = entries.filter((e) => /忠实性/.test(e.prompt) && /verdict/.test(e.prompt))
|
|
1174
|
+
assert(wrongField.length === 0, '★ no fidelity prompt names a `verdict` field (' + wrongField.map((w) => w.label).join(',') + ')')
|
|
1175
|
+
const dirty = entries.filter((e) => /\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(e.prompt))
|
|
1176
|
+
assert(dirty.length === 0, 'no captured prompt contains placeholder garbage (' + dirty.map((d) => d.label).join(',') + ')')
|
|
1177
|
+
assert(joined.indexOf('<VIBEMATH>') !== -1, 'the VibeMath root is normalised to <VIBEMATH>')
|
|
1178
|
+
for (const h of hosts) {
|
|
1179
|
+
const ws = String(h.WS)
|
|
1180
|
+
assert(joined.indexOf(ws) === -1 && joined.indexOf(ws.replace(/\\/g, '/')) === -1, 'no captured prompt leaks a machine path (' + h.label + ')')
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
665
1184
|
// cleanup
|
|
666
1185
|
for (const h of hosts) { try { rmSync(h.WS, { recursive: true, force: true }) } catch (e) { /* ignore */ } }
|
|
667
1186
|
|