dsh-vibe-math 2.2.2 → 2.3.1
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 +61 -3
- package/README.md +119 -1
- package/RELEASE-NOTES-2.3.0.md +207 -0
- package/RELEASE-NOTES-2.3.1.md +134 -0
- package/audit-formal-sensitivity.mjs +333 -0
- package/audit-persona-sensitivity.mjs +249 -0
- package/audit-persona-surface.test.mjs +349 -0
- package/audit-v5-integrity.mjs +43 -2
- package/audit-v5-sensitivity.mjs +77 -6
- package/docs/formal-verification.md +401 -0
- package/docs/generate_framework_diagram_v5.mjs +22 -16
- package/docs/test-timing.md +79 -0
- package/formal-verify-v2.test.mjs +951 -0
- package/formal-verify-v3.test.mjs +1031 -0
- package/formal-verify-v4.test.mjs +882 -0
- package/formal-verify-v5.test.mjs +598 -0
- package/package.json +22 -2
- package/prompt-corpus-persona/persona-corpus.json +32 -0
- package/prompt-corpus-persona/persona-corpus.md +674 -0
- package/prompt-corpus-v2/formal-verify-v2.json +394 -0
- package/prompt-corpus-v2/formal-verify-v2.md +4250 -0
- package/prompt-corpus-v3/formal-verify-v3.json +382 -0
- package/prompt-corpus-v3/formal-verify-v3.md +3843 -0
- package/prompt-corpus-v4/formal-verify-v4.json +84 -0
- package/prompt-corpus-v4/formal-verify-v4.md +255 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +109 -5
- package/prompt-corpus-v5/prompt-corpus-v5.md +653 -109
- package/prompt-v5-integrity.test.mjs +1158 -984
- package/run-tests.mjs +99 -0
- package/vibe-math-v2/agent.cordis.yml +40 -2
- package/vibe-math-v2/vibe-math-v2.js +811 -21
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +218 -1
- package/vibe-math-v3/agent.cordis.yml +46 -2
- package/vibe-math-v3/vibe-math-v3.js +810 -21
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +104 -2
- package/vibe-math-v4/agent.cordis.yml +46 -4
- package/vibe-math-v4/vibe-math-v4.js +744 -15
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +255 -0
- package/vibe-math-v5/agent.cordis.yml +41 -5
- package/vibe-math-v5/vibe-math-v5.js +621 -9
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +131 -4
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +57 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +51 -46
|
@@ -46,12 +46,26 @@ export function apply(ctx) {
|
|
|
46
46
|
// toolFilter (scoped tools.restrict() in the child). Empty = inherit all tools.
|
|
47
47
|
// CAUTION: only set one of these; an empty allow:[] would deny EVERY tool.
|
|
48
48
|
toolAllow: [], toolDeny: [],
|
|
49
|
+
// ---- Lean formal verification (docs/formal-verification.md) ----------------
|
|
50
|
+
// 'off' (default, a TRUE no-op) | 'encourage' | 'require'. The MODE is dynamic: every
|
|
51
|
+
// mode-dependent prompt string is computed from params.formalVerify at the moment the
|
|
52
|
+
// prompt is built, never frozen into a brief, so switching the knob takes effect on the
|
|
53
|
+
// very next wake.
|
|
54
|
+
formalVerify: 'off',
|
|
55
|
+
leanCommand: 'lean',
|
|
56
|
+
leanArgs: [], // inserted BEFORE the file name (e.g. ['env','lean'] with lake)
|
|
57
|
+
leanTimeoutMs: 120000,
|
|
49
58
|
}
|
|
50
59
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
51
60
|
let running = false, autoDone = false, phase = 'idle'
|
|
52
61
|
let residents = new Map(), mailboxes = new Map(), taskboard = [], decisions = []
|
|
53
62
|
let meetings = [], reports = [], activityLog = []
|
|
54
63
|
let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
|
|
64
|
+
// Lean formalization records, keyed by object id. v4 has no session-log projection, so this
|
|
65
|
+
// is persisted through v4's OWN durable State/*.json mechanism (State/formal.json) and must
|
|
66
|
+
// survive `resume` — otherwise a require-mode object would lose the very record that decides
|
|
67
|
+
// whether its verdict may take effect.
|
|
68
|
+
let formal = {}, formalTodos = []
|
|
55
69
|
let meetingState = null, verifyState = null, pendingVerify = [], pendingMeeting = null // pendingVerify: FIFO queue (several residents may independently propose different objects before any verify runs — a single slot silently DROPPED all but the last proposal)
|
|
56
70
|
let busy = new Set(), wakeKind = new Map(), currentResident = ''
|
|
57
71
|
let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
|
|
@@ -132,6 +146,17 @@ export function apply(ctx) {
|
|
|
132
146
|
if(isWindows()) return 'New-Item -Force -ItemType Directory -Path '+paths.map(psQuote).join(',')+' | Out-Null'
|
|
133
147
|
return 'mkdir -p '+paths.map(shQuote).join(' ')
|
|
134
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Delete command for the same two interpreters. DSH's `fs` service has no unlink/remove at all
|
|
151
|
+
* (dsh-fs FileSystem: resolve/stat/readText/writeText/editText/listDir), so the ONE place this
|
|
152
|
+
* preset must remove a file — withdrawing a retracted proof on `defect` (spec §4.1) — goes
|
|
153
|
+
* through the platform shell helper the preset already uses for mkdir. `-ErrorAction
|
|
154
|
+
* SilentlyContinue` / `-f` keep it idempotent: deleting an already-absent file is a success.
|
|
155
|
+
*/
|
|
156
|
+
function rmCmd(paths){
|
|
157
|
+
if(isWindows()) return 'Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath '+paths.map(psQuote).join(',')
|
|
158
|
+
return 'rm -f '+paths.map(shQuote).join(' ')
|
|
159
|
+
}
|
|
135
160
|
async function runShell(script,cwd){
|
|
136
161
|
const subprocess=subprocessOf(); if(subprocess===undefined) return {ok:false,error:'no-subprocess'}
|
|
137
162
|
try {
|
|
@@ -200,7 +225,15 @@ export function apply(ctx) {
|
|
|
200
225
|
} catch(e){}
|
|
201
226
|
return true
|
|
202
227
|
}
|
|
203
|
-
async function ensureDirs(){
|
|
228
|
+
async function ensureDirs(){
|
|
229
|
+
const base=frameworkRoot()
|
|
230
|
+
const dirs=['Problems','Progress','Propos','Methods','Subproblems','Shared/meetings','Shared/debates','Verified/命题','Verified/问题','Verified/Lean','Formal','Reliable','Notes','State']
|
|
231
|
+
// The GLOBAL reuse library (Formal/Lib + Formal/Proved) deliberately lives beside the
|
|
232
|
+
// project tree, NOT inside it: cross-project reuse is the whole point (spec §3). It is
|
|
233
|
+
// created here so the first `lean_archive kind='def'` never has to invent its parent.
|
|
234
|
+
const globalDirs=['Formal/Lib','Formal/Proved']
|
|
235
|
+
return await runShell(mkdirCmd([vibeRoot()+'/Projects'].concat(dirs.map(d=>base+'/'+d)).concat(globalDirs.map(d=>vibeRoot()+'/'+d))))
|
|
236
|
+
}
|
|
204
237
|
async function readTextAbs(path){ try { const t=await fs.resolve(path); const s=await fs.stat(t); if(s===undefined) return undefined; return await fs.readText(t) } catch(e){ return undefined } }
|
|
205
238
|
async function writeTextAbs(path,content){ try { const t=await fs.resolve(path); await fs.writeText(t,content,undefined,undefined,getPolicy()); return true } catch(e){ return false } }
|
|
206
239
|
async function readCurrentProject(){ try { const t=await readTextAbs(vibeRoot()+'/.current'); if(t) return String(t).trim() } catch(e){} return currentProject }
|
|
@@ -213,12 +246,525 @@ export function apply(ctx) {
|
|
|
213
246
|
return obj||{}
|
|
214
247
|
}
|
|
215
248
|
|
|
249
|
+
// ================= Lean formal verification ==============================
|
|
250
|
+
// Contract: docs/formal-verification.md (shared by v2/v3/v4/v5).
|
|
251
|
+
//
|
|
252
|
+
// The point of this feature is a SHIFT IN WHAT MUST BE REVIEWED, not an extra chore.
|
|
253
|
+
// Unanimous consensus answers "do we all believe this?"; a machine-checked Lean
|
|
254
|
+
// development answers "is this true?" and shrinks the open question to the one thing a
|
|
255
|
+
// human (or an agent) can actually audit:
|
|
256
|
+
//
|
|
257
|
+
// do the Lean definitions / objects / conditions / assumptions / conclusion
|
|
258
|
+
// match the proposition as originally stated?
|
|
259
|
+
//
|
|
260
|
+
// So once a Lean run passes, the voting prompt stops asking a resident to redo the
|
|
261
|
+
// derivation and asks for a FIDELITY review. `require` mode makes that concrete: a
|
|
262
|
+
// 真/假 verdict does not take effect until the object is either `passed` (a green run)
|
|
263
|
+
// or carries an explicit, reasoned `blocked` record — "decide by difficulty, but decide
|
|
264
|
+
// out loud, and never silently skip".
|
|
265
|
+
const FORMAL_MODES=['off','encourage','require']
|
|
266
|
+
// Read the mode OFF `params` every single time. Nothing mode-dependent may be cached in a
|
|
267
|
+
// brief/closure: the knob is switchable at runtime and members must see the NEW text on
|
|
268
|
+
// their very next wake.
|
|
269
|
+
const formalMode=()=>{ const m=String(params.formalVerify); return FORMAL_MODES.indexOf(m)!==-1?m:'off' }
|
|
270
|
+
const formalOn=()=>formalMode()!=='off'
|
|
271
|
+
const formalRoot=()=>frameworkRoot()+'/Formal'
|
|
272
|
+
const formalLibRoot=()=>vibeRoot()+'/Formal/Lib'
|
|
273
|
+
const formalProvedRoot=()=>vibeRoot()+'/Formal/Proved'
|
|
274
|
+
const verifiedLeanRoot=()=>frameworkRoot()+'/Verified/Lean'
|
|
275
|
+
const formalRecords=()=>(formal||{})
|
|
276
|
+
const formalTodo=()=>(formalTodos||[])
|
|
277
|
+
function formalOf(target){
|
|
278
|
+
const key=idSafe(String(target==null?'':target))
|
|
279
|
+
if(!key||key==='id') return {status:'none'}
|
|
280
|
+
const r=formalRecords()[key]
|
|
281
|
+
return r||{status:'none'}
|
|
282
|
+
}
|
|
283
|
+
const formalKey=(target)=>{ const k=idSafe(String(target==null?'':target)); return (k&&k!=='id')?k:'' }
|
|
284
|
+
/**
|
|
285
|
+
* Write one object's formal record (and optionally the TODO list) to v4's durable state.
|
|
286
|
+
* ASYNC on purpose: the write goes through v4's per-file serialized `writeJson` queue, and a
|
|
287
|
+
* caller that only fires-and-forgets it could have the process die (or `resume` read the file)
|
|
288
|
+
* before the record lands. Every caller here awaits the result.
|
|
289
|
+
*/
|
|
290
|
+
async function putFormal(target,record,todo){
|
|
291
|
+
const key=formalKey(target); if(!key) return false
|
|
292
|
+
formal[key]=record
|
|
293
|
+
if(Array.isArray(todo)) formalTodos=todo
|
|
294
|
+
await saveFormal()
|
|
295
|
+
return true
|
|
296
|
+
}
|
|
297
|
+
function saveFormal(){ return writeJson('State/formal.json',{records:formal,todo:formalTodos}) }
|
|
298
|
+
// `passed` requires a GREEN RUN, not merely an archived file: a proof file that has never
|
|
299
|
+
// been executed proves nothing, so a hand-written file cannot buy its way past the gate.
|
|
300
|
+
const formalGateOk=(rec)=>!!rec&&(rec.status==='passed'||rec.status==='blocked')
|
|
301
|
+
// Human-readable one-liner reused by the Verified card and the index. The exact strings
|
|
302
|
+
// ('Lean 通过' / '阻塞(…)') are part of the card contract (docs §8).
|
|
303
|
+
function formalStatusLine(target){
|
|
304
|
+
const r=formalOf(target)
|
|
305
|
+
if(r.status==='passed') return 'Lean 通过('+(r.proof||r.file||'')+')'
|
|
306
|
+
if(r.status==='blocked') return '阻塞('+(r.note||'未说明')+')'
|
|
307
|
+
// A retracted proof must not read as a plain "tried and failed": the card has to say WHY the
|
|
308
|
+
// archived proof disappeared (spec §4.1 — a fidelity defect is not a refutation).
|
|
309
|
+
if(r.status==='attempted'&&r.decision==='defect') return '忠实性缺陷('+(r.note||'未说明')+',待重做)'
|
|
310
|
+
if(r.status==='attempted') return '已尝试未通过'
|
|
311
|
+
return '未尝试'
|
|
312
|
+
}
|
|
313
|
+
const runLine=(run)=>run?(run.ok?'ok(exit 0,'+((run.ms||0)/1000).toFixed(1)+'s)':'fail(exit '+String(run.exitCode)+','+((run.ms||0)/1000).toFixed(1)+'s)'):'—'
|
|
314
|
+
function tail(s,n){ const t=String(s==null?'':s); return t.length>n?t.slice(-n):t }
|
|
315
|
+
|
|
316
|
+
// Lexically normalise an absolute path (collapse '.', '..' and duplicate slashes) WITHOUT
|
|
317
|
+
// touching the filesystem. A plain `startsWith(root)` check is NOT enough:
|
|
318
|
+
// "…/VibeMath/Projects/../../../../etc/evil.lean" still starts with the root as a string
|
|
319
|
+
// while resolving outside it.
|
|
320
|
+
function normalizeAbsPath(p){
|
|
321
|
+
const parts=String(p==null?'':p).replace(/\\/g,'/').split('/')
|
|
322
|
+
const out=[]
|
|
323
|
+
for(const seg of parts){
|
|
324
|
+
if(seg===''){ if(out.length===0) out.push(''); continue }
|
|
325
|
+
if(seg==='.') continue
|
|
326
|
+
if(seg==='..'){ if(out.length>1) out.pop(); continue }
|
|
327
|
+
out.push(seg)
|
|
328
|
+
}
|
|
329
|
+
return out.join('/')
|
|
330
|
+
}
|
|
331
|
+
// Resolve a Lean path to an absolute, NORMALISED path provably inside the VibeMath root —
|
|
332
|
+
// or null. Note the boundary is the VibeMath root and NOT the project: the global reuse
|
|
333
|
+
// library deliberately lives at <VibeMath>/Formal/{Lib,Proved}, outside the project tree,
|
|
334
|
+
// so climbing out of the project but staying inside VibeMath is legal. Every Lean file
|
|
335
|
+
// access (run, archive, read) goes through this.
|
|
336
|
+
function leanAbsPath(rel){
|
|
337
|
+
const raw=String(rel==null?'':rel).trim()
|
|
338
|
+
if(!raw) return null
|
|
339
|
+
const abs=(raw.charAt(0)==='/'||/^[a-z]:/i.test(raw))?raw:frameworkRoot()+'/'+raw.replace(/^\.\//,'')
|
|
340
|
+
const norm=normalizeAbsPath(abs)
|
|
341
|
+
const root=normalizeAbsPath(vibeRoot())
|
|
342
|
+
if(norm!==root&&norm.indexOf(root+'/')!==0) return null
|
|
343
|
+
return norm
|
|
344
|
+
}
|
|
345
|
+
// <VibeMath>-relative → absolute. Used for the GLOBAL library, which sits beside the
|
|
346
|
+
// project tree rather than inside it.
|
|
347
|
+
function vibeRelAbs(rel){ return vibeRoot()+'/'+String(rel==null?'':rel).replace(/^\.\//,'') }
|
|
348
|
+
|
|
349
|
+
// Run the toolchain on one file. NEVER throws into the scheduler loop: every failure mode
|
|
350
|
+
// (no service, no executable, spawn failure, timeout, non-zero exit) becomes a readable
|
|
351
|
+
// result, because a thrown error inside an end handler would strand the whole group.
|
|
352
|
+
async function leanRunFile(relPath,timeoutMs){
|
|
353
|
+
const started=now()
|
|
354
|
+
const rel=String(relPath==null?'':relPath).trim()
|
|
355
|
+
if(!rel) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'file is required'}
|
|
356
|
+
// Path guard: only files inside the VibeMath tree may be executed, so a crafted path can
|
|
357
|
+
// never make the framework run something outside the workspace.
|
|
358
|
+
const abs=leanAbsPath(rel)
|
|
359
|
+
if(abs===null) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'Lean file must live under '+vibeRoot().replace(/\\/g,'/')+'/ (got '+rel+')',file:rel}
|
|
360
|
+
if(!/\.lean$/.test(abs)) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'only .lean files can be executed',file:rel}
|
|
361
|
+
if(await readTextAbs(abs)===undefined) return {ok:false,code:'V4_NOT_FOUND',message:'no such file: '+rel,file:rel}
|
|
362
|
+
const sub=subprocessOf()
|
|
363
|
+
if(sub===undefined||typeof sub.spawn!=='function'){
|
|
364
|
+
return {ok:false,code:'NO_SUBPROCESS',message:'the host exposes no subprocess service; Lean cannot be executed here',file:rel,ms:0}
|
|
365
|
+
}
|
|
366
|
+
const cap=Math.max(1000,Math.floor(Number(timeoutMs))||Math.floor(Number(params.leanTimeoutMs))||120000)
|
|
367
|
+
let exe
|
|
368
|
+
try { exe=await sub.resolveExecutable(String(params.leanCommand||'lean')) }
|
|
369
|
+
catch(e){
|
|
370
|
+
return {ok:false,code:'LEAN_NOT_FOUND',message:'cannot resolve "'+String(params.leanCommand||'lean')+'": '+String((e&&e.message)||e)+' — 仍可把形式化代码写下来归档,但无法在此宿主上执行',file:rel,ms:now()-started}
|
|
371
|
+
}
|
|
372
|
+
const argv=[exe].concat((Array.isArray(params.leanArgs)?params.leanArgs:[]).map(String)).concat([abs])
|
|
373
|
+
let handle
|
|
374
|
+
try {
|
|
375
|
+
handle=sub.spawn({ argv, cwd:frameworkRoot(), stdio:{stdin:'ignore',stdout:{maxBytes:64*1024},stderr:{maxBytes:64*1024}}, graceMs:cap })
|
|
376
|
+
} catch(e){ return {ok:false,code:'LEAN_SPAWN_FAILED',message:String((e&&e.message)||e),file:rel,ms:now()-started} }
|
|
377
|
+
let outcome
|
|
378
|
+
try { outcome=await handle.done }
|
|
379
|
+
catch(e){ return {ok:false,code:'LEAN_RUN_FAILED',message:String((e&&e.message)||e),file:rel,ms:now()-started} }
|
|
380
|
+
let out='',err=''
|
|
381
|
+
try { if(handle.collected&&handle.collected.stdout) out=handle.collected.stdout.readFrom(0).text } catch(e){ /* best effort */ }
|
|
382
|
+
try { if(handle.collected&&handle.collected.stderr) err=handle.collected.stderr.readFrom(0).text } catch(e){ /* best effort */ }
|
|
383
|
+
const exitCode=outcome?outcome.exitCode:null
|
|
384
|
+
const ms=now()-started
|
|
385
|
+
const ok=exitCode===0
|
|
386
|
+
const timedOut=!ok&&ms>=cap
|
|
387
|
+
return {
|
|
388
|
+
ok, exitCode, signal:(outcome&&outcome.signal)||null, ms,
|
|
389
|
+
command:argv.join(' '), file:rel,
|
|
390
|
+
stdout:tail(out,4000), stderr:tail(err,4000), timedOut,
|
|
391
|
+
code: ok?undefined:(timedOut?'LEAN_TIMEOUT':'LEAN_FAILED'),
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Record one run against an object. `passed`/`blocked` are NEVER downgraded by a later red
|
|
395
|
+
// run (only an explicit re-archive decides those); everything else becomes `attempted`.
|
|
396
|
+
async function formalSetRun(target,run){
|
|
397
|
+
const key=formalKey(target); if(!key) return
|
|
398
|
+
const prev=formalOf(key)
|
|
399
|
+
await putFormal(key,Object.assign({},prev,{
|
|
400
|
+
status:'attempted',
|
|
401
|
+
file:run.file||prev.file||'',
|
|
402
|
+
decision:prev.decision||'used',
|
|
403
|
+
run:{at:now(),ok:!!run.ok,exitCode:run.exitCode===undefined?null:run.exitCode,ms:run.ms||0,stdoutTail:tail(run.stdout,800),stderrTail:tail(run.stderr,800)},
|
|
404
|
+
updatedAt:now(),
|
|
405
|
+
}))
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function formalModeWord(){ return formalMode()==='require'?'强制':'鼓励' }
|
|
409
|
+
/**
|
|
410
|
+
* The verification-prompt block. Every branch is computed from the CURRENT mode and the
|
|
411
|
+
* object's CURRENT record at call time (spec §6.1). In particular the `passed` branch is
|
|
412
|
+
* what makes the feature worthwhile: it tells the voter that re-deriving is NOT the job.
|
|
413
|
+
*/
|
|
414
|
+
function formalPromptBlock(target){
|
|
415
|
+
if(!formalOn()) return ''
|
|
416
|
+
const rec=target?formalOf(target):{status:'none'}
|
|
417
|
+
const L=[]
|
|
418
|
+
L.push('【Lean 形式化验证('+formalModeWord()+'模式)】')
|
|
419
|
+
if(rec.status==='passed'){
|
|
420
|
+
L.push(' · 该对象已有**通过的 Lean 形式化证明**('+(rec.proof||rec.file||'')+',最近运行 exit 0)。')
|
|
421
|
+
L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')
|
|
422
|
+
L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**。')
|
|
423
|
+
L.push(' ▸ 一致 → verdict = 1。')
|
|
424
|
+
L.push(' ▸ **发现任何偏差,不要投 0**:偏差只说明**形式化不合格**,不代表命题为假。此时请:')
|
|
425
|
+
L.push(" ① verdict 给一个严格介于 0 与 1 之间的值(记为弃权),并在 reason 里写清偏差;")
|
|
426
|
+
L.push(" ② 用回执 formal:{decision:'defect', note:'<具体偏差>'} 记录它。框架会撤回这条证明的")
|
|
427
|
+
L.push(' 「已通过」状态(降级为 attempted、删除归档证明、写入形式化待办),本次裁定**不定论**;')
|
|
428
|
+
L.push(' 修正形式化并重新跑通后再投票。')
|
|
429
|
+
L.push(' ▸ 只有当你**独立于这份 Lean 代码**也能确定命题为假时,才投 0,并在 reason 里写清独立理由。')
|
|
430
|
+
} else if(rec.status==='blocked'){
|
|
431
|
+
L.push(' · 该对象已被记录为**形式化阻塞**:'+(rec.note||'未说明')+'。')
|
|
432
|
+
L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
|
|
433
|
+
L.push(' ▸ 因此请把 verdict 用在"这个阻塞判断是否成立 / 是否仍有别的形式化路线"上,并给出理由。')
|
|
434
|
+
} else {
|
|
435
|
+
L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
|
|
436
|
+
L.push(' · 工具:vibe_v4_lean_run(执行)· vibe_v4_lean_archive(归档)· vibe_v4_lean_lib(查已有可复用库)')
|
|
437
|
+
L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 '+formalLibRoot().replace(/\\/g,'/')+'/,已证引理放 '+formalProvedRoot().replace(/\\/g,'/')+'/;写之前先 vibe_v4_lean_lib 查重。')
|
|
438
|
+
L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
|
|
439
|
+
if(formalMode()==='require'){
|
|
440
|
+
L.push(" · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(vibe_v4_lean_archive kind='blocked' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,会被记为未定论(原因 formal-required)并进入「形式化待办」。")
|
|
441
|
+
} else {
|
|
442
|
+
L.push(" · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision='blocked' 时必须写明 note)。")
|
|
443
|
+
}
|
|
444
|
+
L.push(' · 归档可复用定义/引理前先跑通(vibe_v4_lean_archive run=true 或先 vibe_v4_lean_run);跑不通不要入库。')
|
|
445
|
+
L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"——这算显式阻塞原因,定论门禁可以据此放行。')
|
|
446
|
+
if(rec.status==='attempted'){
|
|
447
|
+
L.push(' ▸ 该对象已有形式化尝试但尚未通过(最近一次 '+(rec.run?(rec.run.ok?'通过':'未通过'):'无运行记录')+')。')
|
|
448
|
+
L.push(" 请修复后重跑(vibe_v4_lean_run),跑通后用 kind='proof' 归档。")
|
|
449
|
+
} else {
|
|
450
|
+
L.push(" ▸ 若你在本轮把它形式化并跑通(vibe_v4_lean_archive kind='proof'),后续轮次的")
|
|
451
|
+
L.push(' 审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。')
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return L.join('\n')
|
|
455
|
+
}
|
|
456
|
+
/** The ordinary-work-round line (spec §6.2): formalize reusable objects as you go. */
|
|
457
|
+
function formalWorkLine(){
|
|
458
|
+
if(!formalOn()) return ''
|
|
459
|
+
return '【顺手形式化('+formalModeWord()+')】把你工作中常用或可能复用的对象、假设、新定义,'
|
|
460
|
+
+"用 Lean 形式化定义并归档到全局可复用库(vibe_v4_lean_archive kind='def'),已成立的引理归到 Formal/Proved/"
|
|
461
|
+
+"(kind='lemma');写之前先 vibe_v4_lean_lib 查重,避免重复定义。"
|
|
462
|
+
+(formalMode()==='require'
|
|
463
|
+
? '本模式下,任何要定论为真/假的对象都必须先有 Lean 通过或显式阻塞记录。'
|
|
464
|
+
: '这会让后续的验证与证明省掉大量重复工作。')
|
|
465
|
+
+'归档前先跑通(vibe_v4_lean_run 或 run=true);跑不通的定义不要进可复用库。'
|
|
466
|
+
}
|
|
467
|
+
/** The `formal` object every non-off prompt documents in its JSON reply contract. */
|
|
468
|
+
function formalReplyField(target){
|
|
469
|
+
return '{"formal":{"target":"'+String(target||'p-x')+'","decision":"used|blocked|defect","file":"Formal/'+String(target||'p-x')+'.lean","note":"难度判断/阻塞原因/具体偏差"}}'
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---- the three indexes (framework-maintained) ---------------------------
|
|
473
|
+
async function writeFormalIndex(){
|
|
474
|
+
const recs=formalRecords()
|
|
475
|
+
const L=['# Lean 形式化索引|'+currentProject+'|'+fmtTime(),'',
|
|
476
|
+
'> 本文件由框架维护(工具调用时更新;`vibe_v4_lean_lib` 会重建)。权威状态在 `State/formal.json`。','',
|
|
477
|
+
'| 对象 | 状态 | 形式化文件 | 归档证明 | 最近运行 | 难度判断 / 阻塞原因 |','|---|---|---|---|---|---|']
|
|
478
|
+
const keys=Object.keys(recs)
|
|
479
|
+
if(!keys.length) L.push('| (暂无) | | | | | |')
|
|
480
|
+
for(const k of keys){
|
|
481
|
+
const r=recs[k]||{}
|
|
482
|
+
L.push('| '+k+' | '+(r.status||'none')+' | '+(r.file||'—')+' | '+(r.proof||'—')+' | '+runLine(r.run)+' | '+String(r.note||'—').replace(/\|/g,'/').slice(0,120)+' |')
|
|
483
|
+
}
|
|
484
|
+
L.push('')
|
|
485
|
+
if(formalTodo().length){
|
|
486
|
+
L.push('## 形式化待办(require 模式:定论被搁置)')
|
|
487
|
+
for(const t of formalTodo()) L.push('- '+t.id+' —— '+(t.why||'formal-required')+'('+fmtTime(t.at)+')')
|
|
488
|
+
L.push('')
|
|
489
|
+
}
|
|
490
|
+
await writeText('Formal/Index.md',L.join('\n'))
|
|
491
|
+
}
|
|
492
|
+
async function writeFormalTodo(){
|
|
493
|
+
const L=['# 形式化待办|'+currentProject+'|'+fmtTime(),'',
|
|
494
|
+
'> 这些对象在 `require` 模式下尚不具备「Lean 已通过」或「显式阻塞记录」,因此**定论被搁置**。',
|
|
495
|
+
"> 完成形式化(vibe_v4_lean_archive kind='proof')或记录阻塞原因(kind='blocked')后,重新提议验证即可。",'']
|
|
496
|
+
const list=formalTodo()
|
|
497
|
+
if(!list.length) L.push('(暂无)')
|
|
498
|
+
for(const t of list) L.push('- '+t.id+'|'+(t.why||'formal-required')+'|'+fmtTime(t.at))
|
|
499
|
+
L.push('')
|
|
500
|
+
await writeText('Formal/TODO.md',L.join('\n'))
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Scan and rebuild the three indexes. Listing is deliberately CHEAP and side-effect free:
|
|
504
|
+
* it does NOT execute the toolchain (running Lean on every library file each time an agent
|
|
505
|
+
* asks "what can I reuse?" would be slow and surprising). Per-object run results live in
|
|
506
|
+
* the object records and are shown in Formal/Index.md.
|
|
507
|
+
*/
|
|
508
|
+
async function rebuildLeanLibIndexes(){
|
|
509
|
+
const scan=async(dirAbs,dirRel,kindLabel)=>{
|
|
510
|
+
const rows=[]
|
|
511
|
+
try {
|
|
512
|
+
const t=await fs.resolve(dirAbs)
|
|
513
|
+
if(await fs.stat(t)===undefined) return rows
|
|
514
|
+
const entries=await fs.listDir(t)
|
|
515
|
+
for(const e of entries||[]){
|
|
516
|
+
if(!e||e.type!=='file'||!/\.lean$/.test(String(e.name))) continue
|
|
517
|
+
const rel=dirRel+'/'+e.name
|
|
518
|
+
const txt=(await readTextAbs(dirAbs+'/'+e.name))||''
|
|
519
|
+
const name=String(e.name).replace(/\.lean$/,'')
|
|
520
|
+
const first=(txt.split('\n').filter(l=>l.trim()&&!/^\s*(\/\/|--|import)/.test(l))[0]||'').trim().slice(0,110)
|
|
521
|
+
rows.push('| '+name+' | '+rel+' | '+kindLabel+' | '+first.replace(/\|/g,'/')+' |')
|
|
522
|
+
}
|
|
523
|
+
} catch(e){ /* listing is best-effort */ }
|
|
524
|
+
return rows
|
|
525
|
+
}
|
|
526
|
+
const libRows=await scan(formalLibRoot(),'Formal/Lib','def')
|
|
527
|
+
await writeTextAbs(formalLibRoot()+'/Index.md',['# 可复用 Lean 定义库(跨项目)|'+currentProject,'',
|
|
528
|
+
'> 写新定义之前先查这里:能复用就不要重新定义。','',
|
|
529
|
+
'| 名称 | 文件 | 类别 | 摘要 |','|---|---|---|---|']
|
|
530
|
+
.concat(libRows.length?libRows:['| (暂无) | | | |']).join('\n')+'\n')
|
|
531
|
+
const provedRows=await scan(formalProvedRoot(),'Formal/Proved','lemma')
|
|
532
|
+
await writeTextAbs(formalProvedRoot()+'/Index.md',['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|'+currentProject,'',
|
|
533
|
+
'> 这些文件是通过内核检查的引理,可直接 import 复用。','',
|
|
534
|
+
'| 名称 | 文件 | 类别 | 陈述 |','|---|---|---|---|']
|
|
535
|
+
.concat(provedRows.length?provedRows:['| (暂无) | | | |']).join('\n')+'\n')
|
|
536
|
+
await writeFormalIndex()
|
|
537
|
+
await writeFormalTodo()
|
|
538
|
+
return {lib:libRows.length,proved:provedRows.length,objects:Object.keys(formalRecords()).length}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Execute one Lean file through the toolchain, record the run (optionally against an
|
|
543
|
+
* object), refresh the index, and report the outcome verbatim. Deliberately callable even
|
|
544
|
+
* from `off` mode: a human debugging their toolchain may want it, and registration is
|
|
545
|
+
* static while the MODE only decides whether the framework TELLS members about it.
|
|
546
|
+
*/
|
|
547
|
+
async function leanRunTool(memberId,o){
|
|
548
|
+
const args=o||{}
|
|
549
|
+
const run=await leanRunFile(String(args.file||''),args.timeout_ms)
|
|
550
|
+
try {
|
|
551
|
+
if(run.ok||run.file){
|
|
552
|
+
if(String(args.target||'').trim()) formalSetRun(String(args.target),run)
|
|
553
|
+
await writeFormalIndex()
|
|
554
|
+
}
|
|
555
|
+
} catch(e){ /* the index is best-effort; a run result must always come back */ }
|
|
556
|
+
logActivity('formal',(memberId||'host')+' lean_run '+(run.file||String(args.file||''))+' → '+(run.ok?'通过':(run.code||'未通过')))
|
|
557
|
+
return Object.assign({ok:!!run.ok},run,{
|
|
558
|
+
mode:formalMode(),
|
|
559
|
+
hint: run.ok
|
|
560
|
+
? "通过。若是某个对象的证明,请用 vibe_v4_lean_archive kind='proof' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind='def'/'lemma' 归档到全局库——归档前先跑通(run=true 或先 vibe_v4_lean_run):跑不通的定义不要进可复用库。"
|
|
561
|
+
: '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_v4_lean_archive kind=\'blocked\' 记录原因。',
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* One tool, three archives (spec §5.2).
|
|
566
|
+
* def/lemma → the GLOBAL cross-project library (Formal/Lib, Formal/Proved)
|
|
567
|
+
* proof → the project working file Formal/<target>.lean, plus Verified/Lean/<target>.lean
|
|
568
|
+
* when the run is GREEN (and only then is the object `passed`)
|
|
569
|
+
* blocked → an explicit, reasoned "we judged this infeasible" record (note REQUIRED)
|
|
570
|
+
*/
|
|
571
|
+
async function leanArchive(memberId,o){
|
|
572
|
+
const args=o||{}
|
|
573
|
+
const kind=String(args.kind||'')
|
|
574
|
+
const content=typeof args.content==='string'?args.content:undefined
|
|
575
|
+
const from=args.from?String(args.from):''
|
|
576
|
+
const bodyFrom=async()=>{
|
|
577
|
+
if(content!==undefined) return {body:content}
|
|
578
|
+
if(from){
|
|
579
|
+
const srcAbs=leanAbsPath(from)
|
|
580
|
+
if(srcAbs===null) return {error:{ok:false,code:'V4_INVALID_ARGUMENT',message:'from must be a .lean file inside the workspace (got '+from+')'}}
|
|
581
|
+
const body=await readTextAbs(srcAbs)
|
|
582
|
+
if(body===undefined) return {error:{ok:false,code:'V4_NOT_FOUND',message:'no such file: '+from}}
|
|
583
|
+
return {body}
|
|
584
|
+
}
|
|
585
|
+
return {error:{ok:false,code:'V4_INVALID_ARGUMENT',message:'provide content, or from=<existing .lean file>'}}
|
|
586
|
+
}
|
|
587
|
+
if(kind==='def'||kind==='lemma'){
|
|
588
|
+
const name=idSafe(String(args.name||''))
|
|
589
|
+
if(!name||name==='id') return {ok:false,code:'V4_INVALID_ARGUMENT',message:'name is required for a reusable definition/lemma'}
|
|
590
|
+
const got=await bodyFrom(); if(got.error) return got.error
|
|
591
|
+
const rel='Formal/'+(kind==='def'?'Lib':'Proved')+'/'+name+'.lean'
|
|
592
|
+
if(!await writeTextAbs(vibeRelAbs(rel),got.body)) return {ok:false,code:'V4_WRITE_FAILED',message:'could not write '+rel}
|
|
593
|
+
// The global library sits BESIDE the project tree, so it must be executed through its
|
|
594
|
+
// ABSOLUTE path — the project-relative form would resolve inside frameworkRoot.
|
|
595
|
+
const run=args.run===false?null:await leanRunFile(vibeRelAbs(rel))
|
|
596
|
+
try { await rebuildLeanLibIndexes() } catch(e){ /* index rebuild is best-effort */ }
|
|
597
|
+
logActivity('formal',String(memberId||'host')+' 归档'+(kind==='def'?'可复用定义':'已证引理')+' '+name+' → '+rel+(run?('(运行 '+(run.ok?'通过':'未通过')+')'):''))
|
|
598
|
+
return {ok:true,kind,name,file:rel,run:run||undefined,note:'已并入全局可复用库,后续项目可直接 import 复用'}
|
|
599
|
+
}
|
|
600
|
+
if(kind==='proof'){
|
|
601
|
+
const key=formalKey(String(args.target||''))
|
|
602
|
+
if(!key) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'target is required for kind=proof'}
|
|
603
|
+
const got=await bodyFrom(); if(got.error) return got.error
|
|
604
|
+
const workRel='Formal/'+key+'.lean'
|
|
605
|
+
if(!await writeText(workRel,got.body)) return {ok:false,code:'V4_WRITE_FAILED',message:'could not write '+workRel}
|
|
606
|
+
const run=await leanRunFile(workRel)
|
|
607
|
+
const prev=formalOf(key)
|
|
608
|
+
const passed=!!run.ok
|
|
609
|
+
const rec=Object.assign({},prev,{
|
|
610
|
+
status:passed?'passed':'attempted',
|
|
611
|
+
file:workRel,
|
|
612
|
+
proof:passed?('Verified/Lean/'+key+'.lean'):(prev.proof||''),
|
|
613
|
+
decision:'used',
|
|
614
|
+
note:String(args.note||prev.note||''),
|
|
615
|
+
run:{at:now(),ok:!!run.ok,exitCode:run.exitCode===undefined?null:run.exitCode,ms:run.ms||0,stdoutTail:tail(run.stdout,800),stderrTail:tail(run.stderr,800)},
|
|
616
|
+
updatedAt:now(),
|
|
617
|
+
})
|
|
618
|
+
// ★ `passed` requires a green run: the proof is only copied into Verified/Lean/ when the
|
|
619
|
+
// kernel actually accepted it. A red run still records the working file (so the agent
|
|
620
|
+
// can iterate) but must not mint a proof.
|
|
621
|
+
if(passed) await writeText('Verified/Lean/'+key+'.lean',got.body)
|
|
622
|
+
await putFormal(key,rec)
|
|
623
|
+
try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ }
|
|
624
|
+
logActivity('formal',String(memberId||'host')+' 为 '+key+' 归档形式化证明 '+workRel+(passed?'(**通过**,已归档到 '+rec.proof+',验证转为忠实性审查)':'(**未通过**:'+tail(run.stderr||run.message,160)+')'))
|
|
625
|
+
return {ok:true,kind,target:key,file:workRel,proof:rec.proof,passed,run,status:rec.status}
|
|
626
|
+
}
|
|
627
|
+
if(kind==='blocked'){
|
|
628
|
+
const key=formalKey(String(args.target||''))
|
|
629
|
+
if(!key) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'target is required for kind=blocked'}
|
|
630
|
+
const note=String(args.note||'').trim()
|
|
631
|
+
// "决定权在代理,但决定必须显式、可审计" — an empty note would make the escape hatch
|
|
632
|
+
// indistinguishable from silently skipping formalization, so it is refused outright.
|
|
633
|
+
if(!note) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'阻塞记录必须写明原因(note)——"因难度决定不做形式化"必须显式、可审计'}
|
|
634
|
+
const prev=formalOf(key)
|
|
635
|
+
const rec=Object.assign({},prev,{status:'blocked',decision:'blocked',note,updatedAt:now()})
|
|
636
|
+
await putFormal(key,rec)
|
|
637
|
+
try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ }
|
|
638
|
+
logActivity('formal',String(memberId||'host')+' 记录 '+key+' 形式化阻塞:'+note)
|
|
639
|
+
return {ok:true,kind,target:key,status:'blocked',note}
|
|
640
|
+
}
|
|
641
|
+
return {ok:false,code:'V4_INVALID_ARGUMENT',message:"kind must be 'def' | 'lemma' | 'proof' | 'blocked'"}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Withdraw an archived proof (spec §4.1). Best-effort by contract: the RECORD is the
|
|
645
|
+
* authoritative state, so a host without a subprocess service (or without Lean) still gets a
|
|
646
|
+
* correct `attempted` record — the stale file is then reported in the activity log instead of
|
|
647
|
+
* silently kept. Every path goes through `leanAbsPath` so this can only ever remove a `.lean`
|
|
648
|
+
* file inside the VibeMath root (a hand-edited State/formal.json must not become an
|
|
649
|
+
* arbitrary-file delete).
|
|
650
|
+
*/
|
|
651
|
+
async function withdrawProof(rel){
|
|
652
|
+
const raw=String(rel==null?'':rel)
|
|
653
|
+
if(!raw) return {ok:false,skipped:true}
|
|
654
|
+
const abs=leanAbsPath(raw)
|
|
655
|
+
if(abs===null||!/\.lean$/.test(abs)){
|
|
656
|
+
logActivity('formal','拒绝删除越界的归档证明路径:'+raw)
|
|
657
|
+
return {ok:false,skipped:true}
|
|
658
|
+
}
|
|
659
|
+
const r=await runShell(rmCmd([abs]),vibeRoot())
|
|
660
|
+
if(!r||!r.ok) return {ok:false,error:(r&&r.error)||('exit '+String(r&&r.exitCode))}
|
|
661
|
+
return {ok:true,abs}
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* §4.1 `defect`: a voter checked the Lean code against the proposition and found a FIDELITY
|
|
665
|
+
* defect (written too narrow/wide, wrong object, missing hypothesis). That is a statement about
|
|
666
|
+
* the FORMALIZATION, not about the proposition, so it must never be absorbed as "the
|
|
667
|
+
* proposition is false". The record is therefore ALWAYS downgraded to `attempted` (even from
|
|
668
|
+
* `blocked`), `proof` is cleared, `Verified/Lean/<id>.lean` is withdrawn, the deviation goes
|
|
669
|
+
* into the record + Formal/Index.md + Formal/TODO.md, and the group is told in the activity
|
|
670
|
+
* log. The working file `Formal/<id>.lean` is deliberately KEPT — the code is not lost, only
|
|
671
|
+
* its "passed" claim. In `require` mode the downgrade also makes `formalGateOk` false, so the
|
|
672
|
+
* verdict defers through the existing `deferForFormal` path (no Verified card, TODO entry).
|
|
673
|
+
*/
|
|
674
|
+
async function recordFormalDefect(rId,key,note){
|
|
675
|
+
const prev=formalOf(key)
|
|
676
|
+
const proofRel=String(prev.proof||'')||('Verified/Lean/'+key+'.lean')
|
|
677
|
+
const rec=Object.assign({},prev,{status:'attempted',proof:'',decision:'defect',note,updatedAt:now()})
|
|
678
|
+
const why='formal-defect:形式化与命题原文不一致——'+String(note).slice(0,160)
|
|
679
|
+
const todo=formalTodo().slice()
|
|
680
|
+
const i=todo.findIndex(t=>t&&t.id===key)
|
|
681
|
+
// The defect reason must survive the later `deferForFormal`, which keeps an EXISTING entry
|
|
682
|
+
// rather than overwriting it — so the TODO file explains "formalization不合格", not merely
|
|
683
|
+
// "formal-required". An already-deferred object keeps its original 真/假 tally here.
|
|
684
|
+
if(i>=0) todo[i]=Object.assign({},todo[i],{why,at:now()})
|
|
685
|
+
else todo.push({id:key,at:now(),why,verdict:null})
|
|
686
|
+
// Durable write FIRST (record + todo together), then withdraw the file: a crash in between
|
|
687
|
+
// leaves an orphaned file with a truthful record, never a record still claiming `passed`.
|
|
688
|
+
await putFormal(key,rec,todo)
|
|
689
|
+
const del=await withdrawProof(proofRel)
|
|
690
|
+
try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
|
|
691
|
+
logActivity('formal',(rId||'host')+' 报告 '+key+' 存在**忠实性缺陷**(formal.decision=defect):'+note
|
|
692
|
+
+' ——已撤回「已通过」状态(→ attempted)、'+(del&&del.ok?'删除归档证明 '+proofRel:'删除归档证明失败('+String((del&&del.error)||'no-subprocess')+',记录已降级)')
|
|
693
|
+
+'、写入 Formal/TODO.md;本次裁定**不定论**,修正形式化并重新跑通后再投票')
|
|
694
|
+
return {ok:true,target:key,status:'attempted',proof:'',decision:'defect',removedProof:!!(del&&del.ok)}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* The per-round `formal` reply channel. This is the path that fires IN PRACTICE: a resident
|
|
698
|
+
* that never calls a Lean tool still has to state its difficulty judgement. Every failure is
|
|
699
|
+
* swallowed into the activity log — an end handler must never throw into the scheduler — but
|
|
700
|
+
* each rejection still returns the preset's typed error so the caller/audit can see WHY a
|
|
701
|
+
* judgement was dropped instead of silently losing it.
|
|
702
|
+
*/
|
|
703
|
+
async function applyFormalReply(rId,formalReply){
|
|
704
|
+
try {
|
|
705
|
+
const key=formalKey(String(formalReply.target||''))
|
|
706
|
+
if(!key){
|
|
707
|
+
logActivity('formal',(rId||'host')+' 的 formal 回执缺少 target(对象 id)——本次未记录(V4_INVALID_ARGUMENT)')
|
|
708
|
+
return {ok:false,code:'V4_INVALID_ARGUMENT',message:'formal.target(对象 id)是必填的'}
|
|
709
|
+
}
|
|
710
|
+
const decision=String(formalReply.decision||'').trim()
|
|
711
|
+
if(decision==='blocked'){
|
|
712
|
+
const note=String(formalReply.note||'').trim()
|
|
713
|
+
if(!note){
|
|
714
|
+
logActivity('formal',(rId||'host')+" 的 formal.decision='blocked' 缺少 note(难度判断/阻塞原因)——本次未记录(V4_INVALID_ARGUMENT)")
|
|
715
|
+
return {ok:false,code:'V4_INVALID_ARGUMENT',message:"formal.decision='blocked' 必须写明 note(难度判断/阻塞原因)"}
|
|
716
|
+
}
|
|
717
|
+
await leanArchive(rId,{kind:'blocked',target:key,note})
|
|
718
|
+
return {ok:true,target:key,decision:'blocked',status:'blocked'}
|
|
719
|
+
} else if(decision==='defect'){
|
|
720
|
+
// §4.1: a fidelity defect is NOT a refutation. Accepting it as "0 / false" would make the
|
|
721
|
+
// framework fabricate a negative conclusion out of a broken formalization, so the ONLY
|
|
722
|
+
// thing this branch may do is RETRACT the passing proof and defer the verdict.
|
|
723
|
+
const note=String(formalReply.note||'').trim()
|
|
724
|
+
if(!note){
|
|
725
|
+
logActivity('formal',(rId||'host')+" 的 formal.decision='defect' 缺少 note(具体偏差)——本次未记录(V4_INVALID_ARGUMENT)")
|
|
726
|
+
return {ok:false,code:'V4_INVALID_ARGUMENT',message:"formal.decision='defect' 必须写明 note(具体偏差:写窄了/写宽了/换了对象/漏了条件…)"}
|
|
727
|
+
}
|
|
728
|
+
return await recordFormalDefect(rId,key,note)
|
|
729
|
+
} else if(decision==='used'){
|
|
730
|
+
const file=String(formalReply.file||('Formal/'+key+'.lean'))
|
|
731
|
+
const prev=formalOf(key)
|
|
732
|
+
await putFormal(key,Object.assign({},prev,{
|
|
733
|
+
status:prev.status==='passed'||prev.status==='blocked'?prev.status:'attempted',
|
|
734
|
+
file,decision:'used',note:String(formalReply.note||prev.note||''),updatedAt:now(),
|
|
735
|
+
}))
|
|
736
|
+
try { await writeFormalIndex() } catch(e){ /* best-effort */ }
|
|
737
|
+
return {ok:true,target:key,decision:'used'}
|
|
738
|
+
} else if(decision){
|
|
739
|
+
logActivity('formal',(rId||'host')+" 的 formal.decision 只能是 'used'、'blocked' 或 'defect'(收到 "+decision+")——本次未记录(V4_INVALID_ARGUMENT)")
|
|
740
|
+
return {ok:false,code:'V4_INVALID_ARGUMENT',message:"formal.decision 只能是 'used' | 'blocked' | 'defect'(收到 "+decision+")"}
|
|
741
|
+
}
|
|
742
|
+
return {ok:false,code:'V4_INVALID_ARGUMENT',message:'formal.decision 是必填的'}
|
|
743
|
+
} catch(e){
|
|
744
|
+
logActivity('formal','formal 回执处理失败:'+String((e&&e.message)||e))
|
|
745
|
+
return {ok:false,code:'V4_INVALID_ARGUMENT',message:String((e&&e.message)||e)}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/** The {mode, on, objects, todo} view the host tools report (state-storage transparency). */
|
|
749
|
+
function formalView(){
|
|
750
|
+
const recs=formalRecords()
|
|
751
|
+
return {
|
|
752
|
+
mode:formalMode(),
|
|
753
|
+
on:formalOn(),
|
|
754
|
+
objects:Object.keys(recs).map(k=>({target:k,status:(recs[k]||{}).status||'none',file:(recs[k]||{}).file||'',proof:(recs[k]||{}).proof||'',note:(recs[k]||{}).note||''})),
|
|
755
|
+
passed:Object.keys(recs).filter(k=>(recs[k]||{}).status==='passed'),
|
|
756
|
+
blocked:Object.keys(recs).filter(k=>(recs[k]||{}).status==='blocked'),
|
|
757
|
+
todo:formalTodo().map(t=>t.id),
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
216
761
|
// ---- persistence ----
|
|
217
762
|
async function saveAll(){
|
|
218
763
|
await writeJson('State/residents.json', Object.fromEntries(residents))
|
|
219
764
|
await writeJson('State/mailboxes.json', Object.fromEntries(mailboxes))
|
|
220
765
|
await writeJson('State/taskboard.json', taskboard)
|
|
221
766
|
await writeJson('State/decisions.json', decisions)
|
|
767
|
+
await writeJson('State/formal.json', {records:formal,todo:formalTodos})
|
|
222
768
|
await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount})
|
|
223
769
|
}
|
|
224
770
|
async function loadAll(){
|
|
@@ -227,6 +773,13 @@ export function apply(ctx) {
|
|
|
227
773
|
const mb=await readJson('State/mailboxes.json'); if(mb&&typeof mb==='object') mailboxes=new Map(Object.entries(mb))
|
|
228
774
|
const tb=await readJson('State/taskboard.json'); if(Array.isArray(tb)) taskboard=tb
|
|
229
775
|
const dc=await readJson('State/decisions.json'); if(Array.isArray(dc)) decisions=dc
|
|
776
|
+
// Formal records are part of the run's durable state: a `require`-mode object's gate
|
|
777
|
+
// decision must survive a process restart, so they are restored alongside the rest.
|
|
778
|
+
const fm=await readJson('State/formal.json')
|
|
779
|
+
if(fm&&typeof fm==='object'){
|
|
780
|
+
formal=(fm.records&&typeof fm.records==='object')?fm.records:{}
|
|
781
|
+
formalTodos=Array.isArray(fm.todo)?fm.todo:[]
|
|
782
|
+
}
|
|
230
783
|
}
|
|
231
784
|
|
|
232
785
|
// ---- resident prompts ----
|
|
@@ -296,7 +849,11 @@ export function apply(ctx) {
|
|
|
296
849
|
// could blank them).
|
|
297
850
|
function coreRulesBrief(){
|
|
298
851
|
const base=frameworkRoot()
|
|
852
|
+
// The formalization line is appended HERE (not frozen into a brief) because the mode is
|
|
853
|
+
// dynamic: after a /compact the resident must re-anchor on the rules it is actually
|
|
854
|
+
// living under right now.
|
|
299
855
|
return '[核心规则重申] 只有 Verified/(及标记"已验证·真/假")算已确立;验证须全组一致(全真或全假)才作数,否则留库附平均概率;你只写自己的库('+base+'/ 的 Progress/<你>/、Propos/<你>/、Methods/<你>/、Subproblems/<你>/),可只读任何人的库;任务分工由团队讨论决定;退出只输出一个 JSON 对象。'
|
|
856
|
+
+(formalOn()?('\n'+formalWorkLine()):'')
|
|
300
857
|
}
|
|
301
858
|
function brainstormPrompt(r){
|
|
302
859
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
@@ -313,8 +870,12 @@ export function apply(ctx) {
|
|
|
313
870
|
+'Resident researcher '+r.rId+' — 第 '+r.rounds+' 轮。一切由你和团队讨论决定。动手前先**读别人的库**对齐事实、避免重复;把新进展/结论**直接用 fs 写进你自己的文件**;想对团队说的话放 "input"(会转给其他常驻)。\n'
|
|
314
871
|
+'\n团队成员:\n'+banner()+'\n'
|
|
315
872
|
+'New items:\n'+ (await inboxText(r.rId)) +'\n'
|
|
873
|
+
// 顺手形式化: computed from the CURRENT mode on every wake (docs §1: the mode is dynamic).
|
|
874
|
+
+(formalOn()?('\n'+formalWorkLine()+'\n'):'')
|
|
316
875
|
+'Reply with ONLY a JSON object:\n'
|
|
317
|
-
+'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40
|
|
876
|
+
+'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40'
|
|
877
|
+
+(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked|defect","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因/具体偏差"}'):'')
|
|
878
|
+
+'}'
|
|
318
879
|
}
|
|
319
880
|
function meetingPrompt(r, st){
|
|
320
881
|
const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
|
|
@@ -333,15 +894,30 @@ export function apply(ctx) {
|
|
|
333
894
|
// others' opinions exist yet. vs.verdicts only ever holds the CURRENT round's votes.
|
|
334
895
|
const src = (vs.stage==='debate' && vs.history && Object.keys(vs.history).length>0) ? vs.history : (vs.stage==='debate' ? vs.verdicts : {})
|
|
335
896
|
const others=Object.entries(src).map(([k,v])=>'- '+k+': 正确概率 '+String(v.prob!=null?Number(v.prob).toFixed(2):0.5)+' → '+v.reason).join('\n')
|
|
336
|
-
|
|
897
|
+
const L=[]
|
|
898
|
+
L.push((params.residentPersona?params.residentPersona+'\n':'')
|
|
337
899
|
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+vs.targetType+',提出者 '+vs.targetOwner+')。\n'
|
|
338
900
|
+'请给出你对「该对象为真」的**正确概率 `verdict`**,仅一个 0–1 数值:**1 = 绝对为真,0 = 绝对为假,0.5 = 完全不确定,其余为介于其间的程度**(不要给 TRUE/FALSE,就给一个数值)。\n'
|
|
339
901
|
+'判定规则:仅当**全体常驻一致给 1(都认为是真)或一致给 0(都认为是假)**,才按「真/假」写入 Verified/;否则**只作为概率数值(一种程度)保留在库中**,附全组平均正确概率,不写成真/假。\n'
|
|
340
902
|
+'请给出你**诚实独立的判断**'
|
|
341
903
|
+(vs.stage==='debate'?',并参考他人意见:\n':'。\n')
|
|
342
|
-
+(vs.stage==='debate'&&others?('### 他人上一轮意见(已转发给你)\n'+others+'\n'):'')
|
|
343
|
-
|
|
344
|
-
|
|
904
|
+
+(vs.stage==='debate'&&others?('### 他人上一轮意见(已转发给你)\n'+others+'\n'):''))
|
|
905
|
+
// ---- Lean formalization: the block is computed from the CURRENT mode and the object's
|
|
906
|
+
// CURRENT record, so a mode switch and a fresh proof both show up on the next wake ----
|
|
907
|
+
if(formalOn()){
|
|
908
|
+
L.push('')
|
|
909
|
+
L.push(formalPromptBlock(vs.targetId))
|
|
910
|
+
}
|
|
911
|
+
L.push('')
|
|
912
|
+
L.push('Reply with ONLY a JSON object:')
|
|
913
|
+
L.push('{"vote":{"verdict":0.9,"reason":"<your logic>"}}')
|
|
914
|
+
if(formalOn()){
|
|
915
|
+
// The formal field belongs in the VOTING contract too: voters are exactly the agents who
|
|
916
|
+
// must either formalize the object or record why they judged it infeasible.
|
|
917
|
+
L.push('若你本轮做了形式化或给出难度判断,请一并加上:')
|
|
918
|
+
L.push(formalReplyField(vs.targetId))
|
|
919
|
+
}
|
|
920
|
+
return L.join('\n')
|
|
345
921
|
}
|
|
346
922
|
|
|
347
923
|
// ---- resident lifecycle ----
|
|
@@ -616,7 +1192,19 @@ export function apply(ctx) {
|
|
|
616
1192
|
// verdict is a PURE 0-1 probability; only ALL=1 (true) or ALL=0 (false) is a binary verdict.
|
|
617
1193
|
const allTrue = allVoted && vals.every(x=>Number(x.prob)===1)
|
|
618
1194
|
const allFalse = allVoted && vals.every(x=>Number(x.prob)===0)
|
|
619
|
-
if(allTrue||allFalse){
|
|
1195
|
+
if(allTrue||allFalse){
|
|
1196
|
+
// ── the `require` gate (docs §8) ───────────────────────────────────────────────
|
|
1197
|
+
// A unanimous verdict is a CONSENSUS, not a proof. In `require` mode the group has
|
|
1198
|
+
// decided that consensus alone may not be promoted to Verified/: the object must
|
|
1199
|
+
// also be either machine-checked (`passed`) or carry an explicit, reasoned
|
|
1200
|
+
// "we judged this infeasible" record (`blocked`). The gate never wedges the run — it
|
|
1201
|
+
// records 未定论 + a formalization TODO so the group keeps going and can formalize
|
|
1202
|
+
// later. This is the SINGLE choke point: every 真/假 promotion passes through here.
|
|
1203
|
+
const rec=formalOf(vs.targetId)
|
|
1204
|
+
if(formalMode()==='require' && !formalGateOk(rec)) await deferForFormal(vs,allTrue)
|
|
1205
|
+
else await closeVerify(vs,allTrue)
|
|
1206
|
+
doSchedule=true
|
|
1207
|
+
}
|
|
620
1208
|
else if(vs.round+1<params.verdictMaxRounds){
|
|
621
1209
|
// Move to a REAL debate round: snapshot the current votes into history (so the next round's
|
|
622
1210
|
// prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
|
|
@@ -640,11 +1228,44 @@ export function apply(ctx) {
|
|
|
640
1228
|
await writeVerifiedCard(vs,isTrue)
|
|
641
1229
|
await rewriteSource(target,isTrue,vs.targetOwner)
|
|
642
1230
|
verifiedRecently.set(target, now()) // dedup: block an immediate re-proposal of the same object
|
|
643
|
-
|
|
1231
|
+
// A formalization TODO that has just been satisfied must not linger in the todo file.
|
|
1232
|
+
if(formalOn() && formalTodos.some(t=>t.id===target)){
|
|
1233
|
+
formalTodos=formalTodos.filter(t=>t.id!==target)
|
|
1234
|
+
await saveFormal()
|
|
1235
|
+
try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
|
|
1236
|
+
}
|
|
1237
|
+
logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus'+(formalOn()?('|形式化: '+formalStatusLine(target)):''))
|
|
644
1238
|
verifyState=null; wakeKind.clear(); await saveAll()
|
|
645
1239
|
// scheduling is done by finalizeVerify AFTER it releases finalizeLock (so a chained verify is
|
|
646
1240
|
// never swallowed by the still-held reentry lock)
|
|
647
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* `require` mode withheld the verdict: record it as 未定论 with a machine-readable reason, put
|
|
1244
|
+
* the object on the formalization TODO, and say so in the activity log. The object keeps its
|
|
1245
|
+
* mean probability (留库附概率, exactly like a non-unanimous round) and stays where it was, so
|
|
1246
|
+
* nothing is lost and the group can carry on and formalize later. Deliberately NOT routed
|
|
1247
|
+
* through closeVerify: no Verified card may be written, and the source card's status must not
|
|
1248
|
+
* become 已验证·真/假.
|
|
1249
|
+
*/
|
|
1250
|
+
async function deferForFormal(vs,isTrue){
|
|
1251
|
+
const target=vs.targetId
|
|
1252
|
+
const rec=formalOf(target)
|
|
1253
|
+
const why='formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 '+(rec.status||'none')+')'
|
|
1254
|
+
await writeDebateDoc(vs,false,isTrue?1:0) // the 真/假 tally is recorded as the debate outcome
|
|
1255
|
+
const vals=Object.values(vs.verdicts)
|
|
1256
|
+
const mean=vals.length?vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length:(isTrue?1:0)
|
|
1257
|
+
// The card stays in the library with the group's mean UNCHANGED relative to a normal
|
|
1258
|
+
// non-unanimous round: this is not a probability revision, it is a withheld conclusion.
|
|
1259
|
+
await rewriteSourceProb(target,mean,vs.targetOwner)
|
|
1260
|
+
if(!formalTodos.some(t=>t.id===target)) formalTodos.push({id:target,at:now(),why,verdict:isTrue?1:0})
|
|
1261
|
+
await putFormal(target,rec,formalTodos) // single durable write of records + todo together
|
|
1262
|
+
try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
|
|
1263
|
+
logActivity('verify',target+' 的表决结果为 '+(isTrue?'真':'假')+',但 **require 模式**要求先有 Lean 通过或显式阻塞记录,因此本轮**不定论**(已记入 Formal/TODO.md;原因 formal-required)')
|
|
1264
|
+
verifyState=null; wakeKind.clear(); await saveAll()
|
|
1265
|
+
// Scheduling is done by finalizeVerify (doSchedule=true) AFTER it releases finalizeLock:
|
|
1266
|
+
// a withheld verdict must return the group to normal work immediately, exactly like a
|
|
1267
|
+
// normal 未定论 round. Without it the run would sit idle until the heartbeat fired.
|
|
1268
|
+
}
|
|
648
1269
|
// Queue a verify proposal UNLESS the same object was just verified (closed as 真/假). In parallel
|
|
649
1270
|
// self-organization several residents may independently propose targets while a verify is already
|
|
650
1271
|
// settling — sometimes the SAME object (test9: p-r3-04 was Verified twice back-to-back), sometimes
|
|
@@ -673,7 +1294,12 @@ export function apply(ctx) {
|
|
|
673
1294
|
const isSub=vs.targetType==='subproblem'
|
|
674
1295
|
const dir= isSub?'问题':'命题'
|
|
675
1296
|
const type= isSub?'问题': vs.targetType==='method'?'方法':'命题'
|
|
676
|
-
|
|
1297
|
+
// In a non-off formal mode the card must state HOW STRONG this conclusion actually is:
|
|
1298
|
+
// 'Lean 通过(Verified/Lean/<id>.lean)' means the kernel checked a formalization (whose
|
|
1299
|
+
// fidelity the m votes then reviewed); '阻塞(…)' means the group explicitly decided not
|
|
1300
|
+
// to formalize and said why. `off` mode is untouched — no formal line at all.
|
|
1301
|
+
const formalLine=formalOn()?('\n- 形式化: '+formalStatusLine(vs.targetId)):''
|
|
1302
|
+
const text='# 已验证|'+vs.targetId+'\n- ID: '+vs.targetId+'\n- 类型: '+type+'\n- 结论: '+(isTrue?'真':'假')+'\n- 概率: '+(isTrue?1:0)+'\n- 来源: 全体常驻一致'+formalLine+'\n## 陈述\n参见来源卡。\n'
|
|
677
1303
|
await writeText('Verified/'+dir+'/'+vs.targetId+'.md', text)
|
|
678
1304
|
}
|
|
679
1305
|
// Does `content` declare the target as its card ID? Accept both the exact `- ID: <id>` and the
|
|
@@ -753,8 +1379,11 @@ export function apply(ctx) {
|
|
|
753
1379
|
function heartbeatPrompt(r){
|
|
754
1380
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
755
1381
|
+'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
|
|
1382
|
+
+(formalOn()?(formalWorkLine()+'\n'):'')
|
|
756
1383
|
+'Reply with ONLY a JSON object:\n'
|
|
757
|
-
+'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40
|
|
1384
|
+
+'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40'
|
|
1385
|
+
+(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked|defect","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因/具体偏差"}'):'')
|
|
1386
|
+
+'}'
|
|
758
1387
|
}
|
|
759
1388
|
function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
|
|
760
1389
|
function armHeartbeat(){
|
|
@@ -948,6 +1577,10 @@ export function apply(ctx) {
|
|
|
948
1577
|
}
|
|
949
1578
|
if((kind==='verif-ind'||kind==='verif-deb') && verifyState){
|
|
950
1579
|
const v=(parsed&&parsed.vote)||{}
|
|
1580
|
+
// Lean difficulty judgement carried on the SAME reply. Handled BEFORE the vote is stored
|
|
1581
|
+
// so a voter that says "I formalized it" / "I judge this infeasible" has that recorded
|
|
1582
|
+
// together with its verdict. Never allowed to throw into the scheduler.
|
|
1583
|
+
if(parsed.formal && typeof parsed.formal==='object') await applyFormalReply(r.rId, parsed.formal)
|
|
951
1584
|
// verdict = 0-1 probability the object is TRUE (1=绝对真, 0=绝对假, 0.5=不确定);
|
|
952
1585
|
// also accept legacy 'TRUE'/'FALSE' strings AND quoted numeric strings ("0.9"), which LLMs
|
|
953
1586
|
// occasionally emit — without this a confident "0.9" was silently misread as 0.5 (uncertainty).
|
|
@@ -965,6 +1598,10 @@ export function apply(ctx) {
|
|
|
965
1598
|
await continueVerifyRound(); return
|
|
966
1599
|
}
|
|
967
1600
|
// normal turn
|
|
1601
|
+
// The `formal` reply field is honoured on EVERY turn kind (docs §4: 回执里 formal:{target,
|
|
1602
|
+
// decision:'blocked'|'used'}), because the ordinary work round is where reusable objects are
|
|
1603
|
+
// formalized and where a difficulty judgement is most often stated.
|
|
1604
|
+
if(parsed.formal && typeof parsed.formal==='object') await applyFormalReply(r.rId, parsed.formal)
|
|
968
1605
|
if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
|
|
969
1606
|
if(typeof parsed.solved==='boolean'){ reports.push({rId:r.rId,solved:parsed.solved,summary:parsed.summary||'',at:now()}); if(reports.length>100) reports.shift() } // solved-signal ring (not surfaced in status/report)
|
|
970
1607
|
if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
|
|
@@ -1002,6 +1639,9 @@ export function apply(ctx) {
|
|
|
1002
1639
|
// still-running turns keep writing into the same per-resident files the new run is about to use.
|
|
1003
1640
|
for(const [,or] of residents){ if(or.childId){ try{ subagents.interrupt(or.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } }
|
|
1004
1641
|
residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=[]; residentSeq=0; artifactCount=0; clearHeartbeat()
|
|
1642
|
+
// A fresh run starts with a clean formal slate: the ids r-1.. and p-* are reused, so
|
|
1643
|
+
// carrying a previous run's records over would let a stale `passed` open the new gate.
|
|
1644
|
+
formal={}; formalTodos=[]
|
|
1005
1645
|
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; lastSyncMeetingAt=0; finalizeLock=null; verifiedRecently.clear() // fresh run must NOT inherit stale concurrency/coordination state (busy/wakeKind/currentResident/pendingMeeting) from a previous run on the same reused session
|
|
1006
1646
|
lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
|
|
1007
1647
|
const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
|
|
@@ -1056,14 +1696,29 @@ export function apply(ctx) {
|
|
|
1056
1696
|
residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
|
|
1057
1697
|
meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
|
|
1058
1698
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
1059
|
-
|
|
1699
|
+
// The Lean knobs and the per-object formal records are part of the readable status: without
|
|
1700
|
+
// them a `require`-mode run that keeps returning 未定论 would be undiagnosable from outside.
|
|
1701
|
+
formal: formalView(),
|
|
1702
|
+
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny','formalVerify','leanCommand','leanArgs','leanTimeoutMs'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
1703
|
+
function formalReportText(){
|
|
1704
|
+
if(!formalOn()) return '- 未启用(`formalVerify` = off;可用 vibe_v4_set 切到 encourage / require)'
|
|
1705
|
+
const v=formalView()
|
|
1706
|
+
return ['- 模式:'+formalMode()+'('+(formalMode()==='require'?'强制:定论前必须有 Lean 通过或显式阻塞记录':'鼓励:按实现难度自行决定')+')',
|
|
1707
|
+
'- 已通过:'+(v.passed.join('、')||'(无)'),
|
|
1708
|
+
'- 已记录阻塞:'+(v.blocked.join('、')||'(无)'),
|
|
1709
|
+
'- 形式化待办:'+(v.todo.join('、')||'(无)'),
|
|
1710
|
+
'- 可复用库:'+formalLibRoot().replace(/\\/g,'/')+'/ 与 '+formalProvedRoot().replace(/\\/g,'/')+'/(跨项目)|本项目形式化:Formal/|归档证明:Verified/Lean/'].join('\n')
|
|
1711
|
+
}
|
|
1060
1712
|
function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
|
|
1061
1713
|
residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
|
|
1062
1714
|
meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.size}:null,
|
|
1063
1715
|
verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
|
|
1064
1716
|
pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
|
|
1065
1717
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
1718
|
+
formal: formalView(),
|
|
1066
1719
|
meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
|
|
1720
|
+
/** Human-readable mirror of the formal state (kept OUT of the JSON report shape). */
|
|
1721
|
+
function formalReport(){ return {ok:true, formalMode:formalMode(), formalReport:'## Lean 形式化\n'+formalReportText(), formal:formalView()} }
|
|
1067
1722
|
async function addMember(direction){
|
|
1068
1723
|
// Adding a member starts a REAL resident turn (spawnResident → brainstorm) — refuse unless the
|
|
1069
1724
|
// run is live: on a concluded (autoDone) or never-started/paused run the new member would work
|
|
@@ -1100,7 +1755,16 @@ export function apply(ctx) {
|
|
|
1100
1755
|
function normalizeParam(k, v){
|
|
1101
1756
|
const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery','stallAutoMeetingMs']
|
|
1102
1757
|
if(INT_KEYS.includes(k)){ const n=Number(v); if(!Number.isFinite(n)) return v; return Math.floor(n) }
|
|
1103
|
-
if(k==='toolAllow'||k==='toolDeny'){ if(Array.isArray(v)) return v.map(x=>String(x).trim()).filter(Boolean); if(typeof v==='string') return v.split(',').map(x=>x.trim()).filter(Boolean); return [] }
|
|
1758
|
+
if(k==='toolAllow'||k==='toolDeny'||k==='leanArgs'){ if(Array.isArray(v)) return v.map(x=>String(x).trim()).filter(Boolean); if(typeof v==='string') return v.split(',').map(x=>x.trim()).filter(Boolean); return [] }
|
|
1759
|
+
// ---- Lean formal verification (docs §1) ------------------------------------------
|
|
1760
|
+
// `formalVerify` is a three-way enum and MUST degrade to the no-op 'off' on anything else.
|
|
1761
|
+
// Degrading to a STRONGER mode would let a typo silently gate every conclusion — the exact
|
|
1762
|
+
// failure mode `require` is supposed to avoid.
|
|
1763
|
+
if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'off'
|
|
1764
|
+
// A blank command would make resolveExecutable('') fail confusingly; fall back to the default.
|
|
1765
|
+
if(k==='leanCommand'){ const s=String(v==null?'':v).trim(); return s||'lean' }
|
|
1766
|
+
// A non-positive timeout is meaningless (the run would be killed instantly) → default.
|
|
1767
|
+
if(k==='leanTimeoutMs'){ const n=Number(v); if(!Number.isFinite(n)||n<=0) return DEFAULT_PARAMS.leanTimeoutMs; return Math.floor(n) }
|
|
1104
1768
|
return v
|
|
1105
1769
|
}
|
|
1106
1770
|
/**
|
|
@@ -1169,6 +1833,41 @@ export function apply(ctx) {
|
|
|
1169
1833
|
frameworkRoot:frameworkRoot, currentProject:()=>currentProject, problemText:()=>problemText,
|
|
1170
1834
|
residentCount:()=>residents.size,
|
|
1171
1835
|
busyCount:()=>busy.size,
|
|
1836
|
+
// Lean formal verification (docs/formal-verification.md) — exposed to the tool layer and to
|
|
1837
|
+
// the test suites exactly as v5 exposes its own helpers.
|
|
1838
|
+
formalMode, formalOn, formalRecords, formalTodo, formalOf, formalView, formalReport,
|
|
1839
|
+
rebuildLeanLibIndexes, leanArchive, leanRunTool, writeFormalIndex, writeFormalTodo,
|
|
1840
|
+
leanRunToolApi: async (relPath,timeoutMs)=>await leanRunFile(relPath,timeoutMs),
|
|
1841
|
+
/**
|
|
1842
|
+
* The prompt builders, addressed BY RESIDENT ID. "成员读到的文字就是产品"
|
|
1843
|
+
* (AUDIT-CHECKLIST §0.1): a suite that can only observe tool return values is blind to a
|
|
1844
|
+
* prompt defect, so the exact strings a resident would receive must be directly readable.
|
|
1845
|
+
* These are pure builders — calling one has no side effects on the run.
|
|
1846
|
+
*/
|
|
1847
|
+
promptApi:{
|
|
1848
|
+
normal:(rId)=>{ const r=residents.get(String(rId)); return r?normalPrompt(r):'' },
|
|
1849
|
+
heartbeat:(rId)=>{ const r=residents.get(String(rId)); return r?heartbeatPrompt(r):'' },
|
|
1850
|
+
brainstorm:(rId)=>{ const r=residents.get(String(rId)); return r?brainstormPrompt(r):'' },
|
|
1851
|
+
coreRules:()=>coreRulesBrief(),
|
|
1852
|
+
// `vs` mirrors the live verification state ({targetId,targetType,targetOwner,stage,history,verdicts});
|
|
1853
|
+
// pass one explicitly to ask "what WOULD the voters read for this object right now?".
|
|
1854
|
+
verify:(rId,vs)=>{ const r=residents.get(String(rId)); return r?verifyPrompt(r, vs||verifyState||{targetId:'',targetType:'proposition',targetOwner:'',stage:'independent',verdicts:{}}):'' },
|
|
1855
|
+
formalBlock:(target)=>formalPromptBlock(target),
|
|
1856
|
+
formalWorkLine:()=>formalWorkLine(),
|
|
1857
|
+
},
|
|
1858
|
+
/** Dispatcher behind vibe_v4_prompts (kept here so the tool layer never re-implements it). */
|
|
1859
|
+
promptFor:async (which,rId,arg)=>{
|
|
1860
|
+
const r=residents.get(String(rId))
|
|
1861
|
+
if(!r) return ''
|
|
1862
|
+
if(which==='brainstorm') return brainstormPrompt(r)
|
|
1863
|
+
if(which==='heartbeat') return heartbeatPrompt(r)
|
|
1864
|
+
if(which==='coreRules') return coreRulesBrief()
|
|
1865
|
+
if(which==='verify'){
|
|
1866
|
+
const target=idSafe(String((arg&&arg.target)||''))
|
|
1867
|
+
return verifyPrompt(r,{targetId:target,targetType:guessTargetType(target),targetOwner:'',stage:String((arg&&arg.stage)||'independent'),history:{},verdicts:{}})
|
|
1868
|
+
}
|
|
1869
|
+
return normalPrompt(r)
|
|
1870
|
+
},
|
|
1172
1871
|
}
|
|
1173
1872
|
} // end makeSession
|
|
1174
1873
|
|
|
@@ -1195,6 +1894,16 @@ export function apply(ctx) {
|
|
|
1195
1894
|
registerTool('vibe_v4_abort','Abort V4 and interrupt residents.',objParams({}),(s)=>s.initAbort())
|
|
1196
1895
|
registerTool('vibe_v4_status','Show V4 status.',objParams({}),(s)=>s.status())
|
|
1197
1896
|
registerTool('vibe_v4_report','Return the V4 progress report.',objParams({}),(s)=>s.report())
|
|
1897
|
+
registerTool('vibe_v4_formal_report','Human-readable Lean formal-verification mirror (mode, Lean-passed objects, recorded blockers, formalization TODO, library paths).',objParams({}),(s)=>s.formalReport())
|
|
1898
|
+
// The exact text a resident would receive. "成员读到的文字就是产品" (AUDIT-CHECKLIST §0.1): a host
|
|
1899
|
+
// (or an audit) must be able to READ the prompt, not just the tool return values, or a prompt
|
|
1900
|
+
// defect stays invisible. Pure builder calls — no side effects on the run.
|
|
1901
|
+
registerTool('vibe_v4_prompts','Read the exact prompt text a resident would receive (which: brainstorm|normal|heartbeat|verify|coreRules). member = resident id; target/stage describe the object for `verify`. Prompt text is the product — this makes it auditable.',objParams({which:{type:'string',enum:['brainstorm','normal','heartbeat','verify','coreRules']},member:{type:'string'},target:{type:'string'},stage:{type:'string'}},['which']),async (s,a)=>{
|
|
1902
|
+
const which=String(a.which||'normal')
|
|
1903
|
+
if(which==='coreRules') return {ok:true,which,text:await s.promptApi.coreRules()}
|
|
1904
|
+
const text=await s.promptFor(which,String(a.member||'r-1'),a)
|
|
1905
|
+
return {ok:true,which,member:String(a.member||'r-1'),text:typeof text==='string'?text:String(text||'')}
|
|
1906
|
+
})
|
|
1198
1907
|
registerTool('vibe_v4_message','Inject a message to a resident (or all).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a)=>{ const to=a.to||'all'; if(to==='all') return s.broadcast(a.content); return s.postMessage('facilitator',to,a.content) })
|
|
1199
1908
|
registerTool('vibe_v4_meeting','Start a meeting (coordinate / allocate / propose verification).',objParams({agenda:{type:'string'}},['agenda']),(s,a)=>s.startMeeting(a.agenda))
|
|
1200
1909
|
registerTool('vibe_v4_list_members','List residents.',objParams({}),(s)=>({ok:true,residents:s.listResidents()}))
|
|
@@ -1203,7 +1912,11 @@ export function apply(ctx) {
|
|
|
1203
1912
|
// model/provider inheritance: set model/provider to override the residents' LLM route (''=inherit
|
|
1204
1913
|
// the main assistant's route). toolAllow/toolDeny are per-resident tool permissions (scoped
|
|
1205
1914
|
// restrict). residentPersona prepends a persona line to every resident prompt.
|
|
1206
|
-
|
|
1915
|
+
// Lean knobs (docs/formal-verification.md §1): formalVerify is the three-way mode switch (the
|
|
1916
|
+
// MODE is dynamic — switching it changes the very next prompt), leanCommand/leanArgs select the
|
|
1917
|
+
// executable, leanTimeoutMs bounds one run. Invalid values fall back to the defaults and an
|
|
1918
|
+
// unknown mode degrades to 'off' (never to a STRONGER mode).
|
|
1919
|
+
registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line; formalVerify: "off" (default, a true no-op) | "encourage" (residents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a unanimous true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record); leanCommand/leanArgs/leanTimeoutMs configure the toolchain.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},stallAutoMeetingMs:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}},formalVerify:{type:'string',enum:['off','encourage','require']},leanCommand:{type:'string'},leanArgs:{type:'array',items:{type:'string'}},leanTimeoutMs:{type:'integer'}}),(s,a)=>{ s.setParams(a); return s.status() })
|
|
1207
1920
|
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
1208
1921
|
// fall back to the last-woken resident when called by the host/assistant.
|
|
1209
1922
|
registerTool('vibe_v4_send_message','(resident) Send a message to another resident (to=all broadcasts to the whole team).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>{ const from=s.residentIdOf(x); if(!from) return {ok:false,message:'no such resident'}; if(String(a.to)==='all') return s.broadcast(a.content, from); return s.postMessage(from,a.to,a.content) })
|
|
@@ -1223,11 +1936,26 @@ export function apply(ctx) {
|
|
|
1223
1936
|
registerTool('vibe_v4_claim_write','Reserved: shared-file write lock (framework-managed).',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target}))
|
|
1224
1937
|
registerTool('vibe_v4_release_write','Reserved: shared-file write lock release.',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target}))
|
|
1225
1938
|
|
|
1939
|
+
// ── Lean formal verification (docs/formal-verification.md §5) ─────────────
|
|
1940
|
+
// These three tools are registered UNCONDITIONALLY. Registration is STATIC (a mode-dependent
|
|
1941
|
+
// registration would be a dynamic effect and break the ctx.effect discipline), while the MODE
|
|
1942
|
+
// only decides whether the framework TELLS residents about them: in 'off' mode they still work
|
|
1943
|
+
// if a human or an agent calls them deliberately, but no prompt mentions them.
|
|
1944
|
+
registerTool('vibe_v4_lean_run','(resident) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output verbatim, a timeout returns LEAN_TIMEOUT. Pass target=<object id> to also record the run against that object.',objParams({file:{type:'string'},target:{type:'string'},timeout_ms:{type:'integer'}},['file']),(s,a,x)=>s.leanRunTool(s.residentIdOf(x),a))
|
|
1945
|
+
registerTool('vibe_v4_lean_archive',"(resident) Archive Lean code. kind=\"def\": a REUSABLE definition/object/assumption → the global cross-project library (Formal/Lib). kind=\"lemma\": a machine-checked lemma → Formal/Proved. kind=\"proof\": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind=\"blocked\": record an explicit, reasoned \"cannot/not worth formalizing\" decision (note required).",objParams({kind:{type:'string',enum:['def','lemma','proof','blocked']},name:{type:'string'},target:{type:'string'},content:{type:'string'},from:{type:'string'},note:{type:'string'},run:{type:'boolean'}},['kind']),(s,a,x)=>s.leanArchive(s.residentIdOf(x),a))
|
|
1946
|
+
registerTool('vibe_v4_lean_lib','(resident) List (and by default rebuild) the Lean reuse library: this project\'s Formal/Index.md, plus the global cross-project Formal/Lib and Formal/Proved indexes. Look here BEFORE writing a new definition so you reuse instead of redefining.',objParams({refresh:{type:'boolean'}}),async (s,a)=>{
|
|
1947
|
+
const r=(a&&a.refresh===false)?{lib:null,proved:null,objects:Object.keys(s.formalRecords()).length}:await s.rebuildLeanLibIndexes()
|
|
1948
|
+
return { ok:true, mode:s.formalMode(), rebuilt:!(a&&a.refresh===false), counts:r, todo:s.formalTodo(),
|
|
1949
|
+
objects:s.formalView().objects,
|
|
1950
|
+
paths:{project:'Formal/(相对项目根)',lib:'VibeMath/Formal/Lib/',proved:'VibeMath/Formal/Proved/',proofs:'Verified/Lean/'},
|
|
1951
|
+
hint:"复用优先:先在 Lib/ 里找现成定义;新定义用 vibe_v4_lean_archive kind='def' 归档,已证引理用 kind='lemma'。" }
|
|
1952
|
+
})
|
|
1953
|
+
|
|
1226
1954
|
// Same lifecycle rule as registerTool: commands.register() returns a disposer, so the
|
|
1227
1955
|
// registration belongs to this fiber and must be unwound with it.
|
|
1228
1956
|
ctx.effect(() => commands.register({
|
|
1229
1957
|
name:'v4', description:'control the Vibe Math V4 framework',
|
|
1230
|
-
input:{hint:'[configure|start|resume|pause|abort|status|report|meeting|members|add|remove|set]'},
|
|
1958
|
+
input:{hint:'[configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set]'},
|
|
1231
1959
|
handler: async function(inv){
|
|
1232
1960
|
const s=getSession(inv&&inv.agent); if(!s) return {kind:'success',text:JSON.stringify({ok:false,error:'no session'})}
|
|
1233
1961
|
const line=String(inv&&inv.rawInput?inv.rawInput:'').trim(); const parts=line.split(/\s+/); const cmd=parts[0]||''; const rest=parts.slice(1)
|
|
@@ -1240,11 +1968,12 @@ export function apply(ctx) {
|
|
|
1240
1968
|
else if(cmd==='status') r=s.status()
|
|
1241
1969
|
else if(cmd==='report') r=s.report()
|
|
1242
1970
|
else if(cmd==='meeting') r=await s.startMeeting(rest.join(' '))
|
|
1971
|
+
else if(cmd==='message'){ const to=rest[0]||'all'; const content=rest.slice(1).join(' '); r=!content?{ok:false,usage:'message <to|all> <content>'}:((to==='all')?await s.broadcast(content):await s.postMessage('facilitator',to,content)) }
|
|
1243
1972
|
else if(cmd==='members') r={ok:true,residents:s.listResidents()}
|
|
1244
1973
|
else if(cmd==='add') r=await s.addMember(rest.join(' '))
|
|
1245
1974
|
else if(cmd==='remove') r=await s.removeMember(rest[0]||'')
|
|
1246
1975
|
else if(cmd==='set'){ const upd={}; for(const tok of rest){ const eq=tok.indexOf('='); if(eq>0){ const k=tok.slice(0,eq); const rv=tok.slice(eq+1); const n=Number(rv); upd[k]=Number.isFinite(n)?n:rv } } r=s.setParams(upd) }
|
|
1247
|
-
else r={ok:false,usage:'configure|start|resume|pause|abort|status|report|message|meeting|members|add|remove|set'}
|
|
1976
|
+
else r={ok:false,usage:'configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set'}
|
|
1248
1977
|
return {kind:'success',text:JSON.stringify(r,null,2)}
|
|
1249
1978
|
},
|
|
1250
1979
|
}))
|