dsh-vibe-math 2.6.0 → 2.7.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/README.en.md +161 -389
- package/README.md +161 -390
- package/cordis.patch.yml +62 -14
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +1 -0
- package/docs/formal-verification.md +184 -29
- package/docs/math-computation.md +163 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.1.en.md +86 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
- package/docs/test-timing.md +24 -12
- package/installer.js +7 -4
- package/package.json +21 -3
- package/prompt-corpus-persona/persona-corpus.json +12 -12
- package/prompt-corpus-persona/persona-corpus.md +128 -32
- package/prompt-corpus-v2/formal-verify-v2.json +182 -92
- package/prompt-corpus-v2/formal-verify-v2.md +2379 -55
- package/prompt-corpus-v3/formal-verify-v3.json +215 -53
- package/prompt-corpus-v3/formal-verify-v3.md +2147 -22
- package/prompt-corpus-v4/formal-verify-v4.json +14 -14
- package/prompt-corpus-v4/formal-verify-v4.md +267 -14
- package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
- package/prompt-corpus-v5/prompt-corpus-v5.md +631 -3
- package/tests/audit-math-computation-contract.mjs +204 -0
- package/tests/audit-math-computation-parity.mjs +198 -0
- package/tests/audit-math-computation-sensitivity.mjs +231 -0
- package/tests/audit-persona-sensitivity.mjs +4 -4
- package/tests/audit-persona-surface.test.mjs +38 -10
- package/tests/audit-v5-integrity.mjs +14 -0
- package/tests/e2e-v5-round2.test.mjs +459 -2
- package/tests/formal-verify-v2.test.mjs +362 -8
- package/tests/formal-verify-v3.test.mjs +355 -6
- package/tests/formal-verify-v4.test.mjs +295 -6
- package/tests/formal-verify-v5.test.mjs +9 -1
- package/tests/helpers/math-computation-fake-seam.mjs +163 -0
- package/tests/math-computation-shared.test.mjs +806 -0
- package/tests/prompt-v5-integrity.test.mjs +5 -5
- package/vibe-math-v2/agent.cordis.yml +22 -8
- package/vibe-math-v2/math-computation.js +1279 -0
- package/vibe-math-v2/math-engines.js +197 -0
- package/vibe-math-v2/vibe-math-v2.js +853 -40
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v3/agent.cordis.yml +22 -8
- package/vibe-math-v3/math-computation.js +1279 -0
- package/vibe-math-v3/math-engines.js +197 -0
- package/vibe-math-v3/vibe-math-v3.js +844 -47
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v4/agent.cordis.yml +38 -6
- package/vibe-math-v4/math-computation.js +1279 -0
- package/vibe-math-v4/math-engines.js +197 -0
- package/vibe-math-v4/vibe-math-v4.js +816 -31
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v5/agent.cordis.yml +42 -6
- package/vibe-math-v5/math-computation.js +1279 -0
- package/vibe-math-v5/math-engines.js +197 -0
- package/vibe-math-v5/vibe-math-v5.js +1132 -37
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
|
@@ -12,6 +12,34 @@
|
|
|
12
12
|
// only from the dynamic-package start path); v2/v3 use those globals and work. An earlier
|
|
13
13
|
// version of this note claimed the globals do not exist — false for file rows, and it would
|
|
14
14
|
// only become true if this preset were ever converted to a dynamic package.
|
|
15
|
+
// The Lean job id and the archive de-duplication are CONTENT-hash based (`lean-incremental-async`
|
|
16
|
+
// spec §4.3), so the plugin needs a real SHA-256. `node:crypto` is a plain Node builtin here: a
|
|
17
|
+
// preset is a FILE row loaded by a host-realm import(), and the plugin already relies on Node
|
|
18
|
+
// builtins (`process.platform`) — see the header note about the vm sandbox applying only to a
|
|
19
|
+
// DYNAMIC package's host half.
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
21
|
+
// math_computation (docs/math-computation.md, `_oneoff/mc-P1-ready/INTERFACE-FREEZE.md` §5): the tool
|
|
22
|
+
// CORE lives in the shared module — installed byte-identically into all four presets by the
|
|
23
|
+
// integration owner and checked by `tests/audit-math-computation-parity.mjs`. This preset only WIRES
|
|
24
|
+
// it: params, one registration, and the prompt text. Names/shapes come from the module, never
|
|
25
|
+
// re-spelled here (the six parameter names are a frozen cross-preset contract).
|
|
26
|
+
import {
|
|
27
|
+
registerMathComputation,
|
|
28
|
+
probeMathEngines,
|
|
29
|
+
MATH_TOOL_DESCRIPTION,
|
|
30
|
+
MATH_PERSONA_TOOL_LINE,
|
|
31
|
+
MATH_RULE_LINES,
|
|
32
|
+
MATH_RULE_LINES_EN,
|
|
33
|
+
MATH_ARCHIVE_WORKFLOW_LINE,
|
|
34
|
+
MATH_ARCHIVE_WORKFLOW_LINE_EN,
|
|
35
|
+
MATH_SUBSTITUTION_RULE_LINE,
|
|
36
|
+
MATH_SUBSTITUTION_RULE_LINE_EN,
|
|
37
|
+
MATH_PARAM_DEFAULTS,
|
|
38
|
+
MATH_PARAM_NAMES,
|
|
39
|
+
normalizeMathParams,
|
|
40
|
+
mathAvailabilityLine,
|
|
41
|
+
} from './math-computation.js'
|
|
42
|
+
|
|
15
43
|
export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands', 'timer']
|
|
16
44
|
|
|
17
45
|
// ---- host live-child cap (DSH ≥ 0.2) ----------------------------------------
|
|
@@ -43,6 +71,15 @@ function hostChildLimitHint(limit){
|
|
|
43
71
|
}
|
|
44
72
|
|
|
45
73
|
export function apply(ctx) {
|
|
74
|
+
// math_computation (v5-style structure, per-session module instances):
|
|
75
|
+
// 1. a THROWAWAY capture-only host yields the frozen tool face (name/description/schema) once;
|
|
76
|
+
// 2. the tool is registered ONCE with a router that resolves the CALLING session;
|
|
77
|
+
// 3. each session lazily gets its OWN `registerMathComputation(...)` instance, cached by that
|
|
78
|
+
// session's host object, so there is NO "current session" slot at all and two sessions can run
|
|
79
|
+
// math calls CONCURRENTLY without ever seeing each other's params/root/fs/probe cache.
|
|
80
|
+
// (An earlier revision used a single holder plus a plugin-level serialisation chain; a shared slot
|
|
81
|
+
// was only safe while calls could not overlap, and serialising cost cross-session parallelism.)
|
|
82
|
+
let mathToolFace = null
|
|
46
83
|
const subagents = ctx.subagents
|
|
47
84
|
const agents = ctx.agents
|
|
48
85
|
const fs = ctx.fs
|
|
@@ -90,6 +127,23 @@ export function apply(ctx) {
|
|
|
90
127
|
leanCommand: 'lean',
|
|
91
128
|
leanArgs: [], // inserted BEFORE the file name (e.g. ['env','lean'] with lake)
|
|
92
129
|
leanTimeoutMs: 120000,
|
|
130
|
+
// ---- incremental + async Lean compilation (lean-incremental-async spec §1.1) ----------
|
|
131
|
+
// true (default): `lean_run` / `lean_archive{run:true}` ENQUEUE a background job and return
|
|
132
|
+
// immediately (`async:{jobId,state:'queued'}`); the queue is drained from the existing
|
|
133
|
+
// heartbeat. false: the historical synchronous `await` path, unchanged. The compile budget
|
|
134
|
+
// is `leanTimeoutMs`; the executable/args are `leanCommand`/`leanArgs` (no new parameter).
|
|
135
|
+
leanAsync: true,
|
|
136
|
+
// AMENDMENT §1: PROACTIVITY is a separate knob from the verification requirement.
|
|
137
|
+
// `formalVerify` says how strong the check must be at VOTING time; `leanInitiative` says how
|
|
138
|
+
// eager the group should be while WORKING. 'off' = no proactive nagging (verify-time only),
|
|
139
|
+
// 'normal' = today's "顺手形式化" line, 'eager' = actively formalize valuable small
|
|
140
|
+
// lemmas/propositions/definitions as they appear.
|
|
141
|
+
leanInitiative: 'normal',
|
|
142
|
+
// AMENDMENT §2: extra search roots, injected in ORDER before the automatic VibeMath root,
|
|
143
|
+
// de-duplicated; an explicit --search-path/-R/--root in `leanArgs` still wins outright.
|
|
144
|
+
leanSearchPaths: [],
|
|
145
|
+
// AMENDMENT §5: how many background compiles may run at once (default 1 = serial).
|
|
146
|
+
leanJobsMaxParallel: 1,
|
|
93
147
|
// ---- final paper (docs/final-paper.md §1) ----------------------------------
|
|
94
148
|
// The run's conclusion is a PAPER the whole team co-authors once consensus has converged.
|
|
95
149
|
// finalPaper=true triggers it automatically at that moment (idempotent); the other four knobs
|
|
@@ -102,8 +156,103 @@ export function apply(ctx) {
|
|
|
102
156
|
// `facilitator` roster member with an LLM — or one named resident (`resident:<id>`).
|
|
103
157
|
paperEditor: 'office',
|
|
104
158
|
paperLatexCommand: '', // preferred LaTeX executable ('' = the standard detection order)
|
|
159
|
+
// ---- math_computation (docs/math-computation.md) ---------------------------
|
|
160
|
+
// The six MATH_* parameter names are a FROZEN cross-preset contract; the VALUES come from the
|
|
161
|
+
// shared module so a drift in one preset cannot diverge from the others (the integration
|
|
162
|
+
// owner's parity guard compares the four presets). `mathEngines`/`mathPackages` are copied,
|
|
163
|
+
// never aliased: `MATH_PARAM_DEFAULTS` is frozen and shared.
|
|
164
|
+
mathComputation: MATH_PARAM_DEFAULTS.mathComputation, // 'auto' | 'on' | 'off' (off = true no-op)
|
|
165
|
+
mathMode: MATH_PARAM_DEFAULTS.mathMode, // 'typed+shell' | 'typed'
|
|
166
|
+
mathEngines: MATH_PARAM_DEFAULTS.mathEngines.slice(),
|
|
167
|
+
mathTimeoutMs: MATH_PARAM_DEFAULTS.mathTimeoutMs,
|
|
168
|
+
mathPackages: MATH_PARAM_DEFAULTS.mathPackages.slice(),
|
|
169
|
+
mathInstallScope: MATH_PARAM_DEFAULTS.mathInstallScope,
|
|
105
170
|
}
|
|
106
171
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
172
|
+
// ── math_computation availability line (P1) ────────────────────────────────────────────────
|
|
173
|
+
// The tool itself is registered ONCE at apply level (the host adapter there resolves the calling
|
|
174
|
+
// session dynamically). The PROMPT line is per-session state, so it is derived here from a
|
|
175
|
+
// session-local probe host: `probeMathEngines` caches per host object, so the expensive probe
|
|
176
|
+
// runs once per session and every later prompt re-uses it. `mathComputation:'off'` is a true
|
|
177
|
+
// no-op (no probe, no line).
|
|
178
|
+
const mathProbeHost = {
|
|
179
|
+
register: ()=>{}, // probeMathEngines never registers; adaptHost requires it
|
|
180
|
+
params: ()=>params,
|
|
181
|
+
projectRoot: ()=>frameworkRoot(),
|
|
182
|
+
writeText: (rel,text)=>writeText(rel,text),
|
|
183
|
+
readText: (rel)=>readText(rel),
|
|
184
|
+
exists: async (rel)=>{ try { const t=await fsTarget(rel); return (await fs.stat(t))!==undefined } catch(e){ return false } },
|
|
185
|
+
// P2a/audit-R2: the module's per-project retention check (MATH_ARCHIVE_MAX_RUNS) only runs
|
|
186
|
+
// when the host exposes listDir. Root ('', '.') means the project root. The field is attached
|
|
187
|
+
// ONLY when the fs surface actually has the method, so "no listDir" stays expressible (a
|
|
188
|
+
// never-absent function would make the module's `typeof H.listDir === 'function'` vacuous).
|
|
189
|
+
...(typeof fs.listDir === 'function' ? { listDir: async (rel)=>{ try { const t=await fsTarget(rel===''||rel==='.'?'':rel); return (await fs.listDir(t))||[] } catch(e){ return [] } } } : {}),
|
|
190
|
+
resolveExecutable: async (cmd)=>{
|
|
191
|
+
const sub=subprocessOf()
|
|
192
|
+
if(sub===undefined||typeof sub.resolveExecutable!=='function') throw new Error('NO_SUBPROCESS')
|
|
193
|
+
return await sub.resolveExecutable(String(cmd))
|
|
194
|
+
},
|
|
195
|
+
spawn: async (o)=>{ const r=await mathSpawnAdapter(o); return r },
|
|
196
|
+
// Same capability flag as the registered host: the prompt-side probe should not advertise
|
|
197
|
+
// engines on a host that cannot execute at all.
|
|
198
|
+
hasSubprocess: ()=>!!subprocessOf(),
|
|
199
|
+
// round-7 (fix 2): DSH's own bundled runtimes (`<home>/.dsh/dsh-runtimes/*/dependencies/<engine>/`)
|
|
200
|
+
// as a LAST resort after PATH. Generic roots only; the module globs the tree name (never
|
|
201
|
+
// `dsh-primary-runtime`) and accepts only the descriptor's own candidate names.
|
|
202
|
+
runtimeRoots: () => { const env = process.env.DSH_HOME; if (env) return [String(env)]; const home = String(process.env.HOME || process.env.USERPROFILE || ''); return home ? [home.replace(/[\\/]+$/, '') + '/.dsh'] : [] },
|
|
203
|
+
listDirAbs: async (abs)=>{ try { const t=await fsTarget(abs); const st=await fs.stat(t); if(!st) return []; return (await fs.listDir(t))||[] } catch(e){ return [] } },
|
|
204
|
+
log: (kind,msg)=>logActivity(String(kind||'math'),String(msg||'')),
|
|
205
|
+
refreshAvailability: ()=>refreshMathAvailability(),
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The shared module's spawn contract is flat — `{argv,cwd,timeoutMs,stdoutCap,stderrCap}` →
|
|
209
|
+
* `{exit,timedOut,killed,ms,stdout,stderr}` — while the host service hands back a live handle.
|
|
210
|
+
* A TIMEOUT MUST ACTIVELY KILL: the same race-and-terminate discipline `leanRunFile` uses, so the
|
|
211
|
+
* module is never told "timed out" while the process is still running. Output is collected up to
|
|
212
|
+
* the caller's cap; `run` passes 4MB on purpose (the module archives the COMPLETE output itself
|
|
213
|
+
* and only truncates its REPLY to 64KB).
|
|
214
|
+
*/
|
|
215
|
+
async function mathSpawnAdapter(o){
|
|
216
|
+
const sub=subprocessOf()
|
|
217
|
+
// `null` is the module's documented "this host cannot execute" signal (it maps it to
|
|
218
|
+
// MATH_NO_SUBPROCESS), so it must NOT be papered over with an empty success-looking result.
|
|
219
|
+
if(sub===undefined||typeof sub.spawn!=='function') return null
|
|
220
|
+
const spec=o||{}
|
|
221
|
+
const cap=Math.max(1000,Math.floor(Number(spec.timeoutMs))||MATH_PARAM_DEFAULTS.mathTimeoutMs)
|
|
222
|
+
const outCap=Math.max(1024,Math.floor(Number(spec.stdoutCap))||64*1024)
|
|
223
|
+
const errCap=Math.max(1024,Math.floor(Number(spec.stderrCap))||64*1024)
|
|
224
|
+
const started=now()
|
|
225
|
+
let handle
|
|
226
|
+
try { handle=sub.spawn({ argv:Array.isArray(spec.argv)?spec.argv:[], cwd:spec.cwd||frameworkRoot(), stdio:{stdin:'ignore',stdout:{maxBytes:outCap},stderr:{maxBytes:errCap}}, graceMs:cap }) }
|
|
227
|
+
catch(e){ return {exit:null,timedOut:false,killed:false,ms:now()-started,stdout:'',stderr:String((e&&e.message)||e)} }
|
|
228
|
+
let killedByUs=false, timerDispose=null, outcome
|
|
229
|
+
try {
|
|
230
|
+
outcome=await Promise.race([
|
|
231
|
+
handle.done,
|
|
232
|
+
new Promise(function(resolve){
|
|
233
|
+
if(typeof ctx.timeout!=='function') return // no timer service: the host grace window is all we have
|
|
234
|
+
timerDispose=ctx.timeout(function(){
|
|
235
|
+
killedByUs=true
|
|
236
|
+
try { if(typeof handle.terminate==='function') handle.terminate() } catch(e){ /* best effort */ }
|
|
237
|
+
resolve({exitCode:null,signal:'SIGTERM'})
|
|
238
|
+
}, cap)
|
|
239
|
+
}),
|
|
240
|
+
])
|
|
241
|
+
} catch(e){
|
|
242
|
+
if(timerDispose){ try { timerDispose() } catch(_e){} }
|
|
243
|
+
return {exit:null,timedOut:false,killed:killedByUs,ms:now()-started,stdout:'',stderr:String((e&&e.message)||e)}
|
|
244
|
+
}
|
|
245
|
+
if(timerDispose){ try { timerDispose() } catch(e){ /* already fired */ } }
|
|
246
|
+
let out='', err=''
|
|
247
|
+
try { if(handle.collected&&handle.collected.stdout) out=handle.collected.stdout.readFrom(0).text } catch(e){ /* best effort */ }
|
|
248
|
+
try { if(handle.collected&&handle.collected.stderr) err=handle.collected.stderr.readFrom(0).text } catch(e){ /* best effort */ }
|
|
249
|
+
const ms=now()-started
|
|
250
|
+
const exit=(outcome&&outcome.exitCode!==undefined)?outcome.exitCode:null
|
|
251
|
+
// Two ways a timeout is observed (identical to `leanRunFile`): OUR timer won and killed the
|
|
252
|
+
// handle, or the host's own grace window killed it first and its `done` beat our timer.
|
|
253
|
+
const timedOut=killedByUs||(exit!==0&&ms>=cap)
|
|
254
|
+
return {exit, timedOut, killed:killedByUs, ms, stdout:out, stderr:err}
|
|
255
|
+
}
|
|
107
256
|
let running = false, autoDone = false, phase = 'idle'
|
|
108
257
|
let residents = new Map(), mailboxes = new Map(), taskboard = [], decisions = []
|
|
109
258
|
let meetings = [], reports = [], activityLog = []
|
|
@@ -119,6 +268,17 @@ export function apply(ctx) {
|
|
|
119
268
|
// existence, v2 §C), so a re-trigger after a restart is still recognised even though this object
|
|
120
269
|
// did not survive. While a paper is in flight this holds the whole team-authoring state machine.
|
|
121
270
|
let paperState = null
|
|
271
|
+
// ---- Lean compile queue (lean-incremental-async spec §2) --------------------------------
|
|
272
|
+
// Per-SESSION in-memory FIFO (created/destroyed with the session), mirrored to
|
|
273
|
+
// `<project>/Formal/Jobs/<jobId>.json` for crash recovery and `lean_lib.jobs`. Concurrency is a
|
|
274
|
+
// CONSTANT (1): compiling competes with the model for CPU and Lean parallelises internally, so
|
|
275
|
+
// the queue serialises instead of fanning out; the cap is not a parameter (D5: reuse, don't
|
|
276
|
+
// grow the parameter surface).
|
|
277
|
+
const LEAN_QUEUE_CONCURRENCY = 1 // the DEFAULT cap; `leanJobsMaxParallel` overrides it (AMENDMENT §5)
|
|
278
|
+
let leanQueue = [] // job ids waiting (queued, not started)
|
|
279
|
+
let leanInflight = [] // jobs currently compiling (length ≤ the concurrency cap)
|
|
280
|
+
let leanJobs = {} // jobId -> job record (mirrors Formal/Jobs/*.json)
|
|
281
|
+
let leanNotices = [] // settled-job announcements, injected once into the next prompt
|
|
122
282
|
let busy = new Set(), wakeKind = new Map(), currentResident = ''
|
|
123
283
|
let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
|
|
124
284
|
const verifiedRecently = new Map() // targetId -> timestamp when it was closed as Verified (dedup re-propose)
|
|
@@ -270,7 +430,7 @@ export function apply(ctx) {
|
|
|
270
430
|
}
|
|
271
431
|
async function ensureDirs(){
|
|
272
432
|
const base=frameworkRoot()
|
|
273
|
-
const dirs=['Problems','Progress','Propos','Methods','Subproblems','Shared/meetings','Shared/debates','Verified/命题','Verified/问题','Verified/Lean','Formal','Reliable','Notes','State']
|
|
433
|
+
const dirs=['Problems','Progress','Propos','Methods','Subproblems','Shared/meetings','Shared/debates','Verified/命题','Verified/问题','Verified/Lean','Formal','Formal/Jobs','Computation','Reliable','Notes','State']
|
|
274
434
|
// The GLOBAL reuse library (Formal/Lib + Formal/Proved) deliberately lives beside the
|
|
275
435
|
// project tree, NOT inside it: cross-project reuse is the whole point (spec §3). It is
|
|
276
436
|
// created here so the first `lean_archive kind='def'` never has to invent its parent.
|
|
@@ -411,7 +571,10 @@ export function apply(ctx) {
|
|
|
411
571
|
catch(e){
|
|
412
572
|
return {ok:false,code:'LEAN_NOT_FOUND',message:'cannot resolve "'+String(params.leanCommand||'lean')+'": '+String((e&&e.message)||e)+' — 仍可把形式化代码写下来归档,但无法在此宿主上执行',file:rel,ms:now()-started}
|
|
413
573
|
}
|
|
414
|
-
|
|
574
|
+
// §3.1: the search path is computed HERE (never persisted into `leanArgs`): user args first,
|
|
575
|
+
// then `--search-path <VibeMath root>` unless the user already stated a search root, then the
|
|
576
|
+
// file. `lake env lean` therefore becomes `lake env lean --search-path <root> <file>` for free.
|
|
577
|
+
const argv=leanArgv(exe, abs)
|
|
415
578
|
let handle
|
|
416
579
|
try {
|
|
417
580
|
handle=sub.spawn({ argv, cwd:frameworkRoot(), stdio:{stdin:'ignore',stdout:{maxBytes:64*1024},stderr:{maxBytes:64*1024}}, graceMs:cap })
|
|
@@ -482,6 +645,338 @@ export function apply(ctx) {
|
|
|
482
645
|
}))
|
|
483
646
|
}
|
|
484
647
|
|
|
648
|
+
// ================= Lean compile queue + incremental reuse (§2-§4) =================
|
|
649
|
+
// · `leanAsync=true` (default): the tools ENQUEUE and return; the queue drains from the existing
|
|
650
|
+
// heartbeat (`armHeartbeat`), concurrency 1, budget = `leanTimeoutMs`.
|
|
651
|
+
// · `leanAsync=false`: the synchronous `await` path, verbatim.
|
|
652
|
+
// · ONLY a settled, green job whose file STILL MATCHES the enqueued content hash may set
|
|
653
|
+
// `passed` / write `Verified/Lean/<id>.lean`; every other outcome stays `attempted`.
|
|
654
|
+
const LEAN_ASYNC_ON = () => params.leanAsync !== false
|
|
655
|
+
// §3.6: the flag spelling is one constant on purpose — if a toolchain wants `-R <root>`, only
|
|
656
|
+
// this line changes and the interface stays put.
|
|
657
|
+
const LEAN_SEARCH_PATH_FLAG = '--search-path'
|
|
658
|
+
const LEAN_READ_MAX_BYTES = 64 * 1024
|
|
659
|
+
/** §3.1: never inject when the user already stated a search root (explicit config wins). */
|
|
660
|
+
function leanHasSearchFlag(args){ return (Array.isArray(args)?args:[]).some(a=>{ const s=String(a); return s===LEAN_SEARCH_PATH_FLAG||s.indexOf(LEAN_SEARCH_PATH_FLAG+'=')===0||s==='-R'||s==='--root' }) }
|
|
661
|
+
/** AMENDMENT §2: the roots to inject — USER paths first, then the automatic VibeMath root,
|
|
662
|
+
* de-duplicated, order preserved. */
|
|
663
|
+
function leanSearchPathList(){
|
|
664
|
+
const out=[]
|
|
665
|
+
for(const p of (Array.isArray(params.leanSearchPaths)?params.leanSearchPaths:[])){ const s=String(p==null?'':p).trim(); if(s&&out.indexOf(s)<0) out.push(s) }
|
|
666
|
+
const root=vibeRoot().replace(/\\/g,'/')
|
|
667
|
+
if(out.indexOf(root)<0) out.push(root)
|
|
668
|
+
return out
|
|
669
|
+
}
|
|
670
|
+
/** The injected flag pairs, or [] when the user's own `leanArgs` already state a search root. */
|
|
671
|
+
function leanSearchPathArgs(){
|
|
672
|
+
const user=(Array.isArray(params.leanArgs)?params.leanArgs:[]).map(String)
|
|
673
|
+
if(leanHasSearchFlag(user)) return []
|
|
674
|
+
const out=[]
|
|
675
|
+
for(const p of leanSearchPathList()) out.push(LEAN_SEARCH_PATH_FLAG,p)
|
|
676
|
+
return out
|
|
677
|
+
}
|
|
678
|
+
/** `[exe, ...userArgs, ...(search roots unless the user stated one), ...tail]` (§3.1 + AMENDMENT §2). */
|
|
679
|
+
function leanArgv(exe,tail){
|
|
680
|
+
const user=(Array.isArray(params.leanArgs)?params.leanArgs:[]).map(String)
|
|
681
|
+
return [exe].concat(user).concat(leanSearchPathArgs()).concat(tail?[tail]:[])
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* AMENDMENT §4: the job id pins the WHOLE BUILD CONTEXT — content + engine + normalized argv +
|
|
685
|
+
* search paths — so a result compiled under a different toolchain/flag set can never be mistaken
|
|
686
|
+
* for a verification of this one, and only THAT context's `settled(ok)` may set `passed`.
|
|
687
|
+
*/
|
|
688
|
+
async function leanBuildContext(){
|
|
689
|
+
const searchPaths=leanSearchPathList()
|
|
690
|
+
let exe=String(params.leanCommand||'lean')
|
|
691
|
+
const sub=subprocessOf()
|
|
692
|
+
try { if(sub&&typeof sub.resolveExecutable==='function') exe=String(await sub.resolveExecutable(exe)) } catch(e){ /* keep the configured name: the run itself will report LEAN_NOT_FOUND */ }
|
|
693
|
+
const argv=leanArgv(exe,null)
|
|
694
|
+
return {engine:exe,argv,argvLine:argv.join(' '),searchPaths,searchArgs:leanSearchPathArgs()}
|
|
695
|
+
}
|
|
696
|
+
/** §4.3: CRLF/CR → LF, strip trailing whitespace per line and trailing blank lines. */
|
|
697
|
+
function normalizeLeanContent(s){ return String(s==null?'':s).replace(/\r\n?/g,'\n').replace(/[ \t]+$/gm,'').replace(/\n+$/,'')+'\n' }
|
|
698
|
+
function sha256Text(s){ return createHash('sha256').update(normalizeLeanContent(s),'utf8').digest('hex') }
|
|
699
|
+
function leanJobsRel(){ return 'Formal/Jobs' }
|
|
700
|
+
function leanJobRel(jobId){ return leanJobsRel()+'/'+jobId+'.json' }
|
|
701
|
+
function leanJobId(key,buildHash){ return idSafe(String(key||'job')).replace(/\.lean$/,'')+'-'+String(buildHash).slice(0,12) }
|
|
702
|
+
/** AMENDMENT §4: the id hash covers content + engine + normalized argv + search paths. */
|
|
703
|
+
function leanBuildHashOf(content,ctx){ return sha256Text(JSON.stringify([sha256Text(content),String(ctx.engine||''),ctx.argv||[],ctx.searchPaths||[]])) }
|
|
704
|
+
async function leanJobIdFor(key,content){ const ctx=await leanBuildContext(); return {jobId:leanJobId(key,leanBuildHashOf(content,ctx)),ctx} }
|
|
705
|
+
/** Sleep through the host timer service (fiber-owned); the global fallback is what the header
|
|
706
|
+
* note documents for file rows. */
|
|
707
|
+
function leanSleep(ms){ return new Promise(res=>{ if(typeof ctx.timeout==='function'){ ctx.timeout(()=>res(),ms); return } setTimeout(res,ms) }) }
|
|
708
|
+
function leanJobView(j){ return j?{jobId:j.jobId,state:j.state,rel:j.rel,target:j.target||j.name||'',attempts:j.attempts||1,settledAt:j.settledAt||0,exitCode:(j.exitCode===undefined?null:j.exitCode)}:null }
|
|
709
|
+
function listLeanJobs(){ return Object.keys(leanJobs).sort().map(k=>leanJobView(leanJobs[k])) }
|
|
710
|
+
async function writeLeanJob(job){
|
|
711
|
+
leanJobs[job.jobId]=job
|
|
712
|
+
try { await writeJson(leanJobRel(job.jobId),job) } catch(e){ logActivity('formal','job 记录写入失败 '+job.jobId+': '+String((e&&e.message)||e)) }
|
|
713
|
+
return job
|
|
714
|
+
}
|
|
715
|
+
async function readLeanJob(jobId){ try { return await readJson(leanJobRel(jobId))||undefined } catch(e){ return undefined } }
|
|
716
|
+
/** §2.4: ONE line per settled job, injected into the next prompt that is constructed. */
|
|
717
|
+
function pushLeanNotice(job,run){
|
|
718
|
+
const sec=run&&run.ms?((run.ms/1000).toFixed(1)+'s'):'—'
|
|
719
|
+
let line
|
|
720
|
+
if(job.state==='settled'&&run&&run.ok) line='通过(exit 0,'+sec+(job.kind==='proof'?(',已归档 '+job.verifiedRel):',已重建索引')+')'
|
|
721
|
+
else if(job.state==='timeout') line='超时(leanTimeoutMs='+posMs(params.leanTimeoutMs,120000)+' 已 terminate,对象保持 attempted,可重跑)'
|
|
722
|
+
else if(job.state==='interrupted') line='中断(会话卸载或崩溃,已标记 attempted,可重跑)'
|
|
723
|
+
else if(job.state==='failed'&&run&&!run.ok&&!run.hashMatch) line='失败(文件在编译期间被改写,结果不可用——请对新内容重跑)'
|
|
724
|
+
else line='失败(exit '+String(run&&run.exitCode)+',见 stderr 尾部:'+tail((run&&run.stderr)||'',160)+')'
|
|
725
|
+
leanNotices.push('【形式化结果】'+job.jobId+':'+line)
|
|
726
|
+
logActivity('formal','形式化结果 '+job.jobId+' → '+job.state+(job.target?('(对象 '+job.target+')'):''))
|
|
727
|
+
}
|
|
728
|
+
/** Consume-and-clear the pending announcements (injected into the next constructed prompt). */
|
|
729
|
+
function pendingLeanNoticeText(){ if(!leanNotices.length) return ''; const t=leanNotices.join('\n'); leanNotices=[]; return t }
|
|
730
|
+
/**
|
|
731
|
+
* Enqueue one compile. `job` = {kind:'run'|'def'|'lemma'|'proof', key, target, rel, content}.
|
|
732
|
+
* Deduplicated by CONTENT hash (§4.3): the job id embeds `sha[0:12]`, so re-archiving identical
|
|
733
|
+
* content cannot produce a second job.
|
|
734
|
+
*/
|
|
735
|
+
async function enqueueLeanJob(job){
|
|
736
|
+
const content=String(job.content==null?'':job.content)
|
|
737
|
+
const sha=job.sha256||sha256Text(content)
|
|
738
|
+
const ctx=job.ctx||await leanBuildContext()
|
|
739
|
+
const buildHash=leanBuildHashOf(content,ctx)
|
|
740
|
+
const jobId=leanJobId(job.key,buildHash)
|
|
741
|
+
const existing=leanJobs[jobId]
|
|
742
|
+
if(existing&&(existing.state==='queued'||existing.state==='running')) return {ok:true,duplicate:true,job:existing}
|
|
743
|
+
const rec={ jobId, kind:job.kind, key:job.key, target:job.target||'', name:job.name||'', rel:job.rel, abs:job.abs||'',
|
|
744
|
+
sha256:sha, buildHash, build:{engine:ctx.engine,argv:ctx.argv,searchPaths:ctx.searchPaths}, contextChanged:false,
|
|
745
|
+
content, via:job.via||'', state:'queued', attempts:((existing&&existing.attempts)||0)+1,
|
|
746
|
+
enqueuedAt:now(), startedAt:0, settledAt:0, exitCode:null, timedOut:false, interrupted:false, run:null, verifiedRel:'' }
|
|
747
|
+
await writeLeanJob(rec)
|
|
748
|
+
leanQueue.push(jobId)
|
|
749
|
+
logActivity('formal','入队后台编译 '+jobId+'('+(job.kind||'run')+' '+job.rel+',engine='+ctx.engine+',search-path='+(ctx.searchPaths.join(',')||'(用户自带)')+')')
|
|
750
|
+
armHeartbeat() // §2.2: the queue is drained from the EXISTING heartbeat
|
|
751
|
+
return {ok:true,job:rec}
|
|
752
|
+
}
|
|
753
|
+
/** §2.1/§2.4: the ONE place an object may become `passed` (and the proof be archived). */
|
|
754
|
+
async function settleLeanJob(job,run){
|
|
755
|
+
const nowText=await readTextAbs(job.abs)
|
|
756
|
+
const hashMatch=nowText!==undefined&&sha256Text(nowText)===job.sha256
|
|
757
|
+
// AMENDMENT §4: the id already pins the build context; if the user changed the toolchain while
|
|
758
|
+
// the job was in flight, RECORD that the current context differs (the compile itself was done
|
|
759
|
+
// with the recorded context, which is what the job id attests to).
|
|
760
|
+
let contextChanged=false
|
|
761
|
+
try { const nowCtx=await leanBuildContext()
|
|
762
|
+
contextChanged=JSON.stringify([nowCtx.engine,nowCtx.argv,nowCtx.searchPaths])!==JSON.stringify([(job.build||{}).engine,(job.build||{}).argv,(job.build||{}).searchPaths])
|
|
763
|
+
if(contextChanged) logActivity('formal','作业 '+job.jobId+' 的结果属于记录中的构建上下文(engine/argv/search-paths 已被改动,未影响该次编译的结论)')
|
|
764
|
+
} catch(e){ /* best effort */ }
|
|
765
|
+
const ok=!!(run&&run.ok)&&hashMatch
|
|
766
|
+
const timedOut=!!(run&&run.timedOut)
|
|
767
|
+
const state=ok?'settled':(timedOut?'timeout':'failed')
|
|
768
|
+
const settled=Object.assign({},job,{
|
|
769
|
+
state, settledAt:now(), exitCode:(run&&run.exitCode===undefined?null:run.exitCode),
|
|
770
|
+
timedOut, interrupted:false, hashMatch, contextChanged,
|
|
771
|
+
run:{at:now(),ok,exitCode:(run&&run.exitCode===undefined?null:run.exitCode),ms:(run&&run.ms)||0,timedOut,interrupted:false,
|
|
772
|
+
hashMatch,stdoutTail:tail((run&&run.stdout)||'',800),stderrTail:tail((run&&run.stderr)||'',800)},
|
|
773
|
+
})
|
|
774
|
+
if(ok&&job.kind==='proof'){
|
|
775
|
+
// ★ ONLY here: a green run on UNCHANGED content mints the proof and flips the record.
|
|
776
|
+
settled.verifiedRel='Verified/Lean/'+job.key+'.lean'
|
|
777
|
+
await writeText(settled.verifiedRel,job.content)
|
|
778
|
+
const prev=formalOf(job.key)
|
|
779
|
+
await putFormal(job.key,Object.assign({},prev,{ status:'passed', file:job.rel, proof:settled.verifiedRel, decision:'used',
|
|
780
|
+
note:prev.note||'', async:{jobId:job.jobId,state,attempts:settled.attempts,enqueuedAt:job.enqueuedAt,startedAt:job.startedAt,settledAt:settled.settledAt,exitCode:settled.exitCode},
|
|
781
|
+
run:settled.run, updatedAt:now() }))
|
|
782
|
+
} else {
|
|
783
|
+
const prev=formalOf(job.key)
|
|
784
|
+
if(job.kind==='proof'){
|
|
785
|
+
await putFormal(job.key,Object.assign({},prev,{ status:prev.status==='passed'?'attempted':(prev.status==='blocked'?'blocked':'attempted'),
|
|
786
|
+
file:job.rel, proof:prev.proof||'', decision:prev.decision||'used', note:prev.note||'',
|
|
787
|
+
async:{jobId:job.jobId,state,attempts:settled.attempts,enqueuedAt:job.enqueuedAt,startedAt:job.startedAt,settledAt:settled.settledAt,exitCode:settled.exitCode},
|
|
788
|
+
run:settled.run, updatedAt:now() }))
|
|
789
|
+
} else if(job.kind==='run'&&job.target){
|
|
790
|
+
// A plain run only touches the formal records when it was aimed at an OBJECT (exactly what
|
|
791
|
+
// the synchronous path does: no target, no record). `passed`/`blocked` are preserved.
|
|
792
|
+
await formalSetRun(job.target,Object.assign({},run,{ok,file:job.rel,timedOut,interrupted:false}))
|
|
793
|
+
const cur=formalOf(job.target)
|
|
794
|
+
await putFormal(job.target,Object.assign({},cur,{status:cur.status==='passed'?'passed':(cur.status==='blocked'?'blocked':'attempted'),
|
|
795
|
+
async:{jobId:job.jobId,state,attempts:settled.attempts,enqueuedAt:job.enqueuedAt,startedAt:job.startedAt,settledAt:settled.settledAt,exitCode:settled.exitCode}}))
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
await writeLeanJob(settled)
|
|
799
|
+
try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ }
|
|
800
|
+
pushLeanNotice(settled,Object.assign({},run,{ok,hashMatch}))
|
|
801
|
+
return settled
|
|
802
|
+
}
|
|
803
|
+
/** §2.2 + AMENDMENT §5: the drain. One pass starts up to `leanJobsMaxParallel` workers and
|
|
804
|
+
* returns IMMEDIATELY (a long compile must not block the heartbeat); each worker chains the
|
|
805
|
+
* next pass when it settles. A no-op pass costs one comparison + one array check. */
|
|
806
|
+
async function runLeanQueue(){
|
|
807
|
+
const cap=Math.max(1,Math.floor(Number(params.leanJobsMaxParallel))||LEAN_QUEUE_CONCURRENCY)
|
|
808
|
+
let started=false
|
|
809
|
+
while(leanInflight.length<cap&&leanQueue.length){
|
|
810
|
+
const jobId=leanQueue.shift()
|
|
811
|
+
const job=leanJobs[jobId]
|
|
812
|
+
if(!job) continue
|
|
813
|
+
const running=Object.assign({},job,{state:'running',startedAt:now()})
|
|
814
|
+
leanInflight.push(running); await writeLeanJob(running)
|
|
815
|
+
started=true
|
|
816
|
+
;(async()=>{
|
|
817
|
+
let run
|
|
818
|
+
// Compile the ABSOLUTE path recorded at enqueue time: a `def`/`lemma` lives under the
|
|
819
|
+
// global `VibeMath/Formal/{Lib,Proved}` (beside the project tree), so the project-relative
|
|
820
|
+
// form would resolve to a non-existent file and report V4_NOT_FOUND.
|
|
821
|
+
try { run=await leanRunFile(String(running.abs||running.rel)) }
|
|
822
|
+
catch(e){ run={ok:false,code:'LEAN_RUN_FAILED',message:String((e&&e.message)||e),file:running.rel} }
|
|
823
|
+
leanInflight=leanInflight.filter(j=>j.jobId!==jobId)
|
|
824
|
+
if(leanJobs[jobId]&&leanJobs[jobId].state==='interrupted') return // dispose() already settled it
|
|
825
|
+
await settleLeanJob(leanJobs[jobId]||running,run)
|
|
826
|
+
runLeanQueue().catch(()=>{}) // keep the pipeline moving at the cap
|
|
827
|
+
})()
|
|
828
|
+
}
|
|
829
|
+
return started
|
|
830
|
+
}
|
|
831
|
+
/** §2.5: dispose/abort terminates every in-flight compile and marks it interrupted — never `passed`. */
|
|
832
|
+
async function disposeLean(){
|
|
833
|
+
leanQueue=[]
|
|
834
|
+
const jobs=leanInflight.slice()
|
|
835
|
+
leanInflight=[]
|
|
836
|
+
if(!jobs.length) return false
|
|
837
|
+
for(const job of jobs){
|
|
838
|
+
try { if(job.handle&&typeof job.handle.terminate==='function') job.handle.terminate() } catch(e){ /* best effort */ }
|
|
839
|
+
const cur=leanJobs[job.jobId]||job
|
|
840
|
+
const rec=Object.assign({},cur,{state:'interrupted',interrupted:true,settledAt:now(),
|
|
841
|
+
run:Object.assign({},cur.run||{},{ok:false,interrupted:true,hashMatch:false})})
|
|
842
|
+
await writeLeanJob(rec)
|
|
843
|
+
if(rec.kind==='proof'&&rec.key){ const prev=formalOf(rec.key)
|
|
844
|
+
await putFormal(rec.key,Object.assign({},prev,{status:prev.status==='passed'?'attempted':(prev.status||'attempted'),
|
|
845
|
+
async:{jobId:rec.jobId,state:'interrupted',attempts:rec.attempts,enqueuedAt:rec.enqueuedAt,startedAt:rec.startedAt,settledAt:rec.settledAt,exitCode:null}})) }
|
|
846
|
+
pushLeanNotice(rec,{ok:false,exitCode:null,ms:0})
|
|
847
|
+
}
|
|
848
|
+
return true
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* §2.6 crash/restart recovery: scan `<project>/Formal/Jobs/*.json` AFTER the project dirs exist.
|
|
852
|
+
* It NEVER mints a `passed` anything unless the job record itself says exitCode===0 AND the file
|
|
853
|
+
* on disk still hashes to the enqueued content.
|
|
854
|
+
*/
|
|
855
|
+
async function recoverLeanJobs(opts){
|
|
856
|
+
const o=opts||{}
|
|
857
|
+
const applySettled=o.applySettled!==false
|
|
858
|
+
let files=[]
|
|
859
|
+
try {
|
|
860
|
+
const t=await fs.resolve(leanJobsRel(),{cwd:frameworkRoot()})
|
|
861
|
+
if(await fs.stat(t)===undefined) return {scanned:0}
|
|
862
|
+
files=await fs.listDir(t)
|
|
863
|
+
} catch(e){ return {scanned:0} }
|
|
864
|
+
let queued=0,redriven=0,stale=0,completed=0
|
|
865
|
+
for(const f of files||[]){
|
|
866
|
+
if(!f||f.type!=='file'||!/\.json$/.test(String(f.name))) continue
|
|
867
|
+
const job=await readJson(leanJobsRel()+'/'+f.name)
|
|
868
|
+
if(!job||!job.jobId||!job.rel) continue
|
|
869
|
+
leanJobs[job.jobId]=job
|
|
870
|
+
const sha=String(job.sha256||'')
|
|
871
|
+
const text=await readTextAbs(job.abs||vibeRelAbs(job.rel))
|
|
872
|
+
const hashMatch=text!==undefined&&sha256Text(text)===sha
|
|
873
|
+
if(job.state==='queued'){ if(leanQueue.indexOf(job.jobId)<0) leanQueue.push(job.jobId); queued++; continue }
|
|
874
|
+
if(job.state==='running'){
|
|
875
|
+
if(hashMatch){
|
|
876
|
+
await writeLeanJob(Object.assign({},job,{state:'queued',interrupted:true,attempts:(job.attempts||1)+1,run:Object.assign({},job.run||{},{ok:false,interrupted:true,hashMatch:true})}))
|
|
877
|
+
if(leanQueue.indexOf(job.jobId)<0) leanQueue.push(job.jobId)
|
|
878
|
+
if(job.kind==='proof'&&job.key){ const prev=formalOf(job.key)
|
|
879
|
+
await putFormal(job.key,Object.assign({},prev,{status:prev.status==='passed'?'attempted':(prev.status||'attempted'),
|
|
880
|
+
async:{jobId:job.jobId,state:'queued',attempts:(job.attempts||1)+1,enqueuedAt:job.enqueuedAt,startedAt:job.startedAt,settledAt:0,exitCode:null}})) }
|
|
881
|
+
leanNotices.push('【形式化结果】'+job.jobId+':崩溃/重启中断,已按新尝试次数重新入队(旧进程结果不可知,对象保持 attempted)')
|
|
882
|
+
redriven++
|
|
883
|
+
} else {
|
|
884
|
+
await writeLeanJob(Object.assign({},job,{state:'interrupted',interrupted:true,settledAt:now(),run:Object.assign({},job.run||{},{ok:false,interrupted:true,hashMatch:false})}))
|
|
885
|
+
if(job.kind==='proof'&&job.key){ const prev=formalOf(job.key)
|
|
886
|
+
await putFormal(job.key,Object.assign({},prev,{status:'attempted',
|
|
887
|
+
async:{jobId:job.jobId,state:'interrupted',attempts:job.attempts||1,enqueuedAt:job.enqueuedAt,startedAt:job.startedAt,settledAt:now(),exitCode:null}})) }
|
|
888
|
+
leanNotices.push('【形式化结果】'+job.jobId+':文件已变,请手动重跑(不自动重驱,避免把旧代码的结果贴到新代码上)')
|
|
889
|
+
stale++
|
|
890
|
+
}
|
|
891
|
+
continue
|
|
892
|
+
}
|
|
893
|
+
if(job.state==='settled'&&job.kind==='proof'&&job.key&&applySettled){
|
|
894
|
+
const rec=formalOf(job.key)
|
|
895
|
+
const applied=rec.async&&rec.async.jobId===job.jobId&&rec.async.state==='settled'
|
|
896
|
+
if(!applied&&job.exitCode===0&&hashMatch&&job.verifiedRel){
|
|
897
|
+
await writeText(job.verifiedRel,job.content)
|
|
898
|
+
await putFormal(job.key,Object.assign({},rec,{status:'passed',file:job.rel,proof:job.verifiedRel,decision:rec.decision||'used',
|
|
899
|
+
async:{jobId:job.jobId,state:'settled',attempts:job.attempts||1,enqueuedAt:job.enqueuedAt,startedAt:job.startedAt,settledAt:job.settledAt,exitCode:0},
|
|
900
|
+
run:job.run||rec.run,updatedAt:now()}))
|
|
901
|
+
leanNotices.push('【形式化结果】'+job.jobId+':按作业记录补齐(通过,已归档 '+job.verifiedRel+')')
|
|
902
|
+
completed++
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if(queued+redriven+stale+completed){ await writeLeanLibIndexesSafe() ; if(leanQueue.length) armHeartbeat() }
|
|
907
|
+
return {scanned:(files||[]).length,queued,redriven,stale,completed}
|
|
908
|
+
}
|
|
909
|
+
async function writeLeanLibIndexesSafe(){ try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ } }
|
|
910
|
+
/**
|
|
911
|
+
* §1.2 `lean_read`: read ONE archived library file VERBATIM (for import-or-copy reuse).
|
|
912
|
+
* Read-only; the path is confined to `<VibeMath>/Formal/{Lib,Proved}` — the sanitised `name`
|
|
913
|
+
* cannot contain a separator, and the resolved path is re-checked against the root so a crafted
|
|
914
|
+
* name can never escape (a `.lean` suffix is added, never accepted as a path).
|
|
915
|
+
*/
|
|
916
|
+
async function leanRead(o){
|
|
917
|
+
const args=o||{}
|
|
918
|
+
const raw=String(args.name==null?'':args.name).trim()
|
|
919
|
+
const kind=String(args.kind||'auto')
|
|
920
|
+
if(!raw) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'name is required'}
|
|
921
|
+
if(raw.indexOf('/')>=0||raw.indexOf('\\')>=0||raw.indexOf('..')>=0) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'name must be a bare library name (no path separators)'}
|
|
922
|
+
if(['auto','lib','proved'].indexOf(kind)<0) return {ok:false,code:'V4_INVALID_ARGUMENT',message:"kind must be 'auto' | 'lib' | 'proved'"}
|
|
923
|
+
const name=idSafe(raw).replace(/\.lean$/,'')
|
|
924
|
+
if(!name||name==='id') return {ok:false,code:'V4_INVALID_ARGUMENT',message:'invalid name'}
|
|
925
|
+
const cands=[]
|
|
926
|
+
if(kind==='auto'||kind==='lib') cands.push({kind:'lib',rel:'Formal/Lib/'+name+'.lean',abs:formalLibRoot()+'/'+name+'.lean'})
|
|
927
|
+
if(kind==='auto'||kind==='proved') cands.push({kind:'proved',rel:'Formal/Proved/'+name+'.lean',abs:formalProvedRoot()+'/'+name+'.lean'})
|
|
928
|
+
const checked=[]
|
|
929
|
+
for(const c of cands){
|
|
930
|
+
const norm=normalizeAbsPath(c.abs)
|
|
931
|
+
const root=normalizeAbsPath(vibeRoot())
|
|
932
|
+
if(norm.indexOf(root+'/Formal/')!==0) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'refused: resolves outside the VibeMath/Formal library'}
|
|
933
|
+
checked.push(c)
|
|
934
|
+
}
|
|
935
|
+
const found=await (async()=>{
|
|
936
|
+
for(const c of checked){ const t=(await readTextAbs(c.abs)); if(t!==undefined) return {c,t} }
|
|
937
|
+
return null
|
|
938
|
+
})()
|
|
939
|
+
if(!found) return {ok:false,code:'V4_NOT_FOUND',message:'no archived library file named '+name+(kind==='auto'?' (checked Formal/Lib and Formal/Proved)':' in Formal/'+(kind==='lib'?'Lib':'Proved')),name,kind}
|
|
940
|
+
const full=String(found.t)
|
|
941
|
+
const bytes=(typeof TextEncoder==='function')?new TextEncoder().encode(full).length:full.length
|
|
942
|
+
const truncated=bytes>LEAN_READ_MAX_BYTES
|
|
943
|
+
const text=truncated?full.slice(0,LEAN_READ_MAX_BYTES):full
|
|
944
|
+
return {ok:true,name,file:found.c.rel,kind:found.c.kind,sha256:sha256Text(full),bytes,text,truncated,
|
|
945
|
+
hint:'复用:新文件里写 '+(found.c.kind==='lib'?'import Formal.Lib.':'import Formal.Proved.')+name+'(模块根=VibeMath 根,框架已把它加入编译搜索路径),或按上面的原文逐字复制。'}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* AMENDMENT §3: the read-only result/wait surface. Without `jobId` it lists THIS session's jobs;
|
|
950
|
+
* with it, returns the job's state / exitCode / receipt path (`Formal/Jobs/<id>.json`) / archive
|
|
951
|
+
* path. `waitMs>0` waits up to that many ms and then reports the CURRENT state (never blocks
|
|
952
|
+
* forever, never throws).
|
|
953
|
+
*/
|
|
954
|
+
const LEAN_JOB_TERMINAL=['settled','failed','timeout','interrupted']
|
|
955
|
+
async function leanJobTool(o){
|
|
956
|
+
const args=o||{}
|
|
957
|
+
const jobId=String(args.jobId||'').trim()
|
|
958
|
+
const waitMs=Math.max(0,Math.min(600000,Math.floor(Number(args.waitMs))||0))
|
|
959
|
+
if(!jobId){
|
|
960
|
+
const ids=Object.keys(leanJobs).sort()
|
|
961
|
+
return {ok:true,count:ids.length,
|
|
962
|
+
jobs:ids.map(k=>Object.assign(leanJobView(leanJobs[k]),{receipt:leanJobRel(k),archive:leanJobs[k].verifiedRel||'',timedOut:!!leanJobs[k].timedOut,interrupted:!!leanJobs[k].interrupted})),
|
|
963
|
+
queued:leanQueue.slice(),running:leanInflight.map(j=>j.jobId),
|
|
964
|
+
hint:'有 jobId 时用 vibe_v4_lean_job {jobId, waitMs} 取结果/等待(waitMs 超时只返回当前 state)。'}
|
|
965
|
+
}
|
|
966
|
+
let job=leanJobs[jobId]||await readLeanJob(jobId)
|
|
967
|
+
if(job&&waitMs>0){
|
|
968
|
+
const t0=now()
|
|
969
|
+
while(LEAN_JOB_TERMINAL.indexOf(job.state)<0&&now()-t0<waitMs){ await leanSleep(25); job=leanJobs[jobId]||job }
|
|
970
|
+
}
|
|
971
|
+
if(!job) return {ok:false,code:'V4_NOT_FOUND',message:'no such job: '+jobId}
|
|
972
|
+
return {ok:true,jobId,job:leanJobView(job),state:job.state,exitCode:(job.exitCode===undefined?null:job.exitCode),
|
|
973
|
+
rel:job.rel,receipt:leanJobRel(jobId),archive:job.verifiedRel||'',target:job.target||'',key:job.key||'',
|
|
974
|
+
timedOut:!!job.timedOut,interrupted:!!job.interrupted,attempts:job.attempts||1,settledAt:job.settledAt||0,
|
|
975
|
+
build:job.build||null,buildHash:job.buildHash||'',sha256:job.sha256||'',
|
|
976
|
+
run:job.run||null,
|
|
977
|
+
hint:LEAN_JOB_TERMINAL.indexOf(job.state)>=0?'(已终态)':'(仍在队列/编译中;可用 waitMs 再等,或等下一轮【形式化结果】公告)'}
|
|
978
|
+
}
|
|
979
|
+
|
|
485
980
|
function formalModeWord(){ return formalMode()==='require'?'强制':'鼓励' }
|
|
486
981
|
/**
|
|
487
982
|
* The verification-prompt block. Every branch is computed from the CURRENT mode and the
|
|
@@ -517,8 +1012,11 @@ export function apply(ctx) {
|
|
|
517
1012
|
L.push(' ▸ 因此请把 verdict 用在"这个阻塞判断是否成立 / 是否仍有别的形式化路线"上,并给出理由。')
|
|
518
1013
|
} else {
|
|
519
1014
|
L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
|
|
520
|
-
|
|
521
|
-
L.push(' ·
|
|
1015
|
+
// lean-incremental-async spec §5 B (verbatim, after the implementation-difficulty line).
|
|
1016
|
+
L.push(' · 形式化只写你有把握的版本;没把握就记 blocked 并写清难点——不要用形式化掩盖不确定。')
|
|
1017
|
+
L.push(' · 该对象若已有后台编译在队列中,**不得**在它落地前声称已通过或走忠实性审查;等 vibe_v4_lean_lib 显示 passed 再审。')
|
|
1018
|
+
L.push(' · 工具:vibe_v4_lean_run(执行/入队)· vibe_v4_lean_archive(归档)· vibe_v4_lean_lib(查已有可复用库与 jobs)· vibe_v4_lean_read(读归档原文逐字复用)')
|
|
1019
|
+
L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 '+formalLibRoot().replace(/\\/g,'/')+'/,已证引理放 '+formalProvedRoot().replace(/\\/g,'/')+'/;写之前先 vibe_v4_lean_lib 查重。复用已归档内容:`import Formal.Lib.<name>` / `import Formal.Proved.<name>`(模块根 = VibeMath 根,框架已加入编译搜索路径)。项目工作文件 Formal/<对象 id>.lean 只是归档用,**不是**模块路径,不要 import 它。')
|
|
522
1020
|
L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
|
|
523
1021
|
if(formalMode()==='require'){
|
|
524
1022
|
L.push(" · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(vibe_v4_lean_archive kind='blocked' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,会被记为未定论(原因 formal-required)并进入「形式化待办」。")
|
|
@@ -537,17 +1035,90 @@ export function apply(ctx) {
|
|
|
537
1035
|
}
|
|
538
1036
|
return L.join('\n')
|
|
539
1037
|
}
|
|
540
|
-
/**
|
|
1038
|
+
/** `leanInitiative` (v5-aligned): how EAGER the group is about formalizing while working. It is a
|
|
1039
|
+
* SEPARATE axis from `formalVerify` — see `leanDailyOn` below. */
|
|
1040
|
+
const leanInitiative=()=>(['off','normal','eager'].indexOf(String(params.leanInitiative))!==-1?String(params.leanInitiative):'normal')
|
|
1041
|
+
/** The DAILY line is injected when the group should formalize while working: any non-'off'
|
|
1042
|
+
* initiative with `formalVerify` on, OR explicitly `eager` — which asks for it even when
|
|
1043
|
+
* `formalVerify:'off'`, because the two knobs are independent (v5 uses the exact same rule).
|
|
1044
|
+
* 'off' never injects the daily line; the VERIFICATION prompt still follows `formalVerify`. */
|
|
1045
|
+
const leanDailyOn=()=>leanInitiative()!=='off'&&(formalOn()||leanInitiative()==='eager')
|
|
1046
|
+
/** The ordinary-work-round line (spec §6.2): formalize reusable objects as you go. Carries the
|
|
1047
|
+
* three selection criteria, the blocked-instead-of-guessing rule, the look-up-first reuse path
|
|
1048
|
+
* and the background-compile rule (lean-incremental-async spec §5 A, verbatim). */
|
|
541
1049
|
function formalWorkLine(){
|
|
542
|
-
if(!
|
|
543
|
-
|
|
1050
|
+
if(!leanDailyOn()) return ''
|
|
1051
|
+
// v5-aligned header: with `formalVerify:'off'` the mode word would be misleading, so the eager
|
|
1052
|
+
// 档 names itself instead.
|
|
1053
|
+
return '【顺手形式化('+(formalOn()?formalModeWord():'主动(leanInitiative=eager)')+')】把你工作中常用或可能复用的对象、假设、新定义,'
|
|
544
1054
|
+"用 Lean 形式化定义并归档到全局可复用库(vibe_v4_lean_archive kind='def'),已成立的引理归到 Formal/Proved/"
|
|
545
1055
|
+"(kind='lemma');写之前先 vibe_v4_lean_lib 查重,避免重复定义。"
|
|
1056
|
+
+'\n · 判断标准:① 有价值或可能复用;② 较为关键或必要;③ 你对该陈述有把握(置信度高)。**没把握的先别入库**——'
|
|
1057
|
+
+'\n 进了 Formal/Proved 的东西会被当成已核对引理;没把握就记 blocked 并写清难点,别用形式化掩盖不确定。'
|
|
1058
|
+
+'\n · 复用优先:写新定义/证明前**先 vibe_v4_lean_lib 查已有库**;复用已归档内容用'
|
|
1059
|
+
+'\n `import Formal.Lib.<name>` / `import Formal.Proved.<name>`(模块根 = VibeMath 根,框架已把它加进编译搜索路径),'
|
|
1060
|
+
+'\n 或 `vibe_v4_lean_read {name}` 取原文逐字复制。**查不到再新写**;同内容重复归档会自动去重。'
|
|
1061
|
+
+'\n · 编译默认走后台队列(leanAsync=true):入队后你可以继续工作;用 vibe_v4_lean_lib 的 jobs 字段或下一轮提示里的'
|
|
1062
|
+
+'\n 【形式化结果】行看结果。**在作业落地为"通过"之前,不得把该对象当成已通过。**'
|
|
546
1063
|
+(formalMode()==='require'
|
|
547
1064
|
? '本模式下,任何要定论为真/假的对象都必须先有 Lean 通过或显式阻塞记录。'
|
|
548
1065
|
: '这会让后续的验证与证明省掉大量重复工作。')
|
|
1066
|
+
// AMENDMENT §1: only `leanInitiative='eager'` asks for MORE proactivity than the normal line.
|
|
1067
|
+
// `formalVerify` is deliberately NOT consulted here: it expresses the verification-time
|
|
1068
|
+
// requirement, not how eagerly the group should formalize while working.
|
|
1069
|
+
+(leanInitiative()==='eager'
|
|
1070
|
+
? '\n · 本档(leanInitiative=eager):工作中出现**有价值且你有把握**的小引理 / 小命题 / 小定义时,**主动**顺手形式化并归档,不必等到验证轮;把握不足就照上面的判据记 blocked。'
|
|
1071
|
+
: '')
|
|
549
1072
|
+'归档前先跑通(vibe_v4_lean_run 或 run=true);跑不通的定义不要进可复用库。'
|
|
550
1073
|
}
|
|
1074
|
+
// ================= math_computation availability line (P1) =================
|
|
1075
|
+
// The line is DYNAMIC (it comes from the module's probe cache) while the resident prompts are
|
|
1076
|
+
// built by BOTH sync and async builders, so the two language variants are cached here and
|
|
1077
|
+
// refreshed from the async paths (start / resume / set / heartbeat) — a sync builder re-uses the
|
|
1078
|
+
// last known line and kicks off a refresh when the cache is still cold. `mathComputation:'off'`
|
|
1079
|
+
// is a true no-op: no probe, no line, nothing injected.
|
|
1080
|
+
let mathAvailZh='', mathAvailEn='', mathAvailBusy=false
|
|
1081
|
+
async function refreshMathAvailability(){
|
|
1082
|
+
if(params.mathComputation==='off'){ mathAvailZh=''; mathAvailEn=''; return }
|
|
1083
|
+
if(mathAvailBusy) return
|
|
1084
|
+
mathAvailBusy=true
|
|
1085
|
+
try {
|
|
1086
|
+
// Use THIS session's cached module instance (keyed by `mathProbeHost`): the probe cache lives
|
|
1087
|
+
// in the instance, so repeated refreshes cost nothing, and two sessions never share it.
|
|
1088
|
+
const inst=mathInstanceFor(mathProbeHost)
|
|
1089
|
+
const probe=inst?await inst.probe():await probeMathEngines(mathProbeHost)
|
|
1090
|
+
// The third argument is the MODE: the module drops the shell-fallback sentence in 'typed'
|
|
1091
|
+
// (prompts.md §4 consistency check 5) — the preset must not re-add it.
|
|
1092
|
+
mathAvailZh=mathAvailabilityLine(probe,'zh',params.mathMode)
|
|
1093
|
+
mathAvailEn=mathAvailabilityLine(probe,'en',params.mathMode)
|
|
1094
|
+
// The availability line is REQUIRED to carry the rule lines (prompts.md §2/§4). Referencing
|
|
1095
|
+
// both rule constants here is the drift check: if the module ever stops appending them, this
|
|
1096
|
+
// preset notices instead of shipping an availability line without the rules.
|
|
1097
|
+
if(mathAvailZh.indexOf(MATH_RULE_LINES[0])===-1||mathAvailEn.indexOf(MATH_RULE_LINES_EN[0])===-1){
|
|
1098
|
+
logActivity('math','警告:mathAvailabilityLine 未附带规则段(MATH_RULE_LINES/_EN),提示词可能缺少用法规则')
|
|
1099
|
+
}
|
|
1100
|
+
// P2a: the archive→edit→re-run workflow rule is part of the same contract (an old receipt is
|
|
1101
|
+
// NOT evidence for edited code). It is appended to the availability line by the module and to
|
|
1102
|
+
// both persona blocks by this preset — this is the drift check for the former.
|
|
1103
|
+
if(mathAvailZh.indexOf(MATH_ARCHIVE_WORKFLOW_LINE.trim())===-1||mathAvailEn.indexOf(MATH_ARCHIVE_WORKFLOW_LINE_EN.trim())===-1){
|
|
1104
|
+
logActivity('math','警告:mathAvailabilityLine 未附带归档工作流规则(MATH_ARCHIVE_WORKFLOW_LINE/_EN)')
|
|
1105
|
+
// round-6 A: substitution honesty is part of the same contract (an alternative that weakens
|
|
1106
|
+
// exactness must be declared). It is appended to the availability line by the module.
|
|
1107
|
+
if(mathAvailZh.indexOf(MATH_SUBSTITUTION_RULE_LINE.trim())===-1||mathAvailEn.indexOf(MATH_SUBSTITUTION_RULE_LINE_EN.trim())===-1){
|
|
1108
|
+
logActivity('math','警告:mathAvailabilityLine 未附带替代诚实性规则(MATH_SUBSTITUTION_RULE_LINE/_EN)')
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
} catch(e){ /* a probe failure must never break a prompt; keep the last known line */ }
|
|
1112
|
+
finally { mathAvailBusy=false }
|
|
1113
|
+
}
|
|
1114
|
+
/** The availability + rules block: the frozen persona tool line (tools/params shape) followed by
|
|
1115
|
+
* the dynamic zh + en availability lines. v4's prompts are bilingual (English frame, Chinese
|
|
1116
|
+
* rules), so both variants are injected; `mathComputation:'off'` yields '' (a true no-op). */
|
|
1117
|
+
function mathAvailabilityBlock(){
|
|
1118
|
+
if(params.mathComputation==='off') return ''
|
|
1119
|
+
if(!mathAvailZh&&!mathAvailEn){ refreshMathAvailability().catch(()=>{}); return '' }
|
|
1120
|
+
return [MATH_PERSONA_TOOL_LINE, mathAvailZh, mathAvailEn].filter(Boolean).join('\n')
|
|
1121
|
+
}
|
|
551
1122
|
/** The `formal` object every non-off prompt documents in its JSON reply contract. */
|
|
552
1123
|
function formalReplyField(target){
|
|
553
1124
|
return '{"formal":{"target":"'+String(target||'p-x')+'","decision":"used|blocked|defect","file":"Formal/'+String(target||'p-x')+'.lean","note":"难度判断/阻塞原因/具体偏差"}}'
|
|
@@ -602,21 +1173,24 @@ export function apply(ctx) {
|
|
|
602
1173
|
const txt=(await readTextAbs(dirAbs+'/'+e.name))||''
|
|
603
1174
|
const name=String(e.name).replace(/\.lean$/,'')
|
|
604
1175
|
const first=(txt.split('\n').filter(l=>l.trim()&&!/^\s*(\/\/|--|import)/.test(l))[0]||'').trim().slice(0,110)
|
|
605
|
-
|
|
1176
|
+
// §4.4: the dependency column v3 already has — one entry per `import` line, so a reader
|
|
1177
|
+
// can see what a reusable file pulls in (and therefore what must travel with it).
|
|
1178
|
+
const deps=(txt.match(/^\s*import\s+[^\s]+/gm)||[]).map(l=>l.replace(/^\s*import\s+/,'').trim()).filter(Boolean)
|
|
1179
|
+
rows.push('| '+name+' | '+rel+' | '+kindLabel+' | '+(deps.length?deps.join(', ').replace(/\|/g,'/'):'—')+' | '+first.replace(/\|/g,'/')+' |')
|
|
606
1180
|
}
|
|
607
1181
|
} catch(e){ /* listing is best-effort */ }
|
|
608
1182
|
return rows
|
|
609
1183
|
}
|
|
610
1184
|
const libRows=await scan(formalLibRoot(),'Formal/Lib','def')
|
|
611
1185
|
await writeTextAbs(formalLibRoot()+'/Index.md',['# 可复用 Lean 定义库(跨项目)|'+currentProject,'',
|
|
612
|
-
'>
|
|
613
|
-
'| 名称 | 文件 | 类别 | 摘要 |','
|
|
614
|
-
.concat(libRows.length?libRows:['| (暂无) | | | |']).join('\n')+'\n')
|
|
1186
|
+
'> 写新定义之前先查这里:能复用就不要重新定义。复用方式:`import Formal.Lib.<名称>`(模块根 = VibeMath 根,框架已加入编译搜索路径),或用 vibe_v4_lean_read 取原文逐字复制。','',
|
|
1187
|
+
'| 名称 | 文件 | 类别 | 依赖 | 摘要 |','|---|---|---|---|---|']
|
|
1188
|
+
.concat(libRows.length?libRows:['| (暂无) | | | | |']).join('\n')+'\n')
|
|
615
1189
|
const provedRows=await scan(formalProvedRoot(),'Formal/Proved','lemma')
|
|
616
1190
|
await writeTextAbs(formalProvedRoot()+'/Index.md',['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|'+currentProject,'',
|
|
617
|
-
'> 这些文件是通过内核检查的引理,可直接 import
|
|
618
|
-
'| 名称 | 文件 | 类别 | 陈述 |','
|
|
619
|
-
.concat(provedRows.length?provedRows:['| (暂无) | | | |']).join('\n')+'\n')
|
|
1191
|
+
'> 这些文件是通过内核检查的引理,可直接 `import Formal.Proved.<名称>` 复用(模块根 = VibeMath 根)。','',
|
|
1192
|
+
'| 名称 | 文件 | 类别 | 依赖 | 陈述 |','|---|---|---|---|---|']
|
|
1193
|
+
.concat(provedRows.length?provedRows:['| (暂无) | | | | |']).join('\n')+'\n')
|
|
620
1194
|
await writeFormalIndex()
|
|
621
1195
|
await writeFormalTodo()
|
|
622
1196
|
return {lib:libRows.length,proved:provedRows.length,objects:Object.keys(formalRecords()).length}
|
|
@@ -630,7 +1204,24 @@ export function apply(ctx) {
|
|
|
630
1204
|
*/
|
|
631
1205
|
async function leanRunTool(memberId,o){
|
|
632
1206
|
const args=o||{}
|
|
633
|
-
const
|
|
1207
|
+
const rel=String(args.file||'')
|
|
1208
|
+
// §2.3 (leanAsync=true, the default): ENQUEUE and return immediately — the member keeps
|
|
1209
|
+
// working; the result lands in Formal/ + the indexes and is announced in the next prompt.
|
|
1210
|
+
if(LEAN_ASYNC_ON()){
|
|
1211
|
+
const abs=leanAbsPath(rel)
|
|
1212
|
+
if(abs===null||!/\.lean$/.test(abs)) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'Lean file must live under '+vibeRoot().replace(/\\/g,'/')+'/ and end in .lean (got '+rel+')',file:rel}
|
|
1213
|
+
const content=await readTextAbs(abs)
|
|
1214
|
+
if(content===undefined) return {ok:false,code:'V4_NOT_FOUND',message:'no such file: '+rel,file:rel}
|
|
1215
|
+
const target=String(args.target||'').trim()
|
|
1216
|
+
const key=formalKey(target||rel.replace(/\.lean$/,''))
|
|
1217
|
+
const q=await enqueueLeanJob({kind:'run',key,target,rel,abs,content,via:memberId||'host'})
|
|
1218
|
+
if(q.duplicate) return {ok:true,async:leanJobView(q.job),jobId:q.job.jobId,file:rel,mode:formalMode(),
|
|
1219
|
+
message:'同一内容已在队列/已归档('+q.job.state+'),未重复入队;用 vibe_v4_lean_lib 的 jobs 查看,或用 lean_read 取原文。'}
|
|
1220
|
+
logActivity('formal',(memberId||'host')+' vibe_v4_lean_run '+rel+' → 已入队 '+q.job.jobId)
|
|
1221
|
+
return {ok:true,async:{jobId:q.job.jobId,state:'queued'},jobId:q.job.jobId,file:rel,mode:formalMode(),
|
|
1222
|
+
message:'已入队后台编译;你可以继续工作。结果会写入 Formal/ 与索引,并在下一轮提示里公告;也可用 vibe_v4_lean_lib 的 jobs 字段随时查看。'}
|
|
1223
|
+
}
|
|
1224
|
+
const run=await leanRunFile(rel,args.timeout_ms)
|
|
634
1225
|
try {
|
|
635
1226
|
if(run.ok||run.file){
|
|
636
1227
|
if(String(args.target||'').trim()) formalSetRun(String(args.target),run)
|
|
@@ -642,6 +1233,9 @@ export function apply(ctx) {
|
|
|
642
1233
|
// bare `lean_run` abbreviation (docs §6 hard requirement 1).
|
|
643
1234
|
logActivity('formal',(memberId||'host')+' vibe_v4_lean_run '+(run.file||String(args.file||''))+' → '+(run.ok?'通过':(run.code||'未通过')))
|
|
644
1235
|
return Object.assign({ok:!!run.ok},run,{
|
|
1236
|
+
// `leanAsync=false`: the historical shape, plus an explicit `async:null` so a caller can
|
|
1237
|
+
// tell "synchronous result" from "queued job" without guessing.
|
|
1238
|
+
async:null, jobId:null,
|
|
645
1239
|
mode:formalMode(),
|
|
646
1240
|
hint: run.ok
|
|
647
1241
|
? "通过。若是某个对象的证明,请用 vibe_v4_lean_archive kind='proof' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind='def'/'lemma' 归档到全局库——归档前先跑通(run=true 或先 vibe_v4_lean_run):跑不通的定义不要进可复用库。"
|
|
@@ -676,20 +1270,57 @@ export function apply(ctx) {
|
|
|
676
1270
|
if(!name||name==='id') return {ok:false,code:'V4_INVALID_ARGUMENT',message:'name is required for a reusable definition/lemma'}
|
|
677
1271
|
const got=await bodyFrom(); if(got.error) return got.error
|
|
678
1272
|
const rel='Formal/'+(kind==='def'?'Lib':'Proved')+'/'+name+'.lean'
|
|
679
|
-
|
|
1273
|
+
const abs=vibeRelAbs(rel)
|
|
1274
|
+
const sha=sha256Text(got.body)
|
|
1275
|
+
// AMENDMENT §4: de-duplication keys on the WHOLE build context (content + engine + argv +
|
|
1276
|
+
// search paths), so identical content compiled by a different toolchain is a NEW job.
|
|
1277
|
+
const jidFor=await leanJobIdFor(name,got.body)
|
|
1278
|
+
const jobId=jidFor.jobId
|
|
1279
|
+
// §4.3: the SAME content that already compiled green is a no-op — no rewrite, no recompile.
|
|
1280
|
+
const prior=leanJobs[jobId]
|
|
1281
|
+
if(prior&&prior.state==='settled'&&prior.run&&prior.run.ok&&sha256Text((await readTextAbs(abs))||'')===sha){
|
|
1282
|
+
logActivity('formal',String(memberId||'host')+' 归档'+kind+' '+name+' → 去重(内容与已归档版本一致)')
|
|
1283
|
+
return {ok:true,kind,name,file:rel,deduped:true,sha256:sha,jobId,async:leanJobView(prior),run:prior.run,
|
|
1284
|
+
note:'内容与已归档版本一致,跳过重写与重编译'}
|
|
1285
|
+
}
|
|
1286
|
+
if(!await writeTextAbs(abs,got.body)) return {ok:false,code:'V4_WRITE_FAILED',message:'could not write '+rel}
|
|
1287
|
+
if(LEAN_ASYNC_ON()){
|
|
1288
|
+
const q=await enqueueLeanJob({kind,key:name,name,rel,abs,content:got.body,sha256:sha,ctx:jidFor.ctx,via:memberId||'host'})
|
|
1289
|
+
logActivity('formal',String(memberId||'host')+' 归档'+(kind==='def'?'可复用定义':'已证引理')+' '+name+' → '+rel+'(已入队 '+(q.job&&q.job.jobId||jobId)+')')
|
|
1290
|
+
return {ok:true,kind,name,file:rel,sha256:sha,jobId:(q.job&&q.job.jobId)||jobId,
|
|
1291
|
+
async:{jobId:(q.job&&q.job.jobId)||jobId,state:q.duplicate?'queued':'queued'},run:undefined,
|
|
1292
|
+
note:'已并入全局可复用库并**入队后台编译**:通过前不要在别处把它当作已核对引理;结果见 vibe_v4_lean_lib.jobs 与下一轮【形式化结果】行'}
|
|
1293
|
+
}
|
|
680
1294
|
// The global library sits BESIDE the project tree, so it must be executed through its
|
|
681
1295
|
// ABSOLUTE path — the project-relative form would resolve inside frameworkRoot.
|
|
682
|
-
const run=args.run===false?null:await leanRunFile(
|
|
1296
|
+
const run=args.run===false?null:await leanRunFile(abs)
|
|
683
1297
|
try { await rebuildLeanLibIndexes() } catch(e){ /* index rebuild is best-effort */ }
|
|
684
1298
|
logActivity('formal',String(memberId||'host')+' 归档'+(kind==='def'?'可复用定义':'已证引理')+' '+name+' → '+rel+(run?('(运行 '+(run.ok?'通过':'未通过')+')'):''))
|
|
685
|
-
return {ok:true,kind,name,file:rel,run:run||undefined,note:'已并入全局可复用库,后续项目可直接 import 复用'}
|
|
1299
|
+
return {ok:true,kind,name,file:rel,deduped:false,sha256:sha,async:null,run:run||undefined,note:'已并入全局可复用库,后续项目可直接 import 复用'}
|
|
686
1300
|
}
|
|
687
1301
|
if(kind==='proof'){
|
|
688
1302
|
const key=formalKey(String(args.target||''))
|
|
689
1303
|
if(!key) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'target is required for kind=proof'}
|
|
690
1304
|
const got=await bodyFrom(); if(got.error) return got.error
|
|
691
1305
|
const workRel='Formal/'+key+'.lean'
|
|
1306
|
+
const sha=sha256Text(got.body)
|
|
1307
|
+
const jidFor=await leanJobIdFor(key,got.body)
|
|
1308
|
+
// §4.3 (proof variant): identical content + the object is ALREADY `passed` and the archived
|
|
1309
|
+
// proof still hashes the same ⇒ nothing to do.
|
|
1310
|
+
const prevRec=formalOf(key)
|
|
1311
|
+
if(prevRec.status==='passed'&&sha256Text((await readText('Verified/Lean/'+key+'.lean'))||'')===sha){
|
|
1312
|
+
logActivity('formal',String(memberId||'host')+' 归档证明 '+key+' → 去重(对象已 passed 且归档内容一致)')
|
|
1313
|
+
return {ok:true,kind,target:key,file:workRel,proof:prevRec.proof,passed:true,deduped:true,sha256:sha,async:leanJobView(leanJobs[jidFor.jobId])||null,status:prevRec.status}
|
|
1314
|
+
}
|
|
692
1315
|
if(!await writeText(workRel,got.body)) return {ok:false,code:'V4_WRITE_FAILED',message:'could not write '+workRel}
|
|
1316
|
+
if(LEAN_ASYNC_ON()){
|
|
1317
|
+
const q=await enqueueLeanJob({kind:'proof',key,target:key,rel:workRel,abs:leanAbsPath(workRel),content:got.body,sha256:sha,ctx:jidFor.ctx,via:memberId||'host'})
|
|
1318
|
+
const jid=(q.job&&q.job.jobId)||jidFor.jobId
|
|
1319
|
+
logActivity('formal',String(memberId||'host')+' 为 '+key+' 归档形式化证明 '+workRel+'(已入队 '+jid+';通过前不改 passed)')
|
|
1320
|
+
return {ok:true,kind,target:key,file:workRel,sha256:sha,jobId:jid,
|
|
1321
|
+
async:{jobId:jid,state:'queued'},run:undefined,
|
|
1322
|
+
note:'证明已入队后台编译:**只有作业落地为通过**才会写入 Verified/Lean/'+key+'.lean 并把对象置 passed;其余情况保持 attempted,见 vibe_v4_lean_lib.jobs'}
|
|
1323
|
+
}
|
|
693
1324
|
const run=await leanRunFile(workRel)
|
|
694
1325
|
const prev=formalOf(key)
|
|
695
1326
|
const passed=!!run.ok
|
|
@@ -709,7 +1340,7 @@ export function apply(ctx) {
|
|
|
709
1340
|
await putFormal(key,rec)
|
|
710
1341
|
try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ }
|
|
711
1342
|
logActivity('formal',String(memberId||'host')+' 为 '+key+' 归档形式化证明 '+workRel+(passed?'(**通过**,已归档到 '+rec.proof+',验证转为忠实性审查)':'(**未通过**:'+tail(run.stderr||run.message,160)+')'))
|
|
712
|
-
return {ok:true,kind,target:key,file:workRel,proof:rec.proof,passed,run,status:rec.status}
|
|
1343
|
+
return {ok:true,kind,target:key,file:workRel,proof:rec.proof,passed,run,status:rec.status,deduped:false,sha256:sha,async:null}
|
|
713
1344
|
}
|
|
714
1345
|
if(kind==='blocked'){
|
|
715
1346
|
const key=formalKey(String(args.target||''))
|
|
@@ -987,7 +1618,9 @@ export function apply(ctx) {
|
|
|
987
1618
|
// living under right now.
|
|
988
1619
|
return '[核心规则重申] 只有 Verified/(及标记"已验证·真/假")算已确立;验证须全组一致(全真或全假)才作数,否则留库附平均概率;你只写自己的库('+base+'/ 的 Progress/<你>/、Propos/<你>/、Methods/<你>/、Subproblems/<你>/),可只读任何人的库;任务分工由团队讨论决定;退出只输出一个 JSON 对象。'
|
|
989
1620
|
+'\n`facilitator` 是**框架/人类介入的信使名**,不是常驻成员,也不在编制里——**不要向它回信**(`vibe_v4_send_message` 会返回 no such resident);要回话请用本轮回执的 "input" 字段(会转给全组)或 `vibe_v4_send_message {to:"all"}`。'
|
|
990
|
-
+(
|
|
1621
|
+
+(leanDailyOn()?('\n'+formalWorkLine()):'')
|
|
1622
|
+
// math_computation is INDEPENDENT of formalVerify (its own `mathComputation` switch).
|
|
1623
|
+
+(mathAvailabilityBlock()?('\n'+mathAvailabilityBlock()):'')
|
|
991
1624
|
}
|
|
992
1625
|
function brainstormPrompt(r){
|
|
993
1626
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
@@ -1000,12 +1633,23 @@ export function apply(ctx) {
|
|
|
1000
1633
|
+'{"summary":"<your insight / direction / rough plan, one tight paragraph>","solved":false}'
|
|
1001
1634
|
}
|
|
1002
1635
|
async function normalPrompt(r){
|
|
1636
|
+
// Consume-and-clear ONCE: calling the accessor twice would return '' the second time (bug).
|
|
1637
|
+
const leanNotice=pendingLeanNoticeText()
|
|
1638
|
+
// The math availability line is built from the probe cache; awaiting the refresh HERE means a
|
|
1639
|
+
// cold cache still yields a complete line on the first wake (later wakes hit the cache).
|
|
1640
|
+
await refreshMathAvailability()
|
|
1641
|
+
const mathAvail=mathAvailabilityBlock()
|
|
1003
1642
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
1004
1643
|
+'Resident researcher '+r.rId+' — 第 '+r.rounds+' 轮。一切由你和团队讨论决定。动手前先**读别人的库**对齐事实、避免重复;把新进展/结论**直接用 fs 写进你自己的文件**;想对团队说的话放 "input"(会转给其他常驻)。\n'
|
|
1005
1644
|
+'\n团队成员:\n'+banner()+'\n'
|
|
1006
1645
|
+'New items:\n'+ (await inboxText(r.rId)) +'\n'
|
|
1646
|
+
// Settled background compiles are announced ONCE, in the next prompt constructed (spec §2.4):
|
|
1647
|
+
// the same dynamic discipline as the mode — an announcement is never frozen into a brief.
|
|
1648
|
+
+(leanNotice?('\n'+leanNotice+'\n'):'')
|
|
1007
1649
|
// 顺手形式化: computed from the CURRENT mode on every wake (docs §1: the mode is dynamic).
|
|
1008
|
-
+(
|
|
1650
|
+
+(leanDailyOn()?('\n'+formalWorkLine()+'\n'):'')
|
|
1651
|
+
// math_computation availability (dynamic probe cache; independent of formalVerify).
|
|
1652
|
+
+(mathAvail?('\n'+mathAvail+'\n'):'')
|
|
1009
1653
|
+'Reply with ONLY a JSON object:\n'
|
|
1010
1654
|
+'{"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'
|
|
1011
1655
|
+(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked|defect","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因/具体偏差"}'):'')
|
|
@@ -1051,6 +1695,9 @@ export function apply(ctx) {
|
|
|
1051
1695
|
L.push('')
|
|
1052
1696
|
L.push(formalPromptBlock(vs.targetId))
|
|
1053
1697
|
}
|
|
1698
|
+
// A settled background compile is announced once, here too: a voter must not conclude on a
|
|
1699
|
+
// "passed" that only landed in the queue a moment ago (spec §2.4/§5 B).
|
|
1700
|
+
{ const notice=pendingLeanNoticeText(); if(notice){ L.push(''); L.push(notice) } }
|
|
1054
1701
|
L.push('')
|
|
1055
1702
|
L.push('Reply with ONLY a JSON object:')
|
|
1056
1703
|
L.push('{"vote":{"verdict":1,"reason":"<your logic>"}}')
|
|
@@ -1775,7 +2422,10 @@ export function apply(ctx) {
|
|
|
1775
2422
|
function heartbeatPrompt(r){
|
|
1776
2423
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
1777
2424
|
+'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
|
|
1778
|
-
+(
|
|
2425
|
+
+(leanDailyOn()?(formalWorkLine()+'\n'):'')
|
|
2426
|
+
// math_computation availability (cached; `normalPrompt` awaits the refresh, so a heartbeat
|
|
2427
|
+
// that follows a wake is never cold).
|
|
2428
|
+
+(mathAvailabilityBlock()?(mathAvailabilityBlock()+'\n'):'')
|
|
1779
2429
|
+'Reply with ONLY a JSON object:\n'
|
|
1780
2430
|
+'{"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'
|
|
1781
2431
|
+(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked|defect","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因/具体偏差"}'):'')
|
|
@@ -1786,7 +2436,12 @@ export function apply(ctx) {
|
|
|
1786
2436
|
clearHeartbeat()
|
|
1787
2437
|
const ms=posMs(params.activityTimeoutMs,120000)
|
|
1788
2438
|
if(typeof ctx.timeout!=='function') return
|
|
1789
|
-
|
|
2439
|
+
// §2.2: the EXISTING heartbeat is the queue driver (same place, same discipline as the paper
|
|
2440
|
+
// retry: independent of `scheduler.running`). `runLeanQueue()` is one boolean when idle.
|
|
2441
|
+
heartbeatDisposer=ctx.timeout(()=>{ heartbeatDisposer=null; runLeanQueue().catch(()=>{}).then(()=>scheduleNext().catch(()=>{})) }, ms)
|
|
2442
|
+
// Keep the math availability line current (engines can be installed while a run is live). The
|
|
2443
|
+
// module's probe cache makes this a no-op after the first probe.
|
|
2444
|
+
refreshMathAvailability().catch(()=>{})
|
|
1790
2445
|
}
|
|
1791
2446
|
// Real DSH /compact of a resident's OWN session via ctx.compaction (if the host provides it);
|
|
1792
2447
|
// falling back silently to the resident self-summary directive when the service is absent.
|
|
@@ -2823,6 +3478,14 @@ export function apply(ctx) {
|
|
|
2823
3478
|
// A fresh run starts with a clean formal slate: the ids r-1.. and p-* are reused, so
|
|
2824
3479
|
// carrying a previous run's records over would let a stale `passed` open the new gate.
|
|
2825
3480
|
formal={}; formalTodos=[]
|
|
3481
|
+
// §2.6 recovery (fresh-run variant): a dead process may have left `Formal/Jobs/*.json` behind.
|
|
3482
|
+
// A FRESH run must not adopt the previous run's SETTLED outcomes (that is what the clean slate
|
|
3483
|
+
// above exists to prevent), so only the safe half of the table runs: `queued` jobs are
|
|
3484
|
+
// re-driven and `running` jobs are marked/re-queued — never a `passed` from a stale record.
|
|
3485
|
+
await recoverLeanJobs({mode:'start',applySettled:false})
|
|
3486
|
+
// Warm the math_computation availability cache so the FIRST resident prompt already carries
|
|
3487
|
+
// the line (the probe is cached per registered instance, so later refreshes cost nothing).
|
|
3488
|
+
await refreshMathAvailability()
|
|
2826
3489
|
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
|
|
2827
3490
|
paperState=null // a fresh run gets a fresh paper flow (its durable record lives in Paper/<run id>/)
|
|
2828
3491
|
// Re-base the artifact counter on the cards already on disk, so the auto-sync meeting counts
|
|
@@ -2855,6 +3518,10 @@ export function apply(ctx) {
|
|
|
2855
3518
|
const pre=await readJson('State/session.json')
|
|
2856
3519
|
if(running && !autoDone && pre && pre.processEpoch===processEpoch) return {ok:true,message:'already running (no-op)'}
|
|
2857
3520
|
await loadAll(); await loadSettings()
|
|
3521
|
+
// §2.6 recovery runs AFTER loadAll (the formal records it may complete/annotate are restored
|
|
3522
|
+
// there — recovering earlier would write onto an unloaded `formal` and clobber them).
|
|
3523
|
+
await recoverLeanJobs({mode:'resume',applySettled:true})
|
|
3524
|
+
await refreshMathAvailability() // same reason as start(): the next wake must carry the line
|
|
2858
3525
|
// A run the group CONCLUDED (unanimous voteSolved → autoDone) must not be silently revived into
|
|
2859
3526
|
// a zombie that keeps waking residents with no consensus that it should still run. The group
|
|
2860
3527
|
// decided it is done; continuing means a NEW run (vibe_v4_start / vibe_v4_configure).
|
|
@@ -2919,7 +3586,7 @@ export function apply(ctx) {
|
|
|
2919
3586
|
// The parameter NAMES (not just the rendered string) so a caller — the `/v4 set` handler in
|
|
2920
3587
|
// particular — can reject an unknown key instead of silently dropping it.
|
|
2921
3588
|
paramsKeys: Object.keys(params),
|
|
2922
|
-
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny','formalVerify','leanCommand','leanArgs','leanTimeoutMs','finalPaper','paperFormat','paperLanguage','paperCompilePdf','paperEditor','paperLatexCommand'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
3589
|
+
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny','formalVerify','leanCommand','leanArgs','leanTimeoutMs','finalPaper','paperFormat','paperLanguage','paperCompilePdf','paperEditor','paperLatexCommand','leanAsync','leanInitiative','leanSearchPaths','leanJobsMaxParallel'].concat(MATH_PARAM_NAMES).map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
2923
3590
|
function formalReportText(){
|
|
2924
3591
|
if(!formalOn()) return '- 未启用(`formalVerify` = off;可用 vibe_v4_set 切到 encourage / require)'
|
|
2925
3592
|
const v=formalView()
|
|
@@ -2976,9 +3643,9 @@ export function apply(ctx) {
|
|
|
2976
3643
|
// Normalize one parameter value to its intended type so a string from /v4 set or configure
|
|
2977
3644
|
// becomes the right number/array. Keeps settings.json clean regardless of how it was set.
|
|
2978
3645
|
function normalizeParam(k, v){
|
|
2979
|
-
const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery','stallAutoMeetingMs']
|
|
3646
|
+
const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery','stallAutoMeetingMs','leanJobsMaxParallel']
|
|
2980
3647
|
if(INT_KEYS.includes(k)){ const n=Number(v); if(!Number.isFinite(n)) return v; return Math.floor(n) }
|
|
2981
|
-
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 [] }
|
|
3648
|
+
if(k==='toolAllow'||k==='toolDeny'||k==='leanArgs'||k==='leanSearchPaths'){ 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 [] }
|
|
2982
3649
|
// ---- Lean formal verification (docs §1) ------------------------------------------
|
|
2983
3650
|
// `formalVerify` is a three-way enum and MUST degrade to the no-op 'off' on anything else.
|
|
2984
3651
|
// Degrading to a STRONGER mode would let a typo silently gate every conclusion — the exact
|
|
@@ -2999,6 +3666,31 @@ export function apply(ctx) {
|
|
|
2999
3666
|
// v2 §A5: `office` (the session root / human side) by default, or `resident:<id>`.
|
|
3000
3667
|
if(k==='paperEditor'){ const s=String(v==null?'':v).trim(); return paperEditorOk(s)?s:DEFAULT_PARAMS.paperEditor }
|
|
3001
3668
|
if(k==='paperLatexCommand'){ const s=String(v==null?'':v).trim(); return s }
|
|
3669
|
+
// math_computation P1: the six keys go through the SHARED normalizer (explicit
|
|
3670
|
+
// enum/array/integer coercion there), so `'false'`-style strings and unknown enum values can
|
|
3671
|
+
// never leak into `params` — v4's function returns unknown keys AS-IS, which is why an
|
|
3672
|
+
// explicit branch is mandatory. The two array keys additionally accept the comma-string form
|
|
3673
|
+
// that `/v4 set` produces (v4's own style for `leanArgs`), then the module is authoritative.
|
|
3674
|
+
if(MATH_PARAM_NAMES.indexOf(k)!==-1){
|
|
3675
|
+
let val=v
|
|
3676
|
+
if((k==='mathEngines'||k==='mathPackages')&&typeof val==='string') val=val.split(',').map(x=>x.trim()).filter(Boolean)
|
|
3677
|
+
const one=normalizeMathParams({[k]:val})
|
|
3678
|
+
if(Object.prototype.hasOwnProperty.call(one,k)) return one[k]
|
|
3679
|
+
const def=DEFAULT_PARAMS[k]
|
|
3680
|
+
return Array.isArray(def)?def.slice():def
|
|
3681
|
+
}
|
|
3682
|
+
// lean-incremental-async §1.1: an EXPLICIT branch — the string 'false' (what `/v4 set` hands us)
|
|
3683
|
+
// must normalise to boolean false, never survive as a truthy string.
|
|
3684
|
+
if(k==='leanAsync'){
|
|
3685
|
+
if(typeof v==='boolean') return v
|
|
3686
|
+
const s=String(v==null?'':v).trim().toLowerCase()
|
|
3687
|
+
if(s==='true'||s==='1'||s==='yes'||s==='on') return true
|
|
3688
|
+
if(s==='false'||s==='0'||s==='no'||s==='off') return false
|
|
3689
|
+
return DEFAULT_PARAMS.leanAsync
|
|
3690
|
+
}
|
|
3691
|
+
// AMENDMENT §1: three-way PROACTIVITY enum — anything unknown degrades to the default
|
|
3692
|
+
// ('normal'), never to a stronger setting.
|
|
3693
|
+
if(k==='leanInitiative') return ['off','normal','eager'].indexOf(String(v))!==-1?String(v):DEFAULT_PARAMS.leanInitiative
|
|
3002
3694
|
// A blank command would make resolveExecutable('') fail confusingly; fall back to the default.
|
|
3003
3695
|
if(k==='leanCommand'){ const s=String(v==null?'':v).trim(); return s||'lean' }
|
|
3004
3696
|
// A non-positive timeout is meaningless (the run would be killed instantly) → default.
|
|
@@ -3016,7 +3708,7 @@ export function apply(ctx) {
|
|
|
3016
3708
|
* 注意**不要**给 activityTimeoutMs / compactThreshold 之类加下界:文档明确建议测试时把
|
|
3017
3709
|
* activityTimeoutMs 设成 40ms 这种小值,钳制它会破坏受支持的配置。
|
|
3018
3710
|
*/
|
|
3019
|
-
const INT_MIN = { maxParallel: 1, residentCount: 1 }
|
|
3711
|
+
const INT_MIN = { maxParallel: 1, residentCount: 1, leanJobsMaxParallel: 1 }
|
|
3020
3712
|
function clampInt(k, n){
|
|
3021
3713
|
const min = INT_MIN[k]
|
|
3022
3714
|
if(min===undefined) return n
|
|
@@ -3036,6 +3728,9 @@ export function apply(ctx) {
|
|
|
3036
3728
|
const has = (o,k)=>Object.prototype.hasOwnProperty.call(o,k)
|
|
3037
3729
|
const ignored=keys.filter(k=>!has(params,k))
|
|
3038
3730
|
for(const k of keys){ if(has(params,k)){ const nv=normalizeParam(k, upd[k]); params[k]= (typeof nv==='number') ? clampInt(k, nv) : nv } }
|
|
3731
|
+
// A math parameter changes what the NEXT prompt must say (the availability line encodes the
|
|
3732
|
+
// enabled engines and the mode), so refresh it here — the probe is cached, so this is cheap.
|
|
3733
|
+
if(keys.some(k=>MATH_PARAM_NAMES.indexOf(k)!==-1)) refreshMathAvailability().catch(()=>{})
|
|
3039
3734
|
saveSettings().catch(()=>{})
|
|
3040
3735
|
if(ignored.length){
|
|
3041
3736
|
logActivity('set','忽略未知参数:'+ignored.join(', '))
|
|
@@ -3071,6 +3766,7 @@ export function apply(ctx) {
|
|
|
3071
3766
|
// between abort and the next action.
|
|
3072
3767
|
meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]; busy=new Set(); wakeKind=new Map(); currentResident=''; finalizeLock=null
|
|
3073
3768
|
paperState=null // an aborted run is over: its paper flow (if any) must not keep driving wakes
|
|
3769
|
+
await disposeLean() // §2.5: abort terminates the in-flight compile and marks it interrupted
|
|
3074
3770
|
await saveAll(); return {ok:true,message:'aborted'} }
|
|
3075
3771
|
function setPause(){ clearHeartbeat(); running=false; return {ok:true,message:'paused'} }
|
|
3076
3772
|
|
|
@@ -3118,6 +3814,17 @@ export function apply(ctx) {
|
|
|
3118
3814
|
formalMode, formalOn, formalRecords, formalTodo, formalOf, formalView, formalReport,
|
|
3119
3815
|
rebuildLeanLibIndexes, leanArchive, leanRunTool, writeFormalIndex, writeFormalTodo,
|
|
3120
3816
|
leanRunToolApi: async (relPath,timeoutMs)=>await leanRunFile(relPath,timeoutMs),
|
|
3817
|
+
// lean-incremental-async §6.3: the queue is reachable from the suite/dispatcher exactly like
|
|
3818
|
+
// `leanRunToolApi`, so the async paths can be driven deterministically (no timer races).
|
|
3819
|
+
leanQueueApi:{ enqueue:enqueueLeanJob, runQueue:runLeanQueue, state:()=>({running:leanInflight.map(leanJobView),queued:leanQueue.slice(),jobs:listLeanJobs(),notices:leanNotices.slice(),cap:Math.max(1,Math.floor(Number(params.leanJobsMaxParallel))||1)}), dispose:disposeLean, recover:recoverLeanJobs, job:leanJobTool },
|
|
3820
|
+
leanRead, listLeanJobs, leanJobs:()=>Object.assign({},leanJobs), leanJobTool,
|
|
3821
|
+
/** math_computation host surface — the SAME object the per-session module instance is keyed
|
|
3822
|
+
* by (`mathInstanceFor`), so the tool path and the prompt-side probe share one instance per
|
|
3823
|
+
* session (and one probe cache), and no state is shared across sessions. These accessors read
|
|
3824
|
+
* LIVE state (never a snapshot — `/v4 set mathMode=typed` must change the very next call). */
|
|
3825
|
+
mathHost: mathProbeHost,
|
|
3826
|
+
/** Absolute paths the Lean layer uses (read-only display; `searchPaths` are injected per spawn). */
|
|
3827
|
+
leanPaths:()=>({searchPath:vibeRoot().replace(/\\/g,'/'),searchPaths:leanSearchPathList(),lib:formalLibRoot().replace(/\\/g,'/'),proved:formalProvedRoot().replace(/\\/g,'/'),jobs:leanJobsRel()}),
|
|
3121
3828
|
/**
|
|
3122
3829
|
* The prompt builders, addressed BY RESIDENT ID. "成员读到的文字就是产品"
|
|
3123
3830
|
* (AUDIT-CHECKLIST §0.1): a suite that can only observe tool return values is blind to a
|
|
@@ -3208,7 +3915,7 @@ export function apply(ctx) {
|
|
|
3208
3915
|
// MODE is dynamic — switching it changes the very next prompt), leanCommand/leanArgs select the
|
|
3209
3916
|
// executable, leanTimeoutMs bounds one run. Invalid values fall back to the defaults and an
|
|
3210
3917
|
// unknown mode degrades to 'off' (never to a STRONGER mode).
|
|
3211
|
-
registerTool('vibe_v4_set','Set V4 parameters: residentCount (how many residents a start spawns), compactThreshold (resident context % that triggers a compaction) and compactAfterRounds (rounds between soft compactions), meetingKeepEvery (every N newly accumulated artifacts an automatic sync meeting is convened), maxParallel (how many residents may be woken concurrently), activityTimeoutMs (idle window before a resident is nudged; also the heartbeat/watchdog period), stallAutoMeetingMs (how long the group may make NO progress before an auto sync meeting is convened), verdictMaxRounds (how many verification rounds one object gets: 1 independent round + re-vote debate rounds), 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; finalPaper (default true) writes the run\'s final PAPER automatically once the closing meeting votes unanimously to stop, paperFormat: "both" (default) | "md" | "tex" decides which text versions are produced, paperLanguage: "zh" (default) | "en", paperCompilePdf (default true) compiles a PDF when a LaTeX engine is actually detected (xelatex|latexmk|pdflatex|lualatex|tectonic, overridable via paperLatexCommand), paperEditor: "office" (default — the session root/human side) | "resident:<id>" names who finalises the merged draft after the cross-review round. A key outside this list is refused (ok:false, ignored:[...]) instead of being silently dropped.',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'},finalPaper:{type:'boolean'},paperFormat:{type:'string',enum:['both','md','tex']},paperLanguage:{type:'string',enum:['zh','en']},paperCompilePdf:{type:'boolean'},paperEditor:{type:'string'},paperLatexCommand:{type:'string'}}),(s,a)=>{ const r=s.setParams(a); const st=s.status(); if(r&&r.ok===false) return Object.assign({},st,{ok:false,warning:r.message,ignored:r.ignored}); return st })
|
|
3918
|
+
registerTool('vibe_v4_set','Set V4 parameters: residentCount (how many residents a start spawns), compactThreshold (resident context % that triggers a compaction) and compactAfterRounds (rounds between soft compactions), meetingKeepEvery (every N newly accumulated artifacts an automatic sync meeting is convened), maxParallel (how many residents may be woken concurrently), activityTimeoutMs (idle window before a resident is nudged; also the heartbeat/watchdog period), stallAutoMeetingMs (how long the group may make NO progress before an auto sync meeting is convened), verdictMaxRounds (how many verification rounds one object gets: 1 independent round + re-vote debate rounds), 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; leanAsync (default true) ENQUEUES each compile to a per-session background queue (concurrency 1, budget = leanTimeoutMs) and returns immediately with async:{jobId,state:\'queued\'} — results are announced in the next prompt and visible in vibe_v4_lean_lib.jobs; set leanAsync=false for the historical synchronous await; leanInitiative: "off" | "normal" (default) | "eager" is how PROACTIVE the group should be while working (it never changes what formalVerify requires at voting time); leanSearchPaths (array, default empty) adds extra compiler search roots — they are injected BEFORE the automatic VibeMath root, de-duplicated, and an explicit --search-path/-R/--root in leanArgs still wins; leanJobsMaxParallel (default 1) caps how many background compiles run at once; mathComputation: "auto" (default) | "on" | "off" enables the math_computation tool (off = a true no-op: no probe and no prompt line), mathMode: "typed+shell" (default) | "typed" drops the host-shell fallback and refuses the cli engine, mathEngines limits which engines may run (default python|r|octave|julia|matlab|maple|wolfram|cli), mathTimeoutMs bounds one computation, mathPackages is the pre-checked package list, mathInstallScope: "user" (default) | "system" is the scope an approved install plan may use (system applies to that request only and is never remembered); finalPaper (default true) writes the run\'s final PAPER automatically once the closing meeting votes unanimously to stop, paperFormat: "both" (default) | "md" | "tex" decides which text versions are produced, paperLanguage: "zh" (default) | "en", paperCompilePdf (default true) compiles a PDF when a LaTeX engine is actually detected (xelatex|latexmk|pdflatex|lualatex|tectonic, overridable via paperLatexCommand), paperEditor: "office" (default — the session root/human side) | "resident:<id>" names who finalises the merged draft after the cross-review round. A key outside this list is refused (ok:false, ignored:[...]) instead of being silently dropped.',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'},finalPaper:{type:'boolean'},paperFormat:{type:'string',enum:['both','md','tex']},paperLanguage:{type:'string',enum:['zh','en']},paperCompilePdf:{type:'boolean'},paperEditor:{type:'string'},paperLatexCommand:{type:'string'},leanAsync:{type:'boolean'},leanInitiative:{type:'string',enum:['off','normal','eager']},leanSearchPaths:{type:'array',items:{type:'string'}},leanJobsMaxParallel:{type:'integer'},mathComputation:{type:'string',enum:['auto','on','off']},mathMode:{type:'string',enum:['typed+shell','typed']},mathEngines:{type:'array',items:{type:'string'}},mathTimeoutMs:{type:'integer'},mathPackages:{type:'array',items:{type:'string'}},mathInstallScope:{type:'string',enum:['user','system']}}),(s,a)=>{ const r=s.setParams(a); const st=s.status(); if(r&&r.ok===false) return Object.assign({},st,{ok:false,warning:r.message,ignored:r.ignored}); return st })
|
|
3212
3919
|
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
3213
3920
|
// fall back to the last-woken resident when called by the host/assistant.
|
|
3214
3921
|
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) })
|
|
@@ -3243,13 +3950,91 @@ export function apply(ctx) {
|
|
|
3243
3950
|
// if a human or an agent calls them deliberately, but no prompt mentions them.
|
|
3244
3951
|
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))
|
|
3245
3952
|
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))
|
|
3246
|
-
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)=>{
|
|
3953
|
+
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. The `paths.searchPath` field is the module root the framework injects into the compiler, and `jobs` shows the state of every background compile (leanAsync).',objParams({refresh:{type:'boolean'}}),async (s,a)=>{
|
|
3247
3954
|
const r=(a&&a.refresh===false)?{lib:null,proved:null,objects:Object.keys(s.formalRecords()).length}:await s.rebuildLeanLibIndexes()
|
|
3248
3955
|
return { ok:true, mode:s.formalMode(), rebuilt:!(a&&a.refresh===false), counts:r, todo:s.formalTodo(),
|
|
3249
3956
|
objects:s.formalView().objects,
|
|
3250
|
-
paths:{project:'Formal/(相对项目根)',lib:'VibeMath/Formal/Lib/',proved:'VibeMath/Formal/Proved/',proofs:'Verified/Lean/'},
|
|
3251
|
-
|
|
3957
|
+
paths:{project:'Formal/(相对项目根)',lib:'VibeMath/Formal/Lib/',proved:'VibeMath/Formal/Proved/',proofs:'Verified/Lean/',searchPath:s.leanPaths().searchPath,searchPaths:s.leanPaths().searchPaths},
|
|
3958
|
+
searchPath:s.leanPaths().searchPath,
|
|
3959
|
+
importNamespace:{lib:'import Formal.Lib.<name>',proved:'import Formal.Proved.<name>'},
|
|
3960
|
+
jobs:s.listLeanJobs(),
|
|
3961
|
+
hint:"复用优先:先在 Lib/ 里找现成定义(vibe_v4_lean_read 可取原文逐字复制);新定义用 vibe_v4_lean_archive kind='def' 归档,已证引理用 kind='lemma';后台编译进度看 jobs。" }
|
|
3962
|
+
})
|
|
3963
|
+
// lean-incremental-async §1.2: the ONLY new (read-only) tool. Reading an archived definition or
|
|
3964
|
+
// lemma VERBATIM is how a member reuses it without re-deriving it — or copies it when the local
|
|
3965
|
+
// toolchain cannot follow the import.
|
|
3966
|
+
registerTool('vibe_v4_lean_read','(resident, read-only) Read ONE archived Lean library file verbatim (for import-or-copy reuse). name is a bare library name (no path); kind is auto|lib|proved. Refuses anything outside <VibeMath>/Formal/{Lib,Proved}, and truncates at 64KB (truncated:true).',objParams({name:{type:'string'},kind:{type:'string',enum:['auto','lib','proved']}},['name']),(s,a)=>s.leanRead(a))
|
|
3967
|
+
// AMENDMENT §3: the read-only job/result surface (no polling tool beyond this one).
|
|
3968
|
+
registerTool('vibe_v4_lean_job','(resident, read-only) Background-compile jobs. Without jobId: this session\'s job list (state/receipt/archive). With jobId: that job\'s state, exitCode, receipt path (Formal/Jobs/<jobId>.json) and archive path; waitMs>0 waits up to that many milliseconds and then reports the current state (it never blocks forever).',objParams({jobId:{type:'string'},waitMs:{type:'integer'}}),(s,a)=>s.leanJobTool(a))
|
|
3969
|
+
|
|
3970
|
+
// §2.5: the fiber's disposer must not leave an orphan compiler behind. Every session's in-flight
|
|
3971
|
+
// Lean job is terminated (and marked `interrupted`, never `passed`) when this preset subtree
|
|
3972
|
+
// unloads — the same lifecycle the tool/command registrations above already follow.
|
|
3973
|
+
ctx.effect(() => () => { for(const s of sessions.values()){ try { s.disposeLean() } catch(e){ /* best effort */ } } })
|
|
3974
|
+
|
|
3975
|
+
// (3) Per-session module instances: LAZY, cached by the session's host object. Each instance has
|
|
3976
|
+
// its own adapted host (and therefore its own probe cache), so no state is shared between
|
|
3977
|
+
// sessions — there is no "current session" slot to clobber, and concurrent calls from two
|
|
3978
|
+
// sessions are fully independent (v5 uses the same structure).
|
|
3979
|
+
const mathInstances = new WeakMap() // session host object -> module instance
|
|
3980
|
+
function mathInstanceFor(hostObj){
|
|
3981
|
+
if(!hostObj) return null
|
|
3982
|
+
const cached=mathInstances.get(hostObj)
|
|
3983
|
+
if(cached) return cached
|
|
3984
|
+
const inst=registerMathComputation({
|
|
3985
|
+
// Capture-only: the tool face was registered ONCE for the plugin instance (step 1/2 above).
|
|
3986
|
+
register: ()=>{},
|
|
3987
|
+
params: ()=>hostObj.params(),
|
|
3988
|
+
projectRoot: ()=>hostObj.projectRoot(),
|
|
3989
|
+
designator: 'vibe-math-v4',
|
|
3990
|
+
writeText: (rel, text)=>hostObj.writeText(rel, text),
|
|
3991
|
+
readText: (rel)=>hostObj.readText(rel),
|
|
3992
|
+
exists: (rel)=>hostObj.exists(rel),
|
|
3993
|
+
// audit-R2 lens-2: attached ONLY when the session surface really has it, so a surface without
|
|
3994
|
+
// listDir reaches the module as an ABSENT field (the module's optional-callback contract).
|
|
3995
|
+
...(typeof hostObj.listDir === 'function' ? { listDir: (rel)=>hostObj.listDir(rel) } : {}),
|
|
3996
|
+
// round-7 (fix 2): pass the bundled-runtime seam through to the per-session module instance.
|
|
3997
|
+
...(typeof hostObj.runtimeRoots === 'function' ? { runtimeRoots: ()=>hostObj.runtimeRoots() } : {}),
|
|
3998
|
+
...(typeof hostObj.listDirAbs === 'function' ? { listDirAbs: (abs)=>hostObj.listDirAbs(abs) } : {}),
|
|
3999
|
+
resolveExecutable: (cmd)=>hostObj.resolveExecutable(cmd),
|
|
4000
|
+
spawn: (o)=>hostObj.spawn(o),
|
|
4001
|
+
// Optional capability flag: a host that KNOWS it has no subprocess service reports
|
|
4002
|
+
// MATH_NO_SUBPROCESS up front instead of a misleading ENGINE_NOT_FOUND + install guide.
|
|
4003
|
+
hasSubprocess: ()=>!!subprocessOf(),
|
|
4004
|
+
log: (kind, msg)=>hostObj.log(kind, msg),
|
|
4005
|
+
})
|
|
4006
|
+
mathInstances.set(hostObj, inst)
|
|
4007
|
+
return inst
|
|
4008
|
+
}
|
|
4009
|
+
function mathHandlerFor(session){
|
|
4010
|
+
const inst=mathInstanceFor(session&&session.mathHost)
|
|
4011
|
+
return inst?inst.handler:async()=>({ok:false,code:'MATH_INVALID_ARGUMENT',message:'math_computation: no session'})
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
// ================= math_computation registration (P1) =================
|
|
4015
|
+
// The tool CORE is the shared module (installed byte-identically in all four presets).
|
|
4016
|
+
// (1) A throwaway CAPTURE-ONLY host yields the frozen tool face once: the module owns the name,
|
|
4017
|
+
// description and closed schema, and this preset must advertise exactly those.
|
|
4018
|
+
registerMathComputation({
|
|
4019
|
+
register: (name, description, parameters, handler)=>{
|
|
4020
|
+
// The description/schema must be the module's frozen text (a local re-spelling would silently
|
|
4021
|
+
// diverge from the other three presets); referencing MATH_TOOL_DESCRIPTION is that check.
|
|
4022
|
+
if(description!==MATH_TOOL_DESCRIPTION) ctx.logger.warn('math_computation: 工具描述与共享模块的 MATH_TOOL_DESCRIPTION 不一致')
|
|
4023
|
+
mathToolFace={name, description, parameters, handler}
|
|
4024
|
+
},
|
|
4025
|
+
params: ()=>DEFAULT_PARAMS,
|
|
4026
|
+
projectRoot: ()=>'',
|
|
4027
|
+
writeText: async()=>false,
|
|
4028
|
+
readText: async()=>undefined,
|
|
4029
|
+
exists: async()=>false,
|
|
4030
|
+
resolveExecutable: async()=>{ throw new Error('NO_SUBPROCESS: capture-only host') },
|
|
4031
|
+
spawn: async()=>null,
|
|
4032
|
+
designator: 'vibe-math-v4',
|
|
4033
|
+
log: ()=>{},
|
|
3252
4034
|
})
|
|
4035
|
+
// (2) ONE tool registration for the whole plugin instance; the handler resolves the CALLING
|
|
4036
|
+
// session's own module instance.
|
|
4037
|
+
registerTool(mathToolFace.name, mathToolFace.description, mathToolFace.parameters, (s,a)=>mathHandlerFor(s)(a||{}))
|
|
3253
4038
|
|
|
3254
4039
|
// Same lifecycle rule as registerTool: commands.register() returns a disposer, so the
|
|
3255
4040
|
// registration belongs to this fiber and must be unwound with it.
|