dsh-vibe-math 2.4.1 → 2.6.0
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 +53 -14
- package/README.md +54 -14
- package/cordis.patch.yml +54 -17
- package/docs/AUDIT-CHECKLIST.md +26 -7
- package/docs/COMPAT-AUDIT-ROUND3.md +16 -1
- package/docs/final-paper.md +171 -0
- package/docs/release-notes/RELEASE-NOTES-2.5.0.md +173 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
- package/docs/test-timing.md +77 -40
- package/installer.js +225 -45
- package/package.json +6 -3
- package/preset-declaration.js +38 -7
- package/prompt-corpus-persona/persona-corpus.json +13 -13
- package/prompt-corpus-persona/persona-corpus.md +113 -39
- package/prompt-corpus-v2/formal-verify-v2.json +128 -92
- package/prompt-corpus-v2/formal-verify-v2.md +380 -142
- package/prompt-corpus-v3/formal-verify-v3.json +117 -81
- package/prompt-corpus-v3/formal-verify-v3.md +693 -1189
- package/prompt-corpus-v4/formal-verify-v4.json +38 -8
- package/prompt-corpus-v4/formal-verify-v4.md +157 -25
- package/prompt-corpus-v5/prompt-corpus-v5.json +17 -17
- package/prompt-corpus-v5/prompt-corpus-v5.md +32 -26
- package/tests/audit-installer-compat.test.mjs +70 -4
- package/tests/audit-installer-policy.test.mjs +229 -4
- package/tests/audit-persona-sensitivity.mjs +68 -8
- package/tests/audit-persona-surface.test.mjs +21 -8
- package/tests/audit-v5-sensitivity.mjs +47 -6
- package/tests/e2e-v5-round2.test.mjs +825 -1
- package/tests/formal-verify-v2.test.mjs +57 -2
- package/tests/formal-verify-v3.test.mjs +67 -2
- package/tests/formal-verify-v4.test.mjs +120 -4
- package/tests/prompt-v5-integrity.test.mjs +9 -3
- package/tests/run-tests.mjs +104 -23
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +20 -4
- package/vibe-math-v2/vibe-math-v2.js +1318 -106
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +20 -4
- package/vibe-math-v3/agent.cordis.yml +36 -14
- package/vibe-math-v3/vibe-math-v3.js +1348 -179
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +3 -3
- package/vibe-math-v4/agent.cordis.yml +36 -8
- package/vibe-math-v4/vibe-math-v4.js +1195 -57
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +91 -9
- package/vibe-math-v5/agent.cordis.yml +16 -8
- package/vibe-math-v5/vibe-math-v5.js +1628 -112
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +143 -41
|
@@ -43,6 +43,50 @@ export function apply(ctx) {
|
|
|
43
43
|
// ================= per-session registry =================
|
|
44
44
|
const sessions = new Map() // rootAgentId -> Session
|
|
45
45
|
const childOwner = new Map() // childId -> rootAgentId (route subagent/end back to its session)
|
|
46
|
+
// childOwner 的**有界化**(审计 H6)。一条 childId→rootAgentId 的映射只在"这个 child 还可能再发
|
|
47
|
+
// subagent/end"时有价值;子代理结束后它就是垃圾,而这里以前明确说"不要回收"(为了修 verdict 收口),
|
|
48
|
+
// 代价是每个历史子代理永久留下一条记录、进程生命周期内无界增长。现在改为**引用 + 宽限期**:
|
|
49
|
+
// · 仍被某个任务(t.children)或 agentRegistry 引用的一律保留;
|
|
50
|
+
// · 没有引用、且距最近一次 subagent/end 已超过宽限期,才回收。
|
|
51
|
+
// 宽限期是必需的:辩论/续轮会在**同一个 child** 上再次 end(这正是当初"结束后立即删"导致
|
|
52
|
+
// 'problem solved after verdict 1' 失败的原因),所以不能只看"当前是否有引用"。
|
|
53
|
+
const CHILD_OWNER_GRACE_MS = 5 * 60 * 1000
|
|
54
|
+
const lastChildEndAt = new Map() // childId -> 最近一次 subagent/end 的时间
|
|
55
|
+
function pruneChildOwner() {
|
|
56
|
+
const keep = new Set()
|
|
57
|
+
for (const s of sessions.values()) {
|
|
58
|
+
try { for (const id of s.referencedChildIds()) keep.add(id) } catch (e) { /* 尽力而为 */ }
|
|
59
|
+
}
|
|
60
|
+
const cutoff = now() - CHILD_OWNER_GRACE_MS
|
|
61
|
+
for (const cid of Array.from(childOwner.keys())) {
|
|
62
|
+
if (keep.has(cid)) continue
|
|
63
|
+
const ended = lastChildEndAt.get(cid)
|
|
64
|
+
if (ended === undefined || ended > cutoff) continue
|
|
65
|
+
childOwner.delete(cid); lastChildEndAt.delete(cid)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// sessions 的**有界化**(审计 L18)。`sessions` 按 root agent id 记;正常路径下 DSH 的每个根会话
|
|
69
|
+
// 各一条,但 `getSession` 对**任何** agent 都会建一条,包括 `rootOf` 向上走失败(父代理已不在注册表里)
|
|
70
|
+
// 的子代理——那种会话永远没人再访问,却会带着 `running=true` 一直被 apply 级的 tick 循环唤醒。
|
|
71
|
+
// `agents.roots()`(契约:全部 live 顶层 agent)是权威判据:root 不再是顶层 live agent ⇒ 该会话是僵尸。
|
|
72
|
+
// 三重保守:主机没有 roots() / 它抛错 / 它返回空数组 ⇒ **一律不裁剪**(空 = 没有可用信息,而不是
|
|
73
|
+
// "一个会话都没有");仍在册子代理的会话也保留。因此测试 mock(roots() 恒返回 [])与旧宿主行为不变。
|
|
74
|
+
function pruneSessions() {
|
|
75
|
+
if (sessions.size === 0) return
|
|
76
|
+
if (typeof agents.roots !== 'function') return
|
|
77
|
+
let roots = []
|
|
78
|
+
try { roots = agents.roots() || [] } catch (e) { return }
|
|
79
|
+
if (!Array.isArray(roots) || roots.length === 0) return
|
|
80
|
+
const live = new Set()
|
|
81
|
+
for (let i = 0; i < roots.length; i++) { const id = sessionIdOf(roots[i]); if (id !== undefined) live.add(id) }
|
|
82
|
+
if (live.size === 0) return
|
|
83
|
+
for (const sid of Array.from(sessions.keys())) {
|
|
84
|
+
if (live.has(sid)) continue
|
|
85
|
+
const s = sessions.get(sid)
|
|
86
|
+
try { if (s && s.referencedChildIds().length > 0) continue } catch (e) { continue }
|
|
87
|
+
sessions.delete(sid)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
46
90
|
// Process epoch: PROCESS-level (one per apply, shared by every session), written to
|
|
47
91
|
// VibeMath_State/process_epoch.json at init; a DIFFERENT persisted epoch means a
|
|
48
92
|
// previous DSH process wrote this state (in-flight children are gone), while an
|
|
@@ -90,7 +134,7 @@ export function apply(ctx) {
|
|
|
90
134
|
directionsPerSolver: 1, // 每个 solver 提示词附带的方向数量(1 = 只看自己方向)
|
|
91
135
|
verifierCount: 3, // independent reviewers per verification
|
|
92
136
|
debateMaxRounds: 5, // debate round cap (spec example)
|
|
93
|
-
verdictMode: 'flat', // flat =
|
|
137
|
+
verdictMode: 'flat', // flat = 均衡机制(不一致时取各评审自报概率的均值)| forced = 强制裁决(按验证者历史准确率+布尔票置信度加权)
|
|
94
138
|
provider: '',
|
|
95
139
|
model: '',
|
|
96
140
|
solverPersona: '',
|
|
@@ -113,19 +157,26 @@ export function apply(ctx) {
|
|
|
113
157
|
priorityAdjust: 'none', // none | deadend-deprioritize | survival-map
|
|
114
158
|
proposPriorityAdjust: 'none', // none | progress-graded(按定论接近度+证明/证伪材料量动态调命题优先级)
|
|
115
159
|
tickIntervalMs: 2000, // 调度器心跳间隔(毫秒)
|
|
116
|
-
activityLogCap: 100, // 活动日志保留条数(report
|
|
160
|
+
activityLogCap: 100, // 活动日志保留条数(status/report 的 recentActivity 最多显示 ACTIVITY_REPORT_MAX = 30 条)
|
|
117
161
|
maxExplorerRetries: 3, // explorer 重派生上限(拆方向失败重试次数)
|
|
118
162
|
// ---- Lean 形式化验证(契约:docs/formal-verification.md §1,四架构同名同语义)----
|
|
119
163
|
formalVerify: 'off', // off = 无任何额外要求(真无操作)| encourage = 鼓励但不强制 | require = 强制 + 定论门禁
|
|
120
164
|
leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
|
|
121
165
|
leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配合 leanCommand='lake')
|
|
122
166
|
leanTimeoutMs: 120000, // 单次 Lean 运行的超时上限(毫秒)
|
|
167
|
+
// ---- 最终论文(规格:docs/final-paper.md;v2/v3 为单作者变体)----
|
|
168
|
+
finalPaper: true, // 收口时是否自动撰写最终论文(false 只关自动触发;/vibe paper 仍可用)
|
|
169
|
+
paperFormat: 'both', // both = md + tex | md = 只写 markdown | tex = 只写 latex
|
|
170
|
+
paperLanguage: 'zh', // zh = 中文(ctexart/xelatex 优先)| en = 英文(article/pdflatex 优先)
|
|
171
|
+
paperCompilePdf: true, // 检测到 LaTeX 时是否编译 paper.pdf
|
|
172
|
+
paperLatexCommand: '', // 指定 LaTeX 引擎(空 = 按语言探测 xelatex/latexmk/pdflatex/lualatex/tectonic)
|
|
123
173
|
}
|
|
124
174
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
125
175
|
let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导,不再作为字段
|
|
126
176
|
let agentRegistry = {}
|
|
127
177
|
let decisionQueue = []
|
|
128
|
-
let verifierAccuracy = {}
|
|
178
|
+
let verifierAccuracy = {} // 稳定身份键('m:<provider>/<model>')→ { correct, total }
|
|
179
|
+
let pendingReviewScores = {} // 对象 id → [{key,result}]:等对象**后来**取得布尔定论时才计分
|
|
129
180
|
let tasks = {} // verify tasks keyed by 'verify:<rId>'
|
|
130
181
|
let activityLog = []
|
|
131
182
|
let lastReportWrite = 0
|
|
@@ -134,6 +185,16 @@ export function apply(ctx) {
|
|
|
134
185
|
let tickInFlight = false
|
|
135
186
|
let lastTickAt = 0
|
|
136
187
|
let explorerRetries = {}
|
|
188
|
+
// 状态/报告里最近活动最多显示多少条(**一个常量**):此前 buildReport 用 min(30, cap)、getStatus
|
|
189
|
+
// 硬编码 min(10, cap),同一个字段两个端点给出不同答案,而参数说明承诺的是 30(审计 M15)。
|
|
190
|
+
const ACTIVITY_REPORT_MAX = 30
|
|
191
|
+
// 至少 2 名独立评审才能出裁决(审计 M11):一票裁决会把单个验证者的判断写成"完全验证"的布尔结论。
|
|
192
|
+
const MIN_REVIEWERS = 2
|
|
193
|
+
// 中段裁决(0<正确概率<1)的**重验冷却**(审计 M12):中段值是"为真的概率",不是定论,对象不能
|
|
194
|
+
// 被永久搁置;但每个 tick 都重开一轮完整辩论会把调度器饿死(那正是当初加 `已验证` 单向闩锁的原因)。
|
|
195
|
+
// 冷却期过后重新入选,所以没有任何对象会被永久停在中间概率上。
|
|
196
|
+
const REVERIFY_COOLDOWN_MS = 5 * 60 * 1000
|
|
197
|
+
function reverifyDue(x) { const at = Number(x && x.最近验证时间) || 0; return (now() - at) >= REVERIFY_COOLDOWN_MS }
|
|
137
198
|
|
|
138
199
|
// ================= helpers =================
|
|
139
200
|
function textBlock(t) { return { type: 'text', text: String(t) } }
|
|
@@ -150,15 +211,57 @@ export function apply(ctx) {
|
|
|
150
211
|
function getPolicy() { const sp = sandboxPolicyOf(); if (!sp) { warnNoPolicyOnce(); return undefined } try { if (rootAgent && rootAgent.session) return sp.resolve({ session: rootAgent.session }) } catch (e) { warnNoPolicyOnce() } try { const p = sp.resolve({}); if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v2: falling back to sandboxPolicy.resolve({}) — the fence root is the host-configured workspace, not necessarily this session cwd') } return p } catch (e) { warnNoPolicyOnce() } return undefined }
|
|
151
212
|
function makeSignal(ms) { return AbortSignal.timeout(ms || 30000) }
|
|
152
213
|
|
|
214
|
+
// ================= capability / tool-name lists =================
|
|
215
|
+
// Tool names for the permission filter, taken from the names the host ACTUALLY
|
|
216
|
+
// registers (dsh-tool-web registers 'web_search'/'web_fetch'; 'web'/'fetch' are
|
|
217
|
+
// only presentation card/kind fields, not tool names), and split by platform
|
|
218
|
+
// because each preset's composition gates them:
|
|
219
|
+
// dsh-tool-bash disabled: process.platform === 'win32'
|
|
220
|
+
// dsh-tool-pwsh disabled: process.platform !== 'win32'
|
|
221
|
+
// dsh-tools' restrict() THROWS on any name outside its registered set, and the
|
|
222
|
+
// host applies the filter when establishing a continuable child
|
|
223
|
+
// (dsh-subagent: childCtx.tools.restrict(...)), so a stale name meant the child
|
|
224
|
+
// was never created at all.
|
|
225
|
+
const IS_WINDOWS = process.platform === 'win32'
|
|
226
|
+
const SCRIPT_TOOLS = IS_WINDOWS ? ['pwsh'] : ['bash']
|
|
227
|
+
// 这两个名字是否真的存在取决于**本次组合**:v2 自己的 agent.cordis.yml 把 tool-web 行设成
|
|
228
|
+
// fetch:false,于是 web_search 在、web_fetch 不在,而 filter 里只要有一个宿主不注册的名字,
|
|
229
|
+
// restrict() 就整条拒绝 → 子代理先失败一次再走 sanitizeToolFilter 重试,日志还会谎称"你的配置过期"
|
|
230
|
+
// (审计 M8/M9)。所以能力清单**向宿主的可见工具面查询**(tools.schemas,宿主自己在
|
|
231
|
+
// dsh-tools/lib/index.js 里就是这么用的),查不到/不可信时才退回静态候选表。
|
|
232
|
+
const NETWORK_TOOLS = ['web_search', 'web_fetch']
|
|
233
|
+
/**
|
|
234
|
+
* 本会话可见的工具名集合;null = 无法可信地确定(此时一律保留候选名,行为与改动前一致)。
|
|
235
|
+
* 自校验:这张表必须包含本插件自己注册的工具,否则说明这个 scope 视图不是子代理真正看到的
|
|
236
|
+
* 组合面(例如只返回全局层)——据它裁剪能力清单会**静默丢掉**用户要的工具,比多一次重试更糟。
|
|
237
|
+
*/
|
|
238
|
+
function composedToolNames() {
|
|
239
|
+
try {
|
|
240
|
+
if (!tools || typeof tools.schemas !== 'function' || !rootAgent) return null
|
|
241
|
+
const list = tools.schemas(rootAgent)
|
|
242
|
+
if (!Array.isArray(list) || list.length === 0) return null
|
|
243
|
+
const names = new Set()
|
|
244
|
+
for (let i = 0; i < list.length; i++) { const n = list[i] && list[i].name; if (n) names.add(String(n)) }
|
|
245
|
+
if (!names.has('vibe_math_status')) return null
|
|
246
|
+
return names
|
|
247
|
+
} catch (e) { return null }
|
|
248
|
+
}
|
|
249
|
+
/** 候选工具名 ∩ 本次组合真正注册的名字(不确定时原样返回候选)。 */
|
|
250
|
+
function composedToolList(candidates) {
|
|
251
|
+
const names = composedToolNames()
|
|
252
|
+
if (!names) return candidates.slice()
|
|
253
|
+
return candidates.filter(function (n) { return names.has(n) })
|
|
254
|
+
}
|
|
255
|
+
|
|
153
256
|
// ================= parameter schema =================
|
|
154
257
|
const PARAM_SCHEMA = [
|
|
155
258
|
{ name: 'mode', type: 'enum', options: ['auto', 'manual'], description: 'auto = 无人值守自动通过关键节点;manual = 关键节点挂起人工决策', suggestion: 'auto' },
|
|
156
259
|
{ name: 'maxParallelThreshold', type: 'integer', description: '全局最大并发子代理轮数(新派发前须满足 active < 阈值)', suggestion: 4 },
|
|
157
260
|
{ name: 'solverMaxRounds', type: 'integer', description: '每个求解方向的最大迭代轮数(agent_self_iteration 上限)', suggestion: 3 },
|
|
158
261
|
{ name: 'directionsPerSolver', type: 'integer', description: '每个 solver 提示词附带的其他活跃方向摘要数量:1 = 只看自己方向(互不干扰);N>1 = 额外附带最多 N 个其他活跃方向摘要用于协调', suggestion: 1 },
|
|
159
|
-
{ name: 'verifierCount', type: 'integer', description: '
|
|
262
|
+
{ name: 'verifierCount', type: 'integer', description: '每个验证对象的独立验证器数量(下限 2:一票不裁决)。并发预算紧张时单个对象同时最多占用 maxParallelThreshold-2 个槽位(至少 2),以保证另一个对象也凑得齐 2 票', suggestion: 3 },
|
|
160
263
|
{ name: 'debateMaxRounds', type: 'integer', description: '验证辩论(交流群)最大轮数', suggestion: 5 },
|
|
161
|
-
{ name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: 'flat =
|
|
264
|
+
{ name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: 'flat = 均衡机制(不一致时取各评审自报概率的**均值**,绝不折叠成固定的 0.5);forced = 强制裁决(按验证者历史准确率+布尔票置信度加权)。准确率按 provider/model 这种稳定身份记,且只在对象**后来**取得布尔定论时才计分', suggestion: 'flat' },
|
|
162
265
|
{ name: 'provider', type: 'string', description: '子代理模型 provider(空 = 继承根代理)', suggestion: '' },
|
|
163
266
|
{ name: 'model', type: 'string', description: '子代理模型 id(空 = 继承根代理)', suggestion: '' },
|
|
164
267
|
{ name: 'solverPersona', type: 'string', description: '注入每个求解器提示词开头的人格/要求', suggestion: '' },
|
|
@@ -169,24 +272,29 @@ export function apply(ctx) {
|
|
|
169
272
|
{ name: 'solverToolDeny', type: 'string[]', description: '求解器禁止的工具名列表', suggestion: [] },
|
|
170
273
|
{ name: 'verifierToolAllow', type: 'string[]', description: '验证器允许的工具名列表', suggestion: [] },
|
|
171
274
|
{ name: 'verifierToolDeny', type: 'string[]', description: '验证器禁止的工具名列表', suggestion: [] },
|
|
172
|
-
{ name: 'solverAllowNetwork', type: 'boolean', description: '求解器网络工具开关:空=继承全部;true=允许(在已有 allow
|
|
275
|
+
{ name: 'solverAllowNetwork', type: 'boolean', description: '求解器网络工具开关:空=继承全部;true=允许(在已有 allow 列表时补入本次组合真正注册的网络工具);false=禁止网络工具', suggestion: '' },
|
|
173
276
|
{ name: 'verifierAllowNetwork', type: 'boolean', description: '验证器网络工具开关(同 solverAllowNetwork)', suggestion: '' },
|
|
174
|
-
{ name: 'solverAllowScripts', type: 'boolean', description: '求解器脚本工具开关:空=继承全部;true=允许(在已有 allow 列表时补入);false=禁止
|
|
277
|
+
{ name: 'solverAllowScripts', type: 'boolean', description: '求解器脚本工具开关:空=继承全部;true=允许(在已有 allow 列表时补入);false=禁止 ' + SCRIPT_TOOLS.join('/'), suggestion: '' },
|
|
175
278
|
{ name: 'verifierAllowScripts', type: 'boolean', description: '验证器脚本工具开关(同 solverAllowScripts)', suggestion: '' },
|
|
176
|
-
{ name: 'solverMaxToolCalls', type: 'integer', description: '
|
|
177
|
-
{ name: 'verifierMaxToolCalls', type: 'integer', description: '
|
|
279
|
+
{ name: 'solverMaxToolCalls', type: 'integer', description: '求解器每轮外部工具调用**建议**上限(0 = 不限)。框架只把它写进提示词,不计数、不强制', suggestion: 0 },
|
|
280
|
+
{ name: 'verifierMaxToolCalls', type: 'integer', description: '验证器每轮外部工具调用**建议**上限(0 = 不限)。框架只把它写进提示词,不计数、不强制', suggestion: 0 },
|
|
178
281
|
{ name: 'reportIntervalMs', type: 'integer', description: '进度汇报间隔(毫秒):0 = 仅事件驱动(有代理状态更新等事件才写/推报告);>0 = 同时按该间隔定时自动汇报', suggestion: 0 },
|
|
179
282
|
{ name: 'reportMode', type: 'enum', options: ['file', 'push', 'both'], description: 'file = 写报告文件;push = 推送消息让主代理主动汇报;both = 两者都做', suggestion: 'file' },
|
|
180
283
|
{ name: 'promoteValueThreshold', type: 'number', description: 'Propos 中「价值/关键性」≥ 该值且未决(0,1) 的命题自动加入 qs.json', suggestion: 0.7 },
|
|
181
284
|
{ name: 'priorityAdjust', type: 'enum', options: ['none', 'deadend-deprioritize', 'survival-map'], description: '优先级动态调整策略:none=不自动调;deadend-deprioritize=方向全死路时降优先级;survival-map=按最高方向存活率重算(存活率高越优先)', suggestion: 'none' },
|
|
182
285
|
{ name: 'proposPriorityAdjust', type: 'enum', options: ['none', 'progress-graded'], description: '命题优先级动态调整:none=不自动调;progress-graded=按「定论接近度(|布尔估计-0.5|)+ 证明/证伪材料量」重算,越接近定论越优先验证', suggestion: 'none' },
|
|
183
286
|
{ name: 'tickIntervalMs', type: 'integer', description: '调度器心跳间隔(毫秒):多久扫描一次子代理状态并推进(越小越灵敏、越大越省资源)', suggestion: 2000 },
|
|
184
|
-
{ name: 'activityLogCap', type: 'integer', description: '活动日志保留条数(影响 report
|
|
287
|
+
{ name: 'activityLogCap', type: 'integer', description: '活动日志保留条数(影响 status/report 里 recentActivity 的细节量,两者最多显示 ' + ACTIVITY_REPORT_MAX + ' 条)', suggestion: 100 },
|
|
185
288
|
{ name: 'maxExplorerRetries', type: 'integer', description: 'explorer 拆方向失败的重派生上限(达到后该问题标记为方向耗尽)', suggestion: 3 },
|
|
186
289
|
{ name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证档位:off=不额外要求(默认,提示词里不出现 Lean);encourage=鼓励按实现难度自行形式化,一旦 Lean 通过则验证重点转为「忠实性审查」;require=同 encourage 且加门禁——对象的 formal.status 未达到 passed/blocked 前,真/假裁定记为未定论(原因 formal-required)并写入 Formal/TODO.md', suggestion: 'off' },
|
|
187
290
|
{ name: 'leanCommand', type: 'string', description: '要执行的 Lean 可执行文件(默认 lean;用 lake 时配合 leanArgs=["env","lean"])', suggestion: 'lean' },
|
|
188
291
|
{ name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加命令行参数(默认空)', suggestion: [] },
|
|
189
292
|
{ name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000)', suggestion: 120000 },
|
|
293
|
+
{ name: 'finalPaper', type: 'boolean', description: '收口(严格终止)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已检验通过的命题/解法/成果整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
|
|
294
|
+
{ name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md;tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
|
|
295
|
+
{ name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
|
|
296
|
+
{ name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志', suggestion: true },
|
|
297
|
+
{ name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。该命令解析不到时按"未检测到"降级(只留 tex+md)', suggestion: '' },
|
|
190
298
|
]
|
|
191
299
|
|
|
192
300
|
// ================= fs =================
|
|
@@ -263,6 +371,20 @@ export function apply(ctx) {
|
|
|
263
371
|
return { ok: outcome.exitCode === 0, exitCode: outcome.exitCode }
|
|
264
372
|
} catch (e) { return { ok: false, error: String((e && e.message) || e) } }
|
|
265
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* `runShell` 已经**明确告诉我们失败原因**('no-subprocess' / spawn 异常 / 非零退出码),但它的
|
|
376
|
+
* 返回值此前无人查看:没有 subprocess 服务的宿主上目录树根本不会建、归档证明的删除静默降级,而
|
|
377
|
+
* 日志里一个字都没有——"mkdir 失败"和"mkdir 成功"完全无法区分(审计 H5)。
|
|
378
|
+
* 这里把每个调用点的失败**如实记一次**(按 调用点+原因 去重,避免每 tick 刷屏)。
|
|
379
|
+
*/
|
|
380
|
+
const shellWarned = {}
|
|
381
|
+
function warnShellOnce(where, r) {
|
|
382
|
+
const why = (r && (r.error || (r.exitCode === undefined ? undefined : ('exit code ' + r.exitCode)))) || 'unknown failure'
|
|
383
|
+
const key = String(where) + '|' + why
|
|
384
|
+
if (shellWarned[key]) return
|
|
385
|
+
shellWarned[key] = true
|
|
386
|
+
console.error('vibe-math-v2: ' + where + ' failed (' + why + ')')
|
|
387
|
+
}
|
|
266
388
|
// 目录布局(docs/formal-verification.md §3):
|
|
267
389
|
// 项目内:Formal/(对象形式化工作文件 + Index.md + TODO.md)、Verified/Lean/(归档证明)
|
|
268
390
|
// 全局(**不在项目内**,跨项目复用):<VibeMath 根>/Formal/Lib/(可复用定义)、Formal/Proved/(已证引理)
|
|
@@ -270,9 +392,16 @@ export function apply(ctx) {
|
|
|
270
392
|
const base = frameworkRoot()
|
|
271
393
|
const dirs = ['qs', 'Propos', 'Reliable', 'Verified', 'Verified/Lean', 'Verification_logs', 'Progress_Logs', 'VibeMath_State', 'Formal']
|
|
272
394
|
const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d }))
|
|
273
|
-
|
|
395
|
+
const r = await runShell(mkdirCmd(paths))
|
|
396
|
+
if (!r || !r.ok) warnShellOnce('ensureDirs (mkdir ' + base + ')', r)
|
|
397
|
+
return r
|
|
398
|
+
}
|
|
399
|
+
async function removeFile(rel) {
|
|
400
|
+
const base = frameworkRoot()
|
|
401
|
+
const r = await runShell(rmCmd(base + '/' + rel))
|
|
402
|
+
if (!r || !r.ok) warnShellOnce('removeFile(' + rel + ')', r)
|
|
403
|
+
return r
|
|
274
404
|
}
|
|
275
|
-
async function removeFile(rel) { const base = frameworkRoot(); return await runShell(rmCmd(base + '/' + rel)) }
|
|
276
405
|
|
|
277
406
|
// ================= settings =================
|
|
278
407
|
function sanitizeParams(obj) {
|
|
@@ -307,12 +436,17 @@ export function apply(ctx) {
|
|
|
307
436
|
else if (k === 'priorityAdjust') { out[k] = (v === 'none' || v === 'deadend-deprioritize' || v === 'survival-map') ? v : DEFAULT_PARAMS[k] }
|
|
308
437
|
else if (k === 'proposPriorityAdjust') { out[k] = (v === 'none' || v === 'progress-graded') ? v : DEFAULT_PARAMS[k] }
|
|
309
438
|
else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
|
|
439
|
+
else if (k === 'finalPaper' || k === 'paperCompilePdf') { out[k] = (v === true || v === false) ? v : DEFAULT_PARAMS[k] }
|
|
440
|
+
else if (k === 'paperFormat') { out[k] = (v === 'both' || v === 'md' || v === 'tex') ? v : DEFAULT_PARAMS[k] }
|
|
441
|
+
else if (k === 'paperLanguage') { out[k] = (v === 'zh' || v === 'en') ? v : DEFAULT_PARAMS[k] }
|
|
310
442
|
else { out[k] = v }
|
|
311
443
|
}
|
|
312
444
|
// 非正的 leanTimeoutMs 会让每次 Lean 运行立即"超时",而超时被记成"未通过"——静默失效。
|
|
313
445
|
// 删除该键(回退默认),而不是抬到最小值,与 v5/v4 的既有处理一致。
|
|
314
446
|
if (out.leanTimeoutMs !== undefined && !(out.leanTimeoutMs > 0)) delete out.leanTimeoutMs
|
|
315
447
|
if (out.leanTimeoutMs !== undefined) out.leanTimeoutMs = Math.max(1000, out.leanTimeoutMs)
|
|
448
|
+
// 空白的 paperLatexCommand 会让 resolveExecutable('') 直接失败;回退默认(自动探测)。
|
|
449
|
+
if (out.paperLatexCommand !== undefined && !String(out.paperLatexCommand).trim()) out.paperLatexCommand = DEFAULT_PARAMS.paperLatexCommand
|
|
316
450
|
// 未知档位一律回退 'off'(无操作),绝不回退到更强的档位:一个拼写错误若静默启用强制形式化,
|
|
317
451
|
// 会让所有结论被门禁拦下。
|
|
318
452
|
if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = DEFAULT_PARAMS.formalVerify
|
|
@@ -379,13 +513,30 @@ export function apply(ctx) {
|
|
|
379
513
|
* 从 registry 长度推导后,两者不可能不一致,"丢一次 end 就永久停摆"这一类故障从根上消失。
|
|
380
514
|
*/
|
|
381
515
|
function activeCount() { return Object.keys(agentRegistry).length }
|
|
516
|
+
/**
|
|
517
|
+
* 只保留**稳定身份键**('m:…')的准确率记录:老版本按一次性的 childId 记的那份数据是自指的
|
|
518
|
+
* (拿聚合值给同一批评审打分),既无意义又会随历史子代理数无界增长(审计 H4/M13)。
|
|
519
|
+
*/
|
|
520
|
+
function sanitizeAccuracy(va) {
|
|
521
|
+
const out = {}
|
|
522
|
+
if (!va || typeof va !== 'object' || Array.isArray(va)) return out
|
|
523
|
+
for (const k of Object.keys(va)) {
|
|
524
|
+
if (k.indexOf('m:') !== 0) continue
|
|
525
|
+
const rec = va[k]
|
|
526
|
+
if (!rec || typeof rec !== 'object') continue
|
|
527
|
+
const correct = Number(rec.correct); const total = Number(rec.total)
|
|
528
|
+
if (!Number.isFinite(correct) || !Number.isFinite(total) || total < 0) continue
|
|
529
|
+
out[k] = { correct: Math.max(0, correct), total: Math.max(0, total) }
|
|
530
|
+
}
|
|
531
|
+
return out
|
|
532
|
+
}
|
|
382
533
|
async function loadState() {
|
|
383
534
|
const s = await readJson('VibeMath_State/scheduler_state.json')
|
|
384
535
|
// 丢弃历史持久化的 activeCount:旧值可能已经漂移,绝不能覆盖推导值(见 activeCount())。
|
|
385
536
|
if (s) { const restored = Object.assign({}, s); delete restored.activeCount; scheduler = Object.assign({}, scheduler, restored) }
|
|
386
537
|
const r = await readJson('VibeMath_State/agent_registry.json'); if (r) agentRegistry = r
|
|
387
538
|
const dq = await readJson('VibeMath_State/decision_queue.json'); if (dq) decisionQueue = dq
|
|
388
|
-
const va = await readJson('VibeMath_State/verifier_accuracy.json'); if (va) verifierAccuracy = va
|
|
539
|
+
const va = await readJson('VibeMath_State/verifier_accuracy.json'); if (va) verifierAccuracy = sanitizeAccuracy(va)
|
|
389
540
|
const tk = await readJson('VibeMath_State/tasks.json'); if (tk) tasks = tk
|
|
390
541
|
const er = await readJson('VibeMath_State/explorer_retries.json'); if (er) explorerRetries = er
|
|
391
542
|
// 形式化记录(docs/formal-verification.md §4):v2 没有会话投影,这条状态必须自己持久化,
|
|
@@ -637,6 +788,9 @@ export function apply(ctx) {
|
|
|
637
788
|
* 调用会把 `objectId: 'r-pAlias'` 写进记录,之后(任务表已不在内存时)这个错误的锚点反而会覆盖
|
|
638
789
|
* 正确的后缀解析结果。
|
|
639
790
|
*/
|
|
791
|
+
// 已加载的对象 id 集合(qs.id + propos.id),供 formalObjectIdOf 的后缀歧义消解使用(审计 L19)。
|
|
792
|
+
// 只增不清:一次读库把对象记进来,即使该对象随后被删/重命名,最坏也只是多保留一个字符串。
|
|
793
|
+
const knownObjectIds = new Set()
|
|
640
794
|
function formalObjectIdOf(id) {
|
|
641
795
|
const t = safeId(String(id == null ? '' : id))
|
|
642
796
|
const task = tasks['verify:' + t]
|
|
@@ -647,7 +801,16 @@ export function apply(ctx) {
|
|
|
647
801
|
const rec = formalRecords()[t]
|
|
648
802
|
if (rec && rec.objectId && formalObjectIdOfIsOwner(rec.objectId)) return safeId(rec.objectId)
|
|
649
803
|
const m = /^r-(.+?)(?:-(?:s\d+|pf\d+|rf\d+))?$/.exec(t)
|
|
650
|
-
|
|
804
|
+
if (!m) return t
|
|
805
|
+
// 后缀剥离是**有损**映射:`r-pAmb-s1` 既可能是"对象 pAmb 的第 1 个解法",也可能是"对象 pAmb-s1"
|
|
806
|
+
// 本身(对象 id 允许以 `-sN`/`-pfN`/`-rfN` 结尾)。惰性匹配总是剥掉后缀,于是后一种情形会被
|
|
807
|
+
// 解析成同前缀的**邻居对象**——一次 defect 就会降级邻居、并撤回它自己的归档证明(审计 L19)。
|
|
808
|
+
// 这里只在"完整串确实是已加载的对象 id"时才优先完整串;否则保持原有的后缀剥离(legacy 记录没有
|
|
809
|
+
// objectId 时这是唯一线索)。knownObjectIds 由 getQs/getPropos 在每次读库时刷新,未读过库时为空
|
|
810
|
+
// ⇒ 行为与从前完全一致。
|
|
811
|
+
const full = t.slice(2)
|
|
812
|
+
if (m[1] !== full && knownObjectIds.has(full)) return full
|
|
813
|
+
return m[1]
|
|
651
814
|
}
|
|
652
815
|
/** 一个 id 能否作为"对象 id"落进记录:它不能再被解析成别的 id(对象 id 不以 `r-` 开头)。 */
|
|
653
816
|
function formalObjectIdOfIsOwner(v) {
|
|
@@ -1147,7 +1310,7 @@ export function apply(ctx) {
|
|
|
1147
1310
|
}
|
|
1148
1311
|
|
|
1149
1312
|
// ================= data layer: qs.json =================
|
|
1150
|
-
async function getQs() { const a = await readJson('qs/qs.json');
|
|
1313
|
+
async function getQs() { const a = await readJson('qs/qs.json'); const list = Array.isArray(a) ? a : []; for (const q of list) { if (q && q.id) knownObjectIds.add(safeId(String(q.id))) } return list }
|
|
1151
1314
|
async function writeQs(list) { await writeJson('qs/qs.json', list) }
|
|
1152
1315
|
async function findQ(qid) { const qs = await getQs(); return qs.find(function (q) { return q.id === qid }) }
|
|
1153
1316
|
|
|
@@ -1174,7 +1337,7 @@ export function apply(ctx) {
|
|
|
1174
1337
|
const fname = files[i]
|
|
1175
1338
|
const cat = fname.replace(/_Propos\.json$/i, '')
|
|
1176
1339
|
const list = await readProposCategory(cat)
|
|
1177
|
-
for (let j = 0; j < list.length; j++) { list[j]._category = cat; out.push(list[j]) }
|
|
1340
|
+
for (let j = 0; j < list.length; j++) { list[j]._category = cat; if (list[j] && list[j].id) knownObjectIds.add(safeId(String(list[j].id))); out.push(list[j]) }
|
|
1178
1341
|
}
|
|
1179
1342
|
return out
|
|
1180
1343
|
}
|
|
@@ -1212,7 +1375,7 @@ export function apply(ctx) {
|
|
|
1212
1375
|
propositions: { total: propos.length, resolved: propos.filter(function (p) { return p.布尔估计 === 1 || p.布尔估计 === 0 }).length },
|
|
1213
1376
|
pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),
|
|
1214
1377
|
registeredAgents: Object.keys(agentRegistry).length,
|
|
1215
|
-
recentActivity: activityLog.slice(-Math.min(
|
|
1378
|
+
recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)),
|
|
1216
1379
|
// Lean 形式化:可调档位与开关同处可读参数表(契约 §1),并附当前形式化记录概况。
|
|
1217
1380
|
formal: {
|
|
1218
1381
|
mode: formalMode(), required: formalRequired(),
|
|
@@ -1249,7 +1412,10 @@ export function apply(ctx) {
|
|
|
1249
1412
|
',问题 ' + report.problems.solved + '/' + report.problems.total + ' 已解决,命题 ' + report.propositions.resolved + '/' + report.propositions.total + ' 已定论,' +
|
|
1250
1413
|
'活跃代理轮数=' + report.activeCount + ',待人工决策=' + report.pendingDecisions.length + '。' +
|
|
1251
1414
|
'请调用 vibe_math_report 汇总当前进展及各代理状态,并用人话简要汇报(不打断用户,简短即可)。'
|
|
1252
|
-
|
|
1415
|
+
// 来源 kind 必须是**已声明**的:`MessageSourceMap` 是 merge-extensible 的联合,但没有共享的
|
|
1416
|
+
// catch-all `plugin` kind(dsh-llm message.d.ts),{kind:'plugin'} 是契约外形状。role 本来就是
|
|
1417
|
+
// 'user',正文也自带 "[Vibe Math V2] 进度更新" 的真署名,所以用核心声明的 {kind:'user'}。
|
|
1418
|
+
rootAgent.followup({ id: uuid(), role: 'user', content: [textBlock(text)], source: { kind: 'user' } })
|
|
1253
1419
|
lastPushReport = now()
|
|
1254
1420
|
} catch (e) {
|
|
1255
1421
|
console.error('vibe-math-v2: push report failed: ' + String((e && e.message) || e))
|
|
@@ -1259,28 +1425,13 @@ export function apply(ctx) {
|
|
|
1259
1425
|
// ================= child spawn / followup =================
|
|
1260
1426
|
function pickProvider() { try { const names = subagents.list ? subagents.list() : []; if (names.indexOf('spawn') !== -1) return 'spawn'; if (names.indexOf('fork') !== -1) return 'fork' } catch (e) {} return 'spawn' }
|
|
1261
1427
|
function childAgentOptions() { const o = {}; try { if (rootAgent && rootAgent.options) { if (rootAgent.options.provider) o.provider = rootAgent.options.provider; if (rootAgent.options.model) o.model = rootAgent.options.model } } catch (e) {} if (params.provider) o.provider = params.provider; if (params.model) o.model = params.model; return o }
|
|
1262
|
-
// Tool names for the permission filter, taken from the names the host ACTUALLY
|
|
1263
|
-
// registers (dsh-tool-web registers 'web_search'/'web_fetch'; 'web'/'fetch' are
|
|
1264
|
-
// only presentation card/kind fields, not tool names), and split by platform
|
|
1265
|
-
// because each preset's composition gates them:
|
|
1266
|
-
// dsh-tool-bash disabled: process.platform === 'win32'
|
|
1267
|
-
// dsh-tool-pwsh disabled: process.platform !== 'win32'
|
|
1268
|
-
// dsh-tools' restrict() THROWS on any name outside its registered set, and the
|
|
1269
|
-
// host applies the filter when establishing a continuable child
|
|
1270
|
-
// (dsh-subagent: childCtx.tools.restrict(...)), so a stale name meant the child
|
|
1271
|
-
// was never created at all.
|
|
1272
|
-
const IS_WINDOWS = process.platform === 'win32'
|
|
1273
|
-
const SCRIPT_TOOLS = IS_WINDOWS ? ['pwsh'] : ['bash']
|
|
1274
|
-
// 'web_fetch' is only registered when the composition enables fetch (the v4
|
|
1275
|
-
// preset sets `fetch: false`), so it is a candidate that sanitizeToolFilter drops.
|
|
1276
|
-
const NETWORK_TOOLS = ['web_search', 'web_fetch']
|
|
1277
1428
|
/**
|
|
1278
1429
|
* Drop filter names this host does not register. `known` comes from the host's
|
|
1279
1430
|
* own rejection message, which lists every registered global tool, so this
|
|
1280
1431
|
* never guesses. Returns undefined when nothing usable remains.
|
|
1281
1432
|
*/
|
|
1282
1433
|
/** The host names the offending tools and then lists the registered ones. */
|
|
1283
|
-
function buildToolFilter(role) { const allow = role === 'solver' ? params.solverToolAllow : role === 'verifier' ? params.verifierToolAllow : undefined; const deny = role === 'solver' ? params.solverToolDeny : role === 'verifier' ? params.verifierToolDeny : undefined; const net = role === 'solver' ? params.solverAllowNetwork : role === 'verifier' ? params.verifierAllowNetwork : undefined; const scr = role === 'solver' ? params.solverAllowScripts : role === 'verifier' ? params.verifierAllowScripts : undefined; let a = Array.isArray(allow) ? allow.slice() : []; let d = Array.isArray(deny) ? deny.slice() : []; if (net === false) d = d.concat(
|
|
1434
|
+
function buildToolFilter(role) { const allow = role === 'solver' ? params.solverToolAllow : role === 'verifier' ? params.verifierToolAllow : undefined; const deny = role === 'solver' ? params.solverToolDeny : role === 'verifier' ? params.verifierToolDeny : undefined; const net = role === 'solver' ? params.solverAllowNetwork : role === 'verifier' ? params.verifierAllowNetwork : undefined; const scr = role === 'solver' ? params.solverAllowScripts : role === 'verifier' ? params.verifierAllowScripts : undefined; let a = Array.isArray(allow) ? allow.slice() : []; let d = Array.isArray(deny) ? deny.slice() : []; const netTools = composedToolList(NETWORK_TOOLS); const scrTools = composedToolList(SCRIPT_TOOLS); if (net === false) d = d.concat(netTools); else if (net === true && a.length > 0) a = a.concat(netTools); if (scr === false) d = d.concat(scrTools); else if (scr === true && a.length > 0) a = a.concat(scrTools); const f = {}; if (a.length > 0) f.allow = a; if (d.length > 0) f.deny = d; return (f.allow || f.deny) ? f : undefined }
|
|
1284
1435
|
async function spawnChild(label, promptText, meta) {
|
|
1285
1436
|
const request = { prompt: [textBlock(promptText)], parent: rootAgent, agentOptions: childAgentOptions() }
|
|
1286
1437
|
const tf = buildToolFilter(meta && meta.role); if (tf) request.toolFilter = tf
|
|
@@ -1319,7 +1470,10 @@ export function apply(ctx) {
|
|
|
1319
1470
|
throw e2
|
|
1320
1471
|
}
|
|
1321
1472
|
}
|
|
1322
|
-
|
|
1473
|
+
const registered = Object.assign({ createdAt: now() }, meta || {})
|
|
1474
|
+
// 验证者的**稳定身份**随 meta 一起登记:forced 模式的历史准确率必须能跨 child 复用(审计 H4)。
|
|
1475
|
+
if (registered.role === 'verifier' && !registered.verificationKey) registered.verificationKey = verifierIdentityKey()
|
|
1476
|
+
agentRegistry[started.childId] = registered
|
|
1323
1477
|
childOwner.set(started.childId, sessionId)
|
|
1324
1478
|
// 并发计数由 agentRegistry 推导,此处无需手工 +1(见 activeCount())。
|
|
1325
1479
|
await saveAll(); return started.childId
|
|
@@ -1389,9 +1543,9 @@ export function apply(ctx) {
|
|
|
1389
1543
|
if (netOn !== false) toolParts.push('web search / literature lookup')
|
|
1390
1544
|
if (scrOn !== false) toolParts.push('symbolic/numeric computation (running scripts)')
|
|
1391
1545
|
let t = '\nYOUR PERMISSIONS / CAPABILITIES:\n'
|
|
1392
|
-
t += '- Network tools (web search / fetch): ' + (netOn === false ? 'DISABLED for you' : 'available') + '; Script/shell tools (
|
|
1546
|
+
t += '- Network tools (web search / fetch): ' + (netOn === false ? 'DISABLED for you' : 'available') + '; Script/shell tools (' + SCRIPT_TOOLS.join('/') + '): ' + (scrOn === false ? 'DISABLED for you' : 'available') + ' (your actual tool list is enforced by the framework).\n'
|
|
1393
1547
|
t += toolParts.length > 0
|
|
1394
|
-
? ('- You may use external tools (' + toolParts.join(', ') + ') to assist; ' + ((maxCalls && Number(maxCalls) > 0) ? ('
|
|
1548
|
+
? ('- You may use external tools (' + toolParts.join(', ') + ') to assist; ' + ((maxCalls && Number(maxCalls) > 0) ? ('as a guideline, keep external tool calls to about ' + maxCalls + ' this round (advisory: the framework does not enforce a hard quota).\n') : 'no per-round limit by default.\n'))
|
|
1395
1549
|
: '- External tools: none enabled for you this round.\n'
|
|
1396
1550
|
t += '- You may READ any file under Verified/ as a known, trusted dependency (resolved facts).\n'
|
|
1397
1551
|
t += '- You should BASE your reasoning on the existing knowledge under Propos/ (propositions with proofs/refutations and probabilities) and Reliable/ (trusted references).\n'
|
|
@@ -1510,12 +1664,23 @@ export function apply(ctx) {
|
|
|
1510
1664
|
}
|
|
1511
1665
|
return '\n' + L.join('\n') + '\n'
|
|
1512
1666
|
}
|
|
1667
|
+
/**
|
|
1668
|
+
* 需求 5:依赖临时假设的结论必须把假设**显式**写出来("若 p_{q-tmp} 成立,则…")。记录在命题上
|
|
1669
|
+
* (`依赖假设`)之后,还要把它**送到验证者眼前**——否则验证者会在不知道前提未定的情况下裁决它。
|
|
1670
|
+
*/
|
|
1671
|
+
function assumptionNote(r) {
|
|
1672
|
+
if (!r || !r.依赖假设) return ''
|
|
1673
|
+
return '\nDEPENDENCY: this conclusion is stated UNDER the temporary assumption ' + r.依赖假设
|
|
1674
|
+
+ '(' + (r.依赖假设已证伪 ? '该假设**已被证伪**:依赖它的结论必须重新审视' : '该假设尚未定论,0 < 布尔估计 < 1') + ')。'
|
|
1675
|
+
+ ' The assumption is a SEPARATE object: a verdict on this conclusion is NOT a verdict on the assumption.\n'
|
|
1676
|
+
}
|
|
1513
1677
|
function verifierReviewPrompt(r) {
|
|
1514
1678
|
let target = ''
|
|
1515
1679
|
if (r.kind === 'proposition') target = 'PROPOSITION (id: ' + r.pId + '): ' + r.概述
|
|
1516
1680
|
else if (r.kind === 'prop-proof') target = 'PROPOSITION (id: ' + r.pId + '): ' + r.概述 + '\n' + r.side + ' PROCESS TO CHECK:\n' + r.process
|
|
1517
1681
|
else target = 'PROBLEM (id: ' + r.qid + '): ' + r.概述 + '\nSOLUTION TO CHECK:\n' + r.process
|
|
1518
1682
|
return verifierPersonaText() + 'You are a STRICT peer reviewer verifying one mathematical object. Check it multiple times.\n\nTARGET (r: ' + r.kind + '):\n' + target + '\n' +
|
|
1683
|
+
assumptionNote(r) +
|
|
1519
1684
|
knowledgeContextText() +
|
|
1520
1685
|
capabilitiesText('verifier') +
|
|
1521
1686
|
formalVerifySection(r) +
|
|
@@ -1529,6 +1694,7 @@ export function apply(ctx) {
|
|
|
1529
1694
|
else if (r.kind === 'prop-proof') target = 'PROPOSITION (id: ' + r.pId + '): ' + r.概述 + '\n' + r.side + ' PROCESS TO CHECK:\n' + r.process
|
|
1530
1695
|
else target = 'PROBLEM (id: ' + r.qid + '): ' + r.概述 + '\nSOLUTION TO CHECK:\n' + r.process
|
|
1531
1696
|
return verifierPersonaText() + 'You are one reviewer in a DEBATE ("交流群") about this object.\n\nTARGET:\n' + target + '\n' +
|
|
1697
|
+
assumptionNote(r) +
|
|
1532
1698
|
knowledgeContextText() +
|
|
1533
1699
|
capabilitiesText('verifier') +
|
|
1534
1700
|
formalVerifySection(r) +
|
|
@@ -1556,10 +1722,29 @@ export function apply(ctx) {
|
|
|
1556
1722
|
} catch (e) { console.error('vibe-math-v2: spawn reject mark failed: ' + String((e && e.message) || e)) }
|
|
1557
1723
|
return { spawned: false, rejected: true }
|
|
1558
1724
|
}
|
|
1559
|
-
if (node === 'verdict') { const overridden = resolution.action === 'override' && (resolution.verdict === 1 || resolution.verdict === 0); const v = overridden ? Number(resolution.verdict) : data.verdict; await settleVerdict(data.task, v); delete tasks[data.task.id]; return { verdict: v, overridden: overridden } }
|
|
1725
|
+
if (node === 'verdict') { const overridden = resolution.action === 'override' && (resolution.verdict === 1 || resolution.verdict === 0); const v = overridden ? Number(resolution.verdict) : data.verdict; const applied = await settleVerdict(data.task, v); delete tasks[data.task.id]; return { verdict: v, overridden: overridden, applied: applied } }
|
|
1560
1726
|
return {}
|
|
1561
1727
|
}
|
|
1562
1728
|
async function resolveDecision(id, resolution) { const d = decisionQueue.find(function (x) { return x.id === id }); if (!d) return { ok: false, message: 'decision not found' }; if (d.status !== 'pending') return { ok: false, message: 'decision already resolved' }; d.status = 'resolved'; d.resolution = resolution; if (scheduler.gate && scheduler.gate.decisionId === id) scheduler.gate = null; logActivity('decide', id + ' resolved: ' + resolution.action + (resolution.verdict !== undefined ? ' ' + resolution.verdict : '')); await saveAll(); scheduleTick(); return { ok: true, message: 'decision resolved' } }
|
|
1729
|
+
/**
|
|
1730
|
+
* 清掉 gate 时**必须结清它指向的那个人工决策**(审计 M10)。start/resume/abort/切项目都会离开
|
|
1731
|
+
* 需要那次决策的运行,而决策若仍是 `pending`,它会永远留在 `vibe_math_list_decisions` 里反复出现——
|
|
1732
|
+
* 一个已经无人等待的节点看起来仍然等待人工输入。与 `dropStaleGate` 互补:那条处理"决策没了但 gate
|
|
1733
|
+
* 还在",这条处理"gate 没了但决策还在"。
|
|
1734
|
+
* 注意 `pauseScheduler` **不**清 gate:暂停期间那次决策仍然有效(人还可以 vibe_math_decide),
|
|
1735
|
+
* 只有真正离开它(resume/abort/重启/换项目)才作废;作废是终态,不会再回到 pending。
|
|
1736
|
+
*/
|
|
1737
|
+
function abandonGatedDecision(why) {
|
|
1738
|
+
const g = scheduler.gate
|
|
1739
|
+
scheduler.gate = null
|
|
1740
|
+
if (!g) return
|
|
1741
|
+
const d = decisionQueue.find(function (x) { return x.id === g.decisionId })
|
|
1742
|
+
if (d && d.status === 'pending') {
|
|
1743
|
+
d.status = 'resolved'
|
|
1744
|
+
d.resolution = { action: 'abandoned', reason: why }
|
|
1745
|
+
logActivity('gate', 'gated decision ' + g.node + '/' + d.id + ' 已作废(' + why + ')——不再挂起等待人工输入')
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1563
1748
|
|
|
1564
1749
|
// ================= scheduler core =================
|
|
1565
1750
|
function scheduleTick() { tick().catch(function (e) { console.error('vibe-math-v2 tick error: ' + String((e && e.stack) || e)) }) }
|
|
@@ -1590,11 +1775,18 @@ export function apply(ctx) {
|
|
|
1590
1775
|
await maybeWriteReport(false)
|
|
1591
1776
|
const qs = await getQs()
|
|
1592
1777
|
const unsolved = qs.filter(function (q) { return !q.已解决 && q.优先级 !== 'never' })
|
|
1593
|
-
|
|
1594
|
-
|
|
1778
|
+
// 收口判据(spec v2 §A2/§A3):**排除**论文撰写子代理——它自己不能阻止收口,也不该被算作"在途工作"。
|
|
1779
|
+
const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'paper' }).length
|
|
1780
|
+
if (unsolved.length === 0 && realAgents === 0 && Object.keys(tasks).length === 0) {
|
|
1595
1781
|
logActivity('stop', 'all active problems solved (never-priority problems excluded) and no active agents/tasks — scheduler stopped (strict termination)')
|
|
1782
|
+
// ★ 最终论文(修订 §A2):**必须在 running=false 之前**派遣——停止之后 scheduleTick() 是空操作,
|
|
1783
|
+
// 而且 v2 没有任何激活上限处理,被拒的撰写者会静默消失;派不出去就排队,由 apply 级心跳重试。
|
|
1784
|
+
if (params.finalPaper !== false) {
|
|
1785
|
+
try { await maybeWritePaper('auto') } catch (e) { logActivity('paper', '自动撰写最终论文失败:' + String((e && e.message) || e)) }
|
|
1786
|
+
}
|
|
1787
|
+
scheduler.running = false
|
|
1596
1788
|
await saveAll(); await maybeWriteReport(true); await maybePushReport(true)
|
|
1597
|
-
} else if (
|
|
1789
|
+
} else if (realAgents === 0 && Object.keys(tasks).length === 0 && unsolved.length > 0) {
|
|
1598
1790
|
let allBlocked = true
|
|
1599
1791
|
for (let i = 0; i < unsolved.length; i++) {
|
|
1600
1792
|
const prog = parseProgress(unsolved[i])
|
|
@@ -1648,6 +1840,8 @@ export function apply(ctx) {
|
|
|
1648
1840
|
cardWritten = cardOk(card)
|
|
1649
1841
|
}
|
|
1650
1842
|
if (targetBE !== null && p.布尔估计 !== targetBE) { p.布尔估计 = targetBE; pChanged = true }
|
|
1843
|
+
// 这个对象**现在**取得了布尔定论:用它给此前那些还没被检验过的评审回溯计分(forced 权重)。
|
|
1844
|
+
if (targetBE !== null) await scorePendingReviews(p.id, targetBE)
|
|
1651
1845
|
if ((p.布尔估计 === 1 || p.布尔估计 === 0) && p.优先级 !== 'never') { p.优先级 = 'never'; pChanged = true }
|
|
1652
1846
|
if (cardWritten) pChanged = true
|
|
1653
1847
|
if (p.布尔估计 === 1 || p.布尔估计 === 0) {
|
|
@@ -1659,6 +1853,21 @@ export function apply(ctx) {
|
|
|
1659
1853
|
else if (String((qj.progress && typeof qj.progress === 'object' ? (qj.progress.来源命题 || '') : qj.progress) || '').indexOf(p.id) !== -1) { qj.已解决 = true; qj.优先级 = 'never'; closedPromoted = true }
|
|
1660
1854
|
}
|
|
1661
1855
|
}
|
|
1856
|
+
// 需求 5 第二半:「若该假设被证伪,则依赖它的主线结论需重新审视」。假设的布尔估计被判定为 0 时,
|
|
1857
|
+
// 把依赖它的结论标记出来并**重新放回验证候选**(清掉 已验 / 冷却时间戳),否则那句承诺只是一句
|
|
1858
|
+
// 空话——依赖关系记下了却没人据此行动(审计 M16)。
|
|
1859
|
+
if (p.布尔估计 === 0) {
|
|
1860
|
+
for (let j = 0; j < propos.length; j++) {
|
|
1861
|
+
const depP = propos[j]
|
|
1862
|
+
if (depP === p || depP.id === p.id) continue
|
|
1863
|
+
if (depP.依赖假设 !== p.id || depP.依赖假设已证伪) continue
|
|
1864
|
+
depP.依赖假设已证伪 = true
|
|
1865
|
+
depP.已验 = false
|
|
1866
|
+
depP.最近验证时间 = 0
|
|
1867
|
+
await upsertProposition(depP)
|
|
1868
|
+
logActivity('dependency', '假设 ' + p.id + ' 被证伪:依赖它的结论 ' + depP.id + ' 标记为需重新审视,已重新进入验证候选')
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1662
1871
|
if (pChanged) await upsertProposition(p)
|
|
1663
1872
|
}
|
|
1664
1873
|
if (closedPromoted) { await writeQs(qs); logActivity('update', 'promoted problems closed because their source proposition resolved') }
|
|
@@ -1733,8 +1942,18 @@ export function apply(ctx) {
|
|
|
1733
1942
|
return // one per tick is enough
|
|
1734
1943
|
}
|
|
1735
1944
|
}
|
|
1945
|
+
/**
|
|
1946
|
+
* 临时假设的「判断问题」是**辅助对象**(审计 C2 的种子解法让它可验证)。它的解法只在主线安静时
|
|
1947
|
+
* 入选验证:一个 q_sub 会新增两个问题,若它的验证与主线同时抢并发槽位,默认组合
|
|
1948
|
+
* (maxParallelThreshold=4 / verifierCount=3)下主线连 2 个验证器都派不出来——实测 e2e-business 的
|
|
1949
|
+
* "verifiers spawned for solution" 会从 3 掉到 1。辅助对象让主线先走;主线一旦没有别的可验证对象、
|
|
1950
|
+
* 也没有 explorer/solver 在跑,它立刻轮到(所以 p_{q-tmp} 不再永远停在 0.5)。
|
|
1951
|
+
*/
|
|
1952
|
+
function busySolving() { return Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && (m.role === 'explorer' || m.role === 'solver') }) }
|
|
1736
1953
|
async function processVerify() {
|
|
1737
|
-
const
|
|
1954
|
+
const all = await buildVerifyCandidates()
|
|
1955
|
+
let candidates = all.filter(function (c) { return !c.auxAssumption })
|
|
1956
|
+
if (candidates.length === 0 && !busySolving()) candidates = all.filter(function (c) { return c.auxAssumption })
|
|
1738
1957
|
for (let i = 0; i < candidates.length; i++) {
|
|
1739
1958
|
if (activeCount() >= params.maxParallelThreshold) break
|
|
1740
1959
|
const c = candidates[i]
|
|
@@ -1742,7 +1961,7 @@ export function apply(ctx) {
|
|
|
1742
1961
|
if (tasks['verify:' + rId]) continue
|
|
1743
1962
|
const inflight = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.role === 'verifier' && m.rId === rId })
|
|
1744
1963
|
if (inflight) continue
|
|
1745
|
-
tasks['verify:' + rId] = { id: 'verify:' + rId, type: 'verify', r: c, rId: rId, status: 'spawning', children: [], childResults: {}, history: [], round: 1, expectedCount: Math.max(
|
|
1964
|
+
tasks['verify:' + rId] = { id: 'verify:' + rId, type: 'verify', r: c, rId: rId, status: 'spawning', children: [], childResults: {}, history: [], round: 1, expectedCount: Math.max(MIN_REVIEWERS, params.verifierCount), createdAt: now() }
|
|
1746
1965
|
await saveAll()
|
|
1747
1966
|
return // one verification at a time keeps scheduling simple; tick will continue next pass
|
|
1748
1967
|
}
|
|
@@ -1756,31 +1975,49 @@ export function apply(ctx) {
|
|
|
1756
1975
|
const sols = q.解法列表 || []
|
|
1757
1976
|
for (let j = 0; j < sols.length; j++) {
|
|
1758
1977
|
const s = sols[j]
|
|
1759
|
-
if (s.正确概率 === 1 || s.正确概率 === 0
|
|
1978
|
+
if (s.正确概率 === 1 || s.正确概率 === 0) continue
|
|
1979
|
+
// 已验的条目只在**中段值**(0<p<1)且冷却期已过时重新入选(审计 M12:中间概率不是定论,
|
|
1980
|
+
// 对象不能被永久搁置;`最近验证时间` 由 settleVerdict 在中段裁决时写下)。
|
|
1981
|
+
if (s.已验 && !reverifyDue(s)) continue
|
|
1760
1982
|
if (!String(s.完整解法 || '').trim()) continue
|
|
1761
|
-
out.push({ rId: 'r-' + q.id + '-s' + j, kind: 'problem-solution', qid: q.id, 概述: q.概述, process: s.完整解法 || '', idx: j, prob: Number(s.正确概率) || 0, priority: q.优先级 === 'never' ? 999 : Number(q.优先级) })
|
|
1983
|
+
out.push({ rId: 'r-' + q.id + '-s' + j, kind: 'problem-solution', qid: q.id, 概述: q.概述, process: s.完整解法 || '', idx: j, prob: Number(s.正确概率) || 0, priority: q.优先级 === 'never' ? 999 : Number(q.优先级), 判断命题: q.判断命题, auxAssumption: s.来源 === 'sub-question-assumption' })
|
|
1762
1984
|
}
|
|
1763
1985
|
}
|
|
1764
1986
|
const propos = await getPropos()
|
|
1765
1987
|
for (let i = 0; i < propos.length; i++) {
|
|
1766
1988
|
const p = propos[i]
|
|
1767
1989
|
if (p.布尔估计 === 1 || p.布尔估计 === 0 || p.优先级 === 'never') continue
|
|
1768
|
-
if (p.已验证) continue //
|
|
1990
|
+
if (p.已验证 && !reverifyDue(p)) continue // 收敛闸门:该命题已由「判断命题」解法裁决过;中段裁决只上锁到冷却期结束(见 settleVerdict 点5)
|
|
1769
1991
|
if (p.在问题清单) continue // 已晋升:其证明/证伪经晋升问题的解法验证,避免同一内容双重验证
|
|
1992
|
+
const dep = { 依赖假设: p.依赖假设, 依赖假设已证伪: p.依赖假设已证伪 }
|
|
1770
1993
|
const proofs = p.证明列表 || []; const refutes = p.证伪列表 || []
|
|
1771
1994
|
if (proofs.length === 0 && refutes.length === 0) {
|
|
1772
1995
|
if (String(p.id).indexOf('p-tmp-') === 0) continue // 临时假设由「判断下述命题是否成立:p_{q-tmp}」问题统一验证,避免裸命题验证双重路径
|
|
1773
|
-
out.push({ rId: 'r-' + p.id, kind: 'proposition', pId: p.id, 概述: p.概述, prob: Number(p.布尔估计) || 0, priority: p.优先级 === 'never' ? 999 : Number(p.优先级) })
|
|
1996
|
+
out.push(Object.assign({ rId: 'r-' + p.id, kind: 'proposition', pId: p.id, 概述: p.概述, prob: Number(p.布尔估计) || 0, priority: p.优先级 === 'never' ? 999 : Number(p.优先级) }, dep))
|
|
1774
1997
|
} else {
|
|
1775
|
-
for (let j = 0; j < proofs.length; j++) { if (proofs[j].正确概率 === 1 || proofs[j].正确概率 === 0 || proofs[j].已验) continue; if (!String(proofs[j].完整过程 || '').trim()) continue; out.push({ rId: 'r-' + p.id + '-pf' + j, kind: 'prop-proof', pId: p.id, 概述: p.概述, side: '证明', process: proofs[j].完整过程 || '', idx: j, prob: Number(proofs[j].正确概率) || 0, priority: p.优先级 === 'never' ? 999 : Number(p.优先级) }) }
|
|
1776
|
-
for (let j = 0; j < refutes.length; j++) { if (refutes[j].正确概率 === 1 || refutes[j].正确概率 === 0 || refutes[j].已验) continue; if (!String(refutes[j].完整过程 || '').trim()) continue; out.push({ rId: 'r-' + p.id + '-rf' + j, kind: 'prop-proof', pId: p.id, 概述: p.概述, side: '证伪', process: refutes[j].完整过程 || '', idx: j, prob: Number(refutes[j].正确概率) || 0, priority: p.优先级 === 'never' ? 999 : Number(p.优先级) }) }
|
|
1998
|
+
for (let j = 0; j < proofs.length; j++) { if (proofs[j].正确概率 === 1 || proofs[j].正确概率 === 0 || (proofs[j].已验 && !reverifyDue(proofs[j]))) continue; if (!String(proofs[j].完整过程 || '').trim()) continue; out.push(Object.assign({ rId: 'r-' + p.id + '-pf' + j, kind: 'prop-proof', pId: p.id, 概述: p.概述, side: '证明', process: proofs[j].完整过程 || '', idx: j, prob: Number(proofs[j].正确概率) || 0, priority: p.优先级 === 'never' ? 999 : Number(p.优先级) }, dep)) }
|
|
1999
|
+
for (let j = 0; j < refutes.length; j++) { if (refutes[j].正确概率 === 1 || refutes[j].正确概率 === 0 || (refutes[j].已验 && !reverifyDue(refutes[j]))) continue; if (!String(refutes[j].完整过程 || '').trim()) continue; out.push(Object.assign({ rId: 'r-' + p.id + '-rf' + j, kind: 'prop-proof', pId: p.id, 概述: p.概述, side: '证伪', process: refutes[j].完整过程 || '', idx: j, prob: Number(refutes[j].正确概率) || 0, priority: p.优先级 === 'never' ? 999 : Number(p.优先级) }, dep)) }
|
|
1777
2000
|
}
|
|
1778
2001
|
}
|
|
1779
2002
|
out.sort(function (a, b) { if (a.priority !== b.priority) return a.priority - b.priority; return (b.prob || 0) - (a.prob || 0) })
|
|
1780
2003
|
return out
|
|
1781
2004
|
}
|
|
2005
|
+
/**
|
|
2006
|
+
* 每个验证对象最多**同时**占多少评审槽位(审计 M11 的资源侧)。
|
|
2007
|
+
*
|
|
2008
|
+
* 目标仍是 verifierCount 个独立评审,但并发预算必须给别的验证对象留下至少 MIN_REVIEWERS 个槽位:
|
|
2009
|
+
* 否则一个对象(例如某条引理的证明)就能把 maxParallelThreshold 吃光,后出现的对象(例如主问题的
|
|
2010
|
+
* 解法)只剩 1 票——而一票不允许裁决 ⇒ 它会被反复补派/搁置。容量宽裕时(maxParallelThreshold ≥
|
|
2011
|
+
* verifierCount + MIN_REVIEWERS)这就是 verifierCount,一个都不少。
|
|
2012
|
+
*/
|
|
2013
|
+
function reviewerCap() {
|
|
2014
|
+
const target = Math.max(MIN_REVIEWERS, Math.floor(Number(params.verifierCount) || MIN_REVIEWERS))
|
|
2015
|
+
const byThreshold = Math.max(MIN_REVIEWERS, (Number(params.maxParallelThreshold) || MIN_REVIEWERS) - MIN_REVIEWERS)
|
|
2016
|
+
return Math.max(MIN_REVIEWERS, Math.min(target, byThreshold))
|
|
2017
|
+
}
|
|
1782
2018
|
async function backfillVerifiers(t) {
|
|
1783
|
-
|
|
2019
|
+
const cap = reviewerCap()
|
|
2020
|
+
while (t.children.length < t.expectedCount && t.children.length < cap) {
|
|
1784
2021
|
if (activeCount() >= params.maxParallelThreshold) break
|
|
1785
2022
|
const index = t.children.length
|
|
1786
2023
|
const childId = await spawnChild('verifier:' + t.rId + ':' + index, verifierReviewPrompt(t.r), { role: 'verifier', rId: t.rId, round: 1, index: index })
|
|
@@ -1793,6 +2030,14 @@ export function apply(ctx) {
|
|
|
1793
2030
|
for (let i = 0; i < ids.length; i++) {
|
|
1794
2031
|
const t = tasks[ids[i]]
|
|
1795
2032
|
if (t.type !== 'verify') continue
|
|
2033
|
+
if (t.status === 'stalled') {
|
|
2034
|
+
// 票数补不足时任务被置为 stalled(见 finalizeVerification):冷却结束再重新派发,避免每 tick
|
|
2035
|
+
// 重建同一个对象、反复重试。任务本身保留,所以 processVerify 不会为同一个 rId 造出第二个任务。
|
|
2036
|
+
if (now() >= Number(t.retryAt || 0)) {
|
|
2037
|
+
t.status = 'spawning'; t.reviewerRespawn = 0; t.children = []; t.childResults = {}; t.round = 1
|
|
2038
|
+
logActivity('verify', t.rId + ' 冷却结束,重新派发验证器(凑齐 ≥' + MIN_REVIEWERS + ' 份独立评审)')
|
|
2039
|
+
} else continue
|
|
2040
|
+
}
|
|
1796
2041
|
if (t.status === 'paused') {
|
|
1797
2042
|
const allReported = t.children.length > 0 && t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === t.round })
|
|
1798
2043
|
if (allReported) { t.status = 'debating'; await advanceVerification(t, t.round); continue }
|
|
@@ -1871,12 +2116,17 @@ export function apply(ctx) {
|
|
|
1871
2116
|
if (!parsed && !scheduler.running) { delete agentRegistry[childId]; return } // abort:不把方向标记为死路,保留待 resume
|
|
1872
2117
|
const status = (parsed && parsed.status) || statusFromStop(stopReason)
|
|
1873
2118
|
dir.round = meta.round
|
|
2119
|
+
// 需求 5 第二半的**记录**侧:本轮产出的引理若处在某条临时假设之下,就把假设 id 记在命题上
|
|
2120
|
+
// (`依赖假设`)。只用**本轮之前**已登记、且尚未被证伪的假设:本轮新报的 q_sub 不算(先有假设
|
|
2121
|
+
// 才有"依赖它的结论",同一轮里 lemma 可能先于该假设产生)。此前 assumeId 只被存进
|
|
2122
|
+
// dir.sub_questions 而从不被读取,"若 p_{q-tmp} 成立,则…"就只是一句写在提示词里的空话(审计 M16)。
|
|
2123
|
+
const activeAssumptions = await activeAssumptionsOf(dir)
|
|
1874
2124
|
if (parsed) {
|
|
1875
2125
|
if (parsed.routes) dir.routes = (dir.routes || []).concat(parsed.routes)
|
|
1876
2126
|
if (parsed.lessons) dir.lessons = (dir.lessons || []).concat(parsed.lessons)
|
|
1877
2127
|
if (parsed.dead_end_reason) dir.dead_end_reason = parsed.dead_end_reason
|
|
1878
2128
|
if (typeof parsed.survival_probability === 'number') dir.survival = clamp01(parsed.survival_probability)
|
|
1879
|
-
if (parsed.lemmas && parsed.lemmas.length) { for (let i = 0; i < parsed.lemmas.length; i++) { const lid = await addLemmaAsProposition(qid, parsed.lemmas[i]); if (lid) { dir.lemmas = dir.lemmas || []; dir.lemmas.push({ id: lid, title: parsed.lemmas[i].title || '' }) } } }
|
|
2129
|
+
if (parsed.lemmas && parsed.lemmas.length) { for (let i = 0; i < parsed.lemmas.length; i++) { const lid = await addLemmaAsProposition(qid, parsed.lemmas[i], activeAssumptions.length ? activeAssumptions[activeAssumptions.length - 1] : undefined); if (lid) { dir.lemmas = dir.lemmas || []; dir.lemmas.push({ id: lid, title: parsed.lemmas[i].title || '' }) } } }
|
|
1880
2130
|
if (parsed.sub_questions && parsed.sub_questions.length) { for (let i = 0; i < parsed.sub_questions.length; i++) { const sq = parsed.sub_questions[i]; if (sq && sq.q_sub_statement && dir.sub_questions && dir.sub_questions.some(function (x) { return x.statement === sq.q_sub_statement })) continue; const rec = await addSubQuestion(qid, dirId, sq); if (rec) { dir.sub_questions = dir.sub_questions || []; dir.sub_questions.push(rec) } } }
|
|
1881
2131
|
}
|
|
1882
2132
|
if (status === 'success') {
|
|
@@ -1923,7 +2173,22 @@ export function apply(ctx) {
|
|
|
1923
2173
|
await saveProgress(qid, prog)
|
|
1924
2174
|
}
|
|
1925
2175
|
function statusFromStop(stopReason) { return (stopReason === 'completed' || stopReason === 'max-tokens') ? 'continue' : 'dead-end' }
|
|
1926
|
-
|
|
2176
|
+
/**
|
|
2177
|
+
* 某个方向当前**在力**的临时假设(p_{q-tmp})id 列表。已被证伪(布尔估计=0)的假设不再"在力":
|
|
2178
|
+
* 依赖它的结论按需求 5 必须重新审视,而不是继续挂在它下面。
|
|
2179
|
+
*/
|
|
2180
|
+
async function activeAssumptionsOf(dir) {
|
|
2181
|
+
const out = []
|
|
2182
|
+
const subs = (dir && Array.isArray(dir.sub_questions)) ? dir.sub_questions : []
|
|
2183
|
+
for (let i = 0; i < subs.length; i++) {
|
|
2184
|
+
const id = subs[i] && subs[i].assumeId
|
|
2185
|
+
if (!id) continue
|
|
2186
|
+
const p = await findProposition(id)
|
|
2187
|
+
if (p && p.布尔估计 !== 0) out.push(id)
|
|
2188
|
+
}
|
|
2189
|
+
return out
|
|
2190
|
+
}
|
|
2191
|
+
async function addLemmaAsProposition(qid, lemma, assumeId) {
|
|
1927
2192
|
if (!lemma || !lemma.title) return
|
|
1928
2193
|
let be = clamp01(lemma.布尔估计 != null ? lemma.布尔估计 : 0.6)
|
|
1929
2194
|
if (be >= 1) be = 0.99; else if (be <= 0) be = 0.01 // 写入时概率必须 <1 且 >0(待验证器验证)
|
|
@@ -1936,8 +2201,11 @@ export function apply(ctx) {
|
|
|
1936
2201
|
'价值/关键性': clamp01(lemma['价值/关键性'] != null ? lemma['价值/关键性'] : 0.5),
|
|
1937
2202
|
progress: { 来源: 'solver-lemma', 问题: qid, 说明: '由求解器针对问题 ' + qid + ' 的方向迭代产出。' }, 来源问题: qid,
|
|
1938
2203
|
}
|
|
2204
|
+
// 需求 5:「后续所得命题/结论中凡依赖该假设的,必须把假设作为前提显式写出」——这里把依赖**落库**
|
|
2205
|
+
// (`依赖假设` 字段),于是它可被 list_propositions / 验证提示词 / 证伪联动读取(审计 M16)。
|
|
2206
|
+
if (assumeId) p.依赖假设 = String(assumeId)
|
|
1939
2207
|
await upsertProposition(p)
|
|
1940
|
-
logActivity('proposition', 'lemma「' + lemma.title + '」→ ' + p.id)
|
|
2208
|
+
logActivity('proposition', 'lemma「' + lemma.title + '」→ ' + p.id + (assumeId ? '(依赖临时假设 ' + assumeId + ')' : ''))
|
|
1941
2209
|
return p.id
|
|
1942
2210
|
}
|
|
1943
2211
|
// 点5(q_sub 严格化):solver 报告子问题 q_sub 时,注册三个对象:
|
|
@@ -1960,7 +2228,18 @@ export function apply(ctx) {
|
|
|
1960
2228
|
const judgeId = qid + '-judge-' + shortId()
|
|
1961
2229
|
const assumeStatement = sq.assumption_statement || sq.assumption_title || ('对子问题「' + (sq.q_sub_title || sq.q_sub_statement) + '」的一种回答(临时假设)')
|
|
1962
2230
|
qs.push({ id: subId, 概述: sq.q_sub_statement, 已解决: false, 解法列表: [], 优先级: 1, progress: { 类型: 'sub-question', 来源问题: qid, 来源方向: dirId, 说明: '临时子问题:由问题 ' + qid + ' 方向 ' + dirId + ' 分支产生;求解主线在 p_{q-tmp}(' + assumeId + ')假设下推进。' } })
|
|
1963
|
-
|
|
2231
|
+
// ★ 判断问题必须**自带一条种子解法**,否则整条链是死的(审计 C2):buildVerifyCandidates 需要至少
|
|
2232
|
+
// 一条非终态的解法/条目才会为它选验证对象,而裸的 p-tmp-* 命题又被显式跳过(那条路我们故意不放宽:
|
|
2233
|
+
// 裸临时假设不该绕过"判断命题"直接裁决)。解法列表为空 ⇒ 没有任何东西可验证 ⇒ p_{q-tmp} 永远停在
|
|
2234
|
+
// 0.5、判断命题永不收口、终止规则(unsolved.length === 0)不可达。
|
|
2235
|
+
// 种子内容 = 这条临时假设本身("p_{q-tmp} 成立"这一候选回答),概率 0.5(>0 且 <1:它是待验证的
|
|
2236
|
+
// 候选,不是定论),并打上 `来源` 标记以示它是辅助对象(processVerify 据此让主线先占并发槽位)。
|
|
2237
|
+
qs.push({
|
|
2238
|
+
id: judgeId, 概述: '判断下述命题是否成立:' + assumeStatement, 已解决: false,
|
|
2239
|
+
解法列表: [{ 完整解法: assumeStatement, 正确概率: 0.5, 已验: false, 来源: 'sub-question-assumption', 验证记录: [] }],
|
|
2240
|
+
优先级: 1, 判断命题: assumeId,
|
|
2241
|
+
progress: { 类型: 'judge', 假设命题: assumeId, 说明: '由临时假设 p_{q-tmp}(' + assumeId + ')生成;它是对子问题 ' + subId + ' 的一种回答的命题化。解法列表里那条种子解法就是"该假设成立"这一候选回答,由验证器的裁决决定它成立(1)还是不成立(0)。' },
|
|
2242
|
+
})
|
|
1964
2243
|
await writeQs(qs)
|
|
1965
2244
|
const p = {
|
|
1966
2245
|
id: assumeId, 概述: assumeStatement, 布尔估计: 0.5,
|
|
@@ -1984,9 +2263,44 @@ export function apply(ctx) {
|
|
|
1984
2263
|
}
|
|
1985
2264
|
|
|
1986
2265
|
// ================= verification (验证器) =================
|
|
1987
|
-
|
|
2266
|
+
// 一票不算共识:≥MIN_REVIEWERS 份独立评审才可能达成/否决共识(审计 M11;与 v3 的最小票数同型)。
|
|
2267
|
+
function consensus(t) { const cids = Object.keys(t.childResults); if (cids.length < MIN_REVIEWERS) return false; const vs = cids.map(function (cid) { return t.childResults[cid].Result }); return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
|
|
1988
2268
|
function buildTranscript(t) { const parts = []; const cids = Object.keys(t.childResults); for (let i = 0; i < cids.length; i++) { const r = t.childResults[cids[i]]; parts.push('Reviewer ' + i + ': Result=' + r.Result + ' Reason=' + r.Reason) } return parts.join('\n') }
|
|
1989
|
-
|
|
2269
|
+
// 说明(审计 L17):这里曾有一个 `verifierWeight(cid, rigor)`,但全仓只有它的定义、没有任何调用点,
|
|
2270
|
+
// 且其公式(clamped ±0.2 rigor 加成、按 childId 取准确率)与 finalVerdict 里真正在用的
|
|
2271
|
+
// 稳定身份键 + 0.1 自信加成**并不相同**——留着它只会让人以为 forced 模式走的是那条公式。已删除。
|
|
2272
|
+
/**
|
|
2273
|
+
* 验证者的**稳定身份**(provider/model):forced 模式的"历史准确率"必须按这种跨子代理稳定的键记,
|
|
2274
|
+
* 按一次性的 childId 记等于永远 0.5(审计 H4)。子代理信息里本来没有这个字段,所以在这里算出来,
|
|
2275
|
+
* 由 spawnChild 写进 agentRegistry 的 meta,再随每次投票进入 childResults。
|
|
2276
|
+
*/
|
|
2277
|
+
function verifierIdentityKey() { const o = childAgentOptions(); return 'm:' + String(o.provider || '-') + '/' + String(o.model || '-') }
|
|
2278
|
+
const PENDING_SCORE_MAX_OBJECTS = 32
|
|
2279
|
+
/** 记下本轮各评审的投票,等**这个对象后来**取得布尔定论时再回溯计分(见 scorePendingReviews)。 */
|
|
2280
|
+
function recordReviewForScoring(objectId, key, result) {
|
|
2281
|
+
const id = String(objectId || '')
|
|
2282
|
+
if (!id) return
|
|
2283
|
+
let list = pendingReviewScores[id]
|
|
2284
|
+
if (!list) { list = []; pendingReviewScores[id] = list }
|
|
2285
|
+
list.push({ key: String(key || 'm:unknown'), result: clamp01(result) })
|
|
2286
|
+
if (list.length > 8) list.shift()
|
|
2287
|
+
const ids = Object.keys(pendingReviewScores)
|
|
2288
|
+
while (ids.length > PENDING_SCORE_MAX_OBJECTS) delete pendingReviewScores[ids.shift()]
|
|
2289
|
+
}
|
|
2290
|
+
/** 对象取得布尔定论(1/0)时,用它给**此前**那些还没被检验过的评审打分。绝不拿聚合值给同一批打分。 */
|
|
2291
|
+
async function scorePendingReviews(objectId, truth) {
|
|
2292
|
+
const id = String(objectId || '')
|
|
2293
|
+
const list = id ? pendingReviewScores[id] : undefined
|
|
2294
|
+
if (!list || list.length === 0) return
|
|
2295
|
+
delete pendingReviewScores[id]
|
|
2296
|
+
for (let i = 0; i < list.length; i++) {
|
|
2297
|
+
const acc = verifierAccuracy[list[i].key] || { correct: 0, total: 0 }
|
|
2298
|
+
acc.total += 1
|
|
2299
|
+
if (list[i].result === truth) acc.correct += 1
|
|
2300
|
+
verifierAccuracy[list[i].key] = acc
|
|
2301
|
+
}
|
|
2302
|
+
await writeJson('VibeMath_State/verifier_accuracy.json', verifierAccuracy)
|
|
2303
|
+
}
|
|
1990
2304
|
async function handleVerifier(childId, meta, output, stopReason) {
|
|
1991
2305
|
const rId = meta.rId
|
|
1992
2306
|
const parsed = parseJson(output)
|
|
@@ -1997,7 +2311,7 @@ export function apply(ctx) {
|
|
|
1997
2311
|
const Result = clamp01((parsed && parsed.Result != null) ? parsed.Result : 0.5)
|
|
1998
2312
|
const Reason = (parsed && parsed.Reason) || ''
|
|
1999
2313
|
let t = tasks['verify:' + rId]
|
|
2000
|
-
if (!t) { t = { id: 'verify:' + rId, type: 'verify', r: { kind: 'proposition', pId: rId, 概述: rId }, rId: rId, status: 'debating', children: [], childResults: {}, history: [], round: 1, expectedCount: Math.max(
|
|
2314
|
+
if (!t) { t = { id: 'verify:' + rId, type: 'verify', r: { kind: 'proposition', pId: rId, 概述: rId }, rId: rId, status: 'debating', children: [], childResults: {}, history: [], round: 1, expectedCount: Math.max(MIN_REVIEWERS, params.verifierCount), createdAt: now() }; tasks[t.id] = t }
|
|
2001
2315
|
if (!parsed && !scheduler.running) {
|
|
2002
2316
|
// abort:被中断的验证器没有产出,丢弃该子代理并清理任务簿记(任务在 resume 时由 processVerify 重建)
|
|
2003
2317
|
delete agentRegistry[childId]
|
|
@@ -2007,7 +2321,7 @@ export function apply(ctx) {
|
|
|
2007
2321
|
return
|
|
2008
2322
|
}
|
|
2009
2323
|
if (t.children.indexOf(childId) === -1) t.children.push(childId)
|
|
2010
|
-
t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round }
|
|
2324
|
+
t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round, key: meta.verificationKey || verifierIdentityKey() }
|
|
2011
2325
|
delete agentRegistry[childId]
|
|
2012
2326
|
const allReported = t.children.length > 0 && t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === meta.round })
|
|
2013
2327
|
if (!allReported) { await saveAll(); return }
|
|
@@ -2042,14 +2356,40 @@ export function apply(ctx) {
|
|
|
2042
2356
|
await finalizeVerification(t)
|
|
2043
2357
|
}
|
|
2044
2358
|
}
|
|
2359
|
+
/**
|
|
2360
|
+
* 裁决前的**票数下限**(审计 M11):验证者掉线(followup 失败会把 childResults 里那一票删掉)时,
|
|
2361
|
+
* 只剩一票也会走 finalVerdict —— 一个"完全验证"的布尔结论就这样由单个评审写成。
|
|
2362
|
+
* 票数不足时**不产生任何裁决**:先补派验证器;补不足(未运行 / 并发被占满 / 已重试两次)就把任务置为
|
|
2363
|
+
* `stalled`,冷却期后重试(任务保留 ⇒ processVerify 不会为同一个 rId 另造任务,也不会每 tick 重试)。
|
|
2364
|
+
*/
|
|
2045
2365
|
async function finalizeVerification(t) {
|
|
2366
|
+
const reviews = Object.keys(t.childResults || {}).length
|
|
2367
|
+
if (reviews < MIN_REVIEWERS) {
|
|
2368
|
+
const tried = Number(t.reviewerRespawn || 0)
|
|
2369
|
+
if (scheduler.running && tried < 2 && reviewerCap() >= MIN_REVIEWERS) {
|
|
2370
|
+
t.reviewerRespawn = tried + 1
|
|
2371
|
+
logActivity('verify', t.rId + ' 只有 ' + reviews + ' 份有效评审(需要 ≥' + MIN_REVIEWERS + '),重新派发验证器补足(第 ' + t.reviewerRespawn + ' 次)')
|
|
2372
|
+
t.status = 'spawning'; t.children = []; t.childResults = {}; t.round = 1
|
|
2373
|
+
await saveAll()
|
|
2374
|
+
return
|
|
2375
|
+
}
|
|
2376
|
+
logActivity('verify', t.rId + ' 有效评审不足 ' + MIN_REVIEWERS + ' 份(' + reviews + '),本次**不产生裁决**;' + REVERIFY_COOLDOWN_MS / 60000 + ' 分钟后重试')
|
|
2377
|
+
t.status = 'stalled'; t.retryAt = now() + REVERIFY_COOLDOWN_MS
|
|
2378
|
+
t.children = []; t.childResults = {}
|
|
2379
|
+
await saveAll()
|
|
2380
|
+
return
|
|
2381
|
+
}
|
|
2046
2382
|
const verdict = finalVerdict(t)
|
|
2047
2383
|
if (params.mode === 'manual') {
|
|
2048
2384
|
const d = enqueueDecision('verdict', 'verdict for ' + t.rId + ' (debate finished) = ' + verdict, { rId: t.rId, verdict: verdict, task: JSON.parse(JSON.stringify(t)) })
|
|
2049
2385
|
scheduler.gate = { decisionId: d.id, node: 'verdict' }
|
|
2050
2386
|
t.status = 'awaiting-verdict'
|
|
2051
2387
|
} else {
|
|
2052
|
-
|
|
2388
|
+
// 任务本条仍丢弃(见 settleVerdict 的返回契约:v2 的严格终止依赖 tasks 为空,既有用例
|
|
2389
|
+
// formal-verify-v2 7/12 也依赖"搁置后这一轮就此结束"),但"裁定是否真的应用"必须留痕,
|
|
2390
|
+
// 否则任务表与 Verification_logs 对同一次裁定的说法不一致。
|
|
2391
|
+
const applied = await settleVerdict(t, verdict)
|
|
2392
|
+
logActivity('verdict', t.rId + ' 的裁决已' + (applied ? '应用' : '搁置(未应用到对象,任务本条随之关闭;形式化通过后重新验证)'))
|
|
2053
2393
|
delete tasks[t.id]
|
|
2054
2394
|
}
|
|
2055
2395
|
}
|
|
@@ -2062,7 +2402,10 @@ export function apply(ctx) {
|
|
|
2062
2402
|
let num = 0; let den = 0
|
|
2063
2403
|
const cids = Object.keys(t.childResults)
|
|
2064
2404
|
for (let i = 0; i < rs.length; i++) {
|
|
2065
|
-
|
|
2405
|
+
// 历史准确率按**稳定身份**取(见 verifierIdentityKey / settleVerdict):新模型的第一票用 0.5
|
|
2406
|
+
// 先验,此后按"后来的布尔定论"累积。
|
|
2407
|
+
const key = (t.childResults[cids[i]] && t.childResults[cids[i]].key) || 'm:unknown'
|
|
2408
|
+
const acc = verifierAccuracy[key] || { correct: 0, total: 0 }
|
|
2066
2409
|
const accRate = acc.total > 0 ? (acc.correct / acc.total) : 0.5
|
|
2067
2410
|
const confident = (rs[i].Result === 1 || rs[i].Result === 0) ? 0.1 : 0
|
|
2068
2411
|
const w = Math.max(0.05, Math.min(0.95, accRate + confident))
|
|
@@ -2070,7 +2413,12 @@ export function apply(ctx) {
|
|
|
2070
2413
|
}
|
|
2071
2414
|
return den > 0 ? Math.max(0.01, Math.min(0.99, num / den)) : 0.5
|
|
2072
2415
|
}
|
|
2073
|
-
|
|
2416
|
+
// flat = 均衡机制:不再"不一致就判 0.5"——各评审自报的 0<Result<1 是**它为真的概率**,
|
|
2417
|
+
// 取它们的均值(用户语义:只有精确的 1/0 是绝对真/假)。均值仍严格落在 (0,1),绝不当成绝对结论;
|
|
2418
|
+
// 这样 0.9 vs 1 这种高置信分歧得到的是 ≈0.95 而不是被折叠成 0.5(v3 用近共识规则修的就是这一点)。
|
|
2419
|
+
let sum = 0
|
|
2420
|
+
for (let i = 0; i < rs.length; i++) sum += Number(rs[i].Result) || 0
|
|
2421
|
+
return Math.max(0.01, Math.min(0.99, sum / rs.length))
|
|
2074
2422
|
}
|
|
2075
2423
|
/**
|
|
2076
2424
|
* `require` 门禁对**一次具体裁定**的判定(契约 §8)。
|
|
@@ -2090,26 +2438,44 @@ export function apply(ctx) {
|
|
|
2090
2438
|
}
|
|
2091
2439
|
return false
|
|
2092
2440
|
}
|
|
2441
|
+
/**
|
|
2442
|
+
* 应用一次裁定。
|
|
2443
|
+
*
|
|
2444
|
+
* 返回值(审计 L20):`true` = 裁定**真的落到了对象上**;`false` = 被 require 门禁搁置
|
|
2445
|
+
* (`formalVerdictDeferred`),对象一个字段都没改。调用方必须据此决定任务簿记:以前返回值没人看,
|
|
2446
|
+
* `delete tasks[...]` 无条件执行,任务表就"声称"这次裁定已应用,而唯一的证据只剩 Verification_logs/
|
|
2447
|
+
* 与 Formal/TODO.md。注意 v2 的严格终止(`Object.keys(tasks).length === 0`)与既有用例
|
|
2448
|
+
* (formal-verify-v2 用例 7/12:搁置后调度器停在那里,人形式化后再 resume)都要求**任务本条仍被丢弃**,
|
|
2449
|
+
* 所以这里只把"未应用"这一事实显式报给调用方,不改变调度契约。
|
|
2450
|
+
*/
|
|
2093
2451
|
async function settleVerdict(t, verdict) {
|
|
2094
2452
|
const v = clamp01(verdict)
|
|
2095
2453
|
const r = t.r
|
|
2096
2454
|
const cids = Object.keys(t.childResults)
|
|
2097
|
-
//
|
|
2455
|
+
// 验证者历史准确率(forced 加权用)。两条不变式(审计 H4/M13):
|
|
2456
|
+
// ① 键是**稳定身份**('m:<provider>/<model>'),不是一次性的 childId —— 按 childId 记的准确率
|
|
2457
|
+
// 在裁决时永远是 {0,0}(每次验证都新开一个 child,handleVerifier 又把它从 registry 删掉),
|
|
2458
|
+
// 于是 accRate 恒 0.5,"按历史准确率加权"根本不可能成立;
|
|
2459
|
+
// ② **只按后来的布尔定论计分**。拿本轮各评审的 Result 去和"同一批 Result 的聚合值"比较是自指:
|
|
2460
|
+
// non-unanimous 时没人是错的,flat 0.5 时凡 Result≠0.5 的人全被记错。所以这里先把本轮投票
|
|
2461
|
+
// 记进待计分表(pendingReviewScores),等这个对象**后来**真的取得 0/1 时再回溯计分。
|
|
2462
|
+
const scoreTarget = String(r.pId || r.判断命题 || r.qid || '')
|
|
2463
|
+
if ((v === 1 || v === 0) && scoreTarget) await scorePendingReviews(scoreTarget, v)
|
|
2098
2464
|
for (let i = 0; i < cids.length; i++) {
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
if (t.childResults[cids[i]].Result === v) acc.correct += 1
|
|
2102
|
-
verifierAccuracy[cids[i]] = acc
|
|
2465
|
+
const res = t.childResults[cids[i]] || {}
|
|
2466
|
+
recordReviewForScoring(scoreTarget, res.key, res.Result)
|
|
2103
2467
|
}
|
|
2104
|
-
await writeJson('VibeMath_State/verifier_accuracy.json', verifierAccuracy)
|
|
2105
2468
|
// debate transcript log
|
|
2106
|
-
|
|
2469
|
+
// 文件名来自 t.rId,而 rId 是**用户可写**的 q.id / p.id 拼出来的(buildVerifyCandidates 里
|
|
2470
|
+
// 'r-' + q.id + '-s' + j)。fs.resolve 只做规范化、并不拒绝 '..',所以 q.id = 'x/../../../../pwn'
|
|
2471
|
+
// 会让这次写落到项目树之外(审计 C1,已用 pathdemo2.cjs 复现)。路径段一律过 safeId。
|
|
2472
|
+
await writeJson('Verification_logs/' + safeId(String(t.rId)) + '_' + Date.now() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
|
|
2107
2473
|
|
|
2108
2474
|
if (r.kind === 'proposition') {
|
|
2109
2475
|
const p = await findProposition(r.pId)
|
|
2110
2476
|
if (p) {
|
|
2111
2477
|
// require 门禁:裸命题的 1/0 裁定不生效(对象留在原库、布尔估计不变、无卡片)
|
|
2112
|
-
if (await formalVerdictDeferred(t, v, v === 1 || v === 0)) return
|
|
2478
|
+
if (await formalVerdictDeferred(t, v, v === 1 || v === 0)) return false
|
|
2113
2479
|
p.布尔估计 = v
|
|
2114
2480
|
if (v === 1) { p.证明列表 = p.证明列表 || []; p.证明列表.push({ 完整过程: strongestReason(t, 1), 正确概率: 1, '支持信息/依据': '', 已验: true }); p.优先级 = 'never' }
|
|
2115
2481
|
else if (v === 0) { p.证伪列表 = p.证伪列表 || []; p.证伪列表.push({ 完整过程: strongestReason(t, 0), 正确概率: 1, '支持信息/依据': '', 已验: true }); p.优先级 = 'never' }
|
|
@@ -2117,6 +2483,9 @@ export function apply(ctx) {
|
|
|
2117
2483
|
p.证明列表 = p.证明列表 || []; p.证伪列表 = p.证伪列表 || []
|
|
2118
2484
|
p.证明列表.push({ 完整过程: strongestReason(t, 1) || '根据辩论得到的支持性论证', 正确概率: v, '支持信息/依据': '', 已验: true })
|
|
2119
2485
|
p.证伪列表.push({ 完整过程: strongestReason(t, 0) || '根据辩论得到的反驳性论证', 正确概率: 1 - v, '支持信息/依据': '', 已验: true })
|
|
2486
|
+
// 中段值 = "为真的概率",不是定论:记下时间戳,冷却期过后这个命题可以**重新入选验证**
|
|
2487
|
+
// (审计 M12:以前它被永久搁置在中间概率上,没有任何回到验证的路径)。
|
|
2488
|
+
p.最近验证时间 = now()
|
|
2120
2489
|
}
|
|
2121
2490
|
await upsertProposition(p)
|
|
2122
2491
|
await writeVerifiedCardIfNeeded(p)
|
|
@@ -2125,7 +2494,7 @@ export function apply(ctx) {
|
|
|
2125
2494
|
const p = await findProposition(r.pId)
|
|
2126
2495
|
if (p) {
|
|
2127
2496
|
// require 门禁:一条证明/证伪被判定为 1(严格成立)时同样受门禁约束
|
|
2128
|
-
if (await formalVerdictDeferred(t, v, v === 1)) return
|
|
2497
|
+
if (await formalVerdictDeferred(t, v, v === 1)) return false
|
|
2129
2498
|
const list = r.side === '证明' ? (p.证明列表 = p.证明列表 || []) : (p.证伪列表 = p.证伪列表 || [])
|
|
2130
2499
|
const item = list[r.idx]
|
|
2131
2500
|
if (item) {
|
|
@@ -2133,12 +2502,20 @@ export function apply(ctx) {
|
|
|
2133
2502
|
item.已验 = true
|
|
2134
2503
|
if (v === 1) { item['支持信息/依据'] = strongestReason(t, 1) || item['支持信息/依据'] }
|
|
2135
2504
|
else if (v === 0) {
|
|
2136
|
-
|
|
2137
|
-
|
|
2505
|
+
// 判 0 = "这份<证明/证伪>无效",**不是**"命题为假"(反过来说 side=证伪 时也不是"命题为真")。
|
|
2506
|
+
// 旧实现在这里往对侧列表推入一条 正确概率=1 的反条目,等于用"证明无效"伪造出一个布尔定论:
|
|
2507
|
+
// processStatusUpdates 随即写 布尔估计=0/1、优先级=never 和一张 结论:false/true 的 Verified
|
|
2508
|
+
// 卡片。这与仓库已修过的 defect 事故是同一类(机制本意是让验证更严,却伪造出假否定)。
|
|
2509
|
+
// 现在只把这一条记为无效(已验 / 正确概率 0),判据文字留在支持信息里,绝不伪造概率 1 的反条目。
|
|
2510
|
+
item['支持信息/依据'] = strongestReason(t, 0) || item['支持信息/依据']
|
|
2511
|
+
logActivity('verdict', t.rId + ' 判定' + r.side + '无效:' + r.pId + ' 的布尔估计保持不变(不因此认定命题为假/真)')
|
|
2138
2512
|
} else {
|
|
2139
2513
|
const other = r.side === '证明' ? (p.证伪列表 = p.证伪列表 || []) : (p.证明列表 = p.证明列表 || [])
|
|
2140
2514
|
other.push({ 完整过程: strongestReason(t, v >= 0.5 ? 0 : 1) || '辩论得出的相反方向论证', 正确概率: 1 - v, '支持信息/依据': '', 已验: true })
|
|
2141
2515
|
item['支持信息/依据'] = strongestReason(t, v >= 0.5 ? 1 : 0) || item['支持信息/依据']
|
|
2516
|
+
// 中段值:条目本身与新增的反向条目都是 (0,1) 的概率,冷却期过后可重新入选验证。
|
|
2517
|
+
item.最近验证时间 = now()
|
|
2518
|
+
other[other.length - 1].最近验证时间 = now()
|
|
2142
2519
|
}
|
|
2143
2520
|
}
|
|
2144
2521
|
await upsertProposition(p)
|
|
@@ -2150,11 +2527,13 @@ export function apply(ctx) {
|
|
|
2150
2527
|
if (sol) {
|
|
2151
2528
|
// require 门禁:v=1 意味着"这个问题的解法严格成立"(问题收口),必须被门禁拦住,
|
|
2152
2529
|
// 否则问题会被标成已解决、优先级 never,而 formal 记录仍是 none。
|
|
2153
|
-
if (await formalVerdictDeferred(t, v, v === 1)) return
|
|
2530
|
+
if (await formalVerdictDeferred(t, v, v === 1)) return false
|
|
2154
2531
|
sol.正确概率 = v
|
|
2155
2532
|
sol.已验 = true
|
|
2156
2533
|
sol.验证记录 = sol.验证记录 || []
|
|
2157
2534
|
sol.验证记录.push({ 结果: v, 时间: now(), 依据: strongestReason(t, v >= 0.5 ? 1 : 0) })
|
|
2535
|
+
// 中段值(0<v<1)= "这份解法为真的概率",不是定论:冷却期过后可重新入选验证(审计 M12)。
|
|
2536
|
+
if (v !== 1 && v !== 0) sol.最近验证时间 = now()
|
|
2158
2537
|
// 点3 回写联动:晋升问题的解法验证结果同步回源命题的证明/证伪条目(含内容比对防错位)
|
|
2159
2538
|
if (sol.来源命题 && (sol.来源列表 === '证明' || sol.来源列表 === '证伪')) {
|
|
2160
2539
|
const sp = await findProposition(sol.来源命题)
|
|
@@ -2184,15 +2563,39 @@ export function apply(ctx) {
|
|
|
2184
2563
|
logActivity('gate', t.rId + ' 对源命题 ' + ap.id + ' 的裁定 ' + v + ' 被 require 模式搁置为未定论(formal-required)')
|
|
2185
2564
|
} else {
|
|
2186
2565
|
ap.布尔估计 = v
|
|
2187
|
-
// 收敛闸门:本条路径只在 v=1/0 时才写入证明/证伪条目,中间裁决(flat
|
|
2188
|
-
//
|
|
2189
|
-
//
|
|
2190
|
-
//
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
|
|
2194
|
-
if (v === 1) {
|
|
2195
|
-
|
|
2566
|
+
// 收敛闸门:本条路径只在 v=1/0 时才写入证明/证伪条目,中间裁决(flat 现在给出各评审
|
|
2567
|
+
// 自报概率的均值,forced 给出加权均值)会让 ap 停留在"中间布尔估计 + 两个列表皆空"
|
|
2568
|
+
// 的状态——而这正是 buildVerifyCandidates 认定"裸命题需要验证"的条件。若不留时间戳,
|
|
2569
|
+
// 该命题会在每个 tick 重新入选、重开一轮完整辩论;又因 processVerify 每 tick 只跑一个
|
|
2570
|
+
// 验证,其它对象被无限饿死,终止条件(所有问题已解决)永不可达。
|
|
2571
|
+
// ★ 但**中段值不是定论**(用户语义:只有 1/0 是绝对真/假):`已验证` 只对布尔裁决生效;
|
|
2572
|
+
// 中段裁决改记时间戳,冷却期过后重新入选——对象不会被永久停在中间概率上(审计 M12)。
|
|
2573
|
+
if (v === 1) {
|
|
2574
|
+
ap.证明列表 = ap.证明列表 || []
|
|
2575
|
+
ap.证明列表.push({ 完整过程: strongestReason(t, 1) || '判断问题解法验证通过', 正确概率: 1, '支持信息/依据': '经「判断下述命题是否成立」问题解法验证', 已验: true })
|
|
2576
|
+
ap.优先级 = 'never'
|
|
2577
|
+
ap.已验证 = true
|
|
2578
|
+
} else if (v === 0) {
|
|
2579
|
+
ap.证伪列表 = ap.证伪列表 || []
|
|
2580
|
+
ap.证伪列表.push({ 完整过程: strongestReason(t, 0) || '判断问题解法判定不成立', 正确概率: 1, '支持信息/依据': '经「判断下述命题是否成立」问题解法验证', 已验: true })
|
|
2581
|
+
ap.优先级 = 'never'
|
|
2582
|
+
ap.已验证 = true
|
|
2583
|
+
} else {
|
|
2584
|
+
ap.最近验证时间 = now()
|
|
2585
|
+
}
|
|
2586
|
+
// 需求 5 第二半的联动:假设被**确认**(v=1)时,它就是对子问题 q_sub 的那一种回答的
|
|
2587
|
+
// 定论——把这份回答作为子问题的解法交回,让 processStatusUpdates 按收口规则正常关闭
|
|
2588
|
+
// 子问题并写问题卡片。否则 q_sub 永远未解决,终止规则(unsolved.length === 0)依然不可达。
|
|
2589
|
+
if (v === 1 && ap.progress && ap.progress.子问题) {
|
|
2590
|
+
const subQ = qs.find(function (x) { return x.id === ap.progress.子问题 })
|
|
2591
|
+
if (subQ && !subQ.已解决) {
|
|
2592
|
+
subQ.解法列表 = subQ.解法列表 || []
|
|
2593
|
+
if (!subQ.解法列表.some(function (s2) { return s2.正确概率 === 1 })) {
|
|
2594
|
+
subQ.解法列表.push({ 完整解法: '【假设确认】' + ap.概述 + '(子问题的候选回答经「判断下述命题是否成立」问题验证为真)', 正确概率: 1, 已验: true, 来源: 'sub-question-assumption', 验证记录: [] })
|
|
2595
|
+
logActivity('subquestion', '子问题 ' + subQ.id + ' 由临时假设 ' + ap.id + ' 的确认回答收口(交由收口规则关闭并写卡片)')
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2196
2599
|
await upsertProposition(ap)
|
|
2197
2600
|
await writeVerifiedCardIfNeeded(ap)
|
|
2198
2601
|
logActivity('judge-sync', 'judge problem verdict ' + v + ' synced to proposition ' + ap.id)
|
|
@@ -2205,6 +2608,7 @@ export function apply(ctx) {
|
|
|
2205
2608
|
}
|
|
2206
2609
|
}
|
|
2207
2610
|
logActivity('verdict', t.rId + ' = ' + v + (v === 1 ? ' (fully verified)' : v === 0 ? ' (refuted)' : ' (uncertain)'))
|
|
2611
|
+
return true
|
|
2208
2612
|
}
|
|
2209
2613
|
function strongestReason(t, wantTrue) {
|
|
2210
2614
|
const cids = Object.keys(t.childResults)
|
|
@@ -2282,15 +2686,507 @@ export function apply(ctx) {
|
|
|
2282
2686
|
// 卡片写入结果 → 布尔(向后兼容既有的 if (await writeVerifiedCardIfNeeded(p)) 语义)。
|
|
2283
2687
|
function cardOk(r) { return !!(r && r.ok) }
|
|
2284
2688
|
|
|
2689
|
+
// ==========================================================================================
|
|
2690
|
+
// 最终论文(规格:docs/final-paper.md(含本轮修订))—— 会话侧。
|
|
2691
|
+
//
|
|
2692
|
+
// 时序(修订 §A2):v2 的收口信号是严格终止(本文件 tick() 的 branch,见 `strict termination`)。
|
|
2693
|
+
// `scheduler.running=false` 之后 `scheduleTick()` 变空操作,所以**撰写子代理必须在停止翻转之前**
|
|
2694
|
+
// 派遣;派不出去(宿主激活上限 ACTIVATION_LIMIT_REACHED)时排队,由 apply 级心跳
|
|
2695
|
+
// (paperRetryDue/runPaperRetry)重试并给出可见告警。子代理的回复由 onChildEnd 的 `paper` 分支
|
|
2696
|
+
// 处理(未知 role 会静默丢失)。收口判据里 paper 子代理被排除(realAgents),避免它自己阻止收口。
|
|
2697
|
+
// 落盘(修订 §C/§D):md/tex 是文本(走宿主 fs),**paper.pdf 只能由编译器子进程生成**,插件只 stat;
|
|
2698
|
+
// paper.meta.json 最后写(提交点)。幂等 = run id + finalizedAt + 逐产物存在性(不引入材料稳定哈希)。
|
|
2699
|
+
// ==========================================================================================
|
|
2700
|
+
let paperInFlight = '' // 正在撰写的 childId(空 = 无)
|
|
2701
|
+
let paperInFlightAt = 0 // 派遣时刻(判定卡死窗口;force 与心跳都据它降级)
|
|
2702
|
+
let paperReaps = 0 // 本 run 内已回收卡死撰写者的次数(自动重派上限)
|
|
2703
|
+
const paperAbandoned = {} // childId -> true:已放弃的撰写者;它事后返回时输出被丢弃并留痕
|
|
2704
|
+
let paperPending = null // 激活上限排队重试:{ tries, retryAt, reason, limit, trigger, opts }
|
|
2705
|
+
const PAPER_RETRY_MS = 5000 // 激活上限拒绝后的重试间隔
|
|
2706
|
+
const PAPER_MAX_RETRIES = 12 // 连续被拒上限(超过则放弃并给可见告警)
|
|
2707
|
+
const PAPER_STALE_MS = 10 * 60 * 1000 // 撰写者卡死窗口:超过它,force 与心跳都会回收并重派(10 分钟)
|
|
2708
|
+
const PAPER_MAX_AUTO_REAPS = 2 // 心跳自动回收+重派的次数上限,之后交给 /vibe paper force
|
|
2709
|
+
const PAPER_LOCK_STALE_MS = 120000 // paper 作用域锁的失效时间(跨会话/跨进程尽力而为)
|
|
2710
|
+
/** 宿主激活上限(maxActiveSubagents)报错里的人数:v4/v5 用的同一判据。 */
|
|
2711
|
+
function activationLimitFrom(message) { const m = /active child limit:\s*(\d+)/.exec(String(message == null ? '' : message)); return m ? Number(m[1]) : undefined }
|
|
2712
|
+
function paperId(opts) { return paperDirId((opts && opts.id) || currentProject) }
|
|
2713
|
+
function paperDir(id) { return 'Paper/' + paperDirId(id) }
|
|
2714
|
+
function paperAbsDir(id) { return frameworkRoot() + '/' + paperDir(id) }
|
|
2715
|
+
function paperAuthorLine() { return 'Vibe Math V2(单作者:论文撰写子代理)· 项目 ' + currentProject }
|
|
2716
|
+
/**
|
|
2717
|
+
* 本次撰写的参数(spec §1/§2):命令行的 lang=/format= 覆盖参数,参数覆盖默认值。
|
|
2718
|
+
* 非法值已在 paperCommand 里拒绝;这里再做一次保守回退(落到纸面上的参数永远合法)。
|
|
2719
|
+
*/
|
|
2720
|
+
function paperOpts(opts) {
|
|
2721
|
+
const o = opts || {}
|
|
2722
|
+
const fmt = (o.format === 'md' || o.format === 'tex' || o.format === 'both') ? o.format : ((params.paperFormat === 'md' || params.paperFormat === 'tex') ? params.paperFormat : 'both')
|
|
2723
|
+
const lang = (o.lang === 'en' || o.lang === 'zh') ? o.lang : (params.paperLanguage === 'en' ? 'en' : 'zh')
|
|
2724
|
+
const compilePdf = (o.compilePdf === true || o.compilePdf === false) ? o.compilePdf : (params.paperCompilePdf !== false)
|
|
2725
|
+
return { format: fmt, lang: lang, compilePdf: compilePdf, force: o.force === true, id: o.id }
|
|
2726
|
+
}
|
|
2727
|
+
async function paperReadMeta(id) { const m = await readJson(paperDir(id) + '/paper.meta.json'); return (m && typeof m === 'object' && !Array.isArray(m)) ? m : undefined }
|
|
2728
|
+
async function paperPathExists(rel) { try { const t = await fsTarget(rel); return (await fs.stat(t)) !== undefined } catch (e) { return false } }
|
|
2729
|
+
async function paperAppendLog(id, tag, text) {
|
|
2730
|
+
const rel = paperDir(id) + '/paper.log.md'
|
|
2731
|
+
const prev = await readText(rel)
|
|
2732
|
+
const head = (prev === undefined || !String(prev).trim()) ? (paperLogHead(id).join('\n') + '\n') : String(prev)
|
|
2733
|
+
await writeText(rel, head + paperLogLine(now(), tag, text) + '\n')
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* paper 作用域锁(修订 §A3;v3 在触发前已释放项目锁,两个会话可能同时写同一棵 Paper/ 树)。
|
|
2737
|
+
* 尽力而为:同一会话总是可重入(用于续租 at);别的会话持有的锁未过期则拒绝。
|
|
2738
|
+
*/
|
|
2739
|
+
async function paperLockAcquire(id) {
|
|
2740
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
2741
|
+
const cur = await readJson(rel)
|
|
2742
|
+
if (cur && cur.sessionId && cur.sessionId !== sessionId && (now() - Number(cur.at || 0)) < PAPER_LOCK_STALE_MS) return { ok: false, holder: String(cur.sessionId), at: Number(cur.at || 0) }
|
|
2743
|
+
await writeJson(rel, { sessionId: sessionId, at: now(), project: currentProject })
|
|
2744
|
+
return { ok: true }
|
|
2745
|
+
}
|
|
2746
|
+
async function paperLockRelease(id) {
|
|
2747
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
2748
|
+
const cur = await readJson(rel)
|
|
2749
|
+
if (cur && cur.sessionId === sessionId) await writeJson(rel, { sessionId: '', at: now(), released: true })
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* 回收一个卡死的撰写者(修订 §1:死掉的撰写者绝不能把论文永久卡住)。
|
|
2753
|
+
* 做法:清状态 + 尽力中断 + 从 agentRegistry 摘除(否则它一直算"活跃子代理",也会挡住后续派发)
|
|
2754
|
+
* + 记进 paperAbandoned(事后返回的输出被明确丢弃并留痕,不是静默消失)+ 可见告警。
|
|
2755
|
+
*/
|
|
2756
|
+
async function paperReapWriter(id, why) {
|
|
2757
|
+
const cid = paperInFlight
|
|
2758
|
+
paperInFlight = ''; paperInFlightAt = 0
|
|
2759
|
+
if (cid) {
|
|
2760
|
+
try { await interruptChild(cid) } catch (e) { /* best effort */ }
|
|
2761
|
+
delete agentRegistry[cid]
|
|
2762
|
+
paperAbandoned[cid] = true
|
|
2763
|
+
}
|
|
2764
|
+
await paperAppendLog(id, 'reap', '回收卡死的撰写者 ' + (cid || '(未知)') + ':' + why + '(论文不会被永久卡住;force/心跳会继续)')
|
|
2765
|
+
logActivity('paper', '回收卡死的论文撰写子代理 ' + (cid || '(未知)') + ':' + why)
|
|
2766
|
+
console.error('vibe-math-v2: reaped a stalled paper writer (' + (cid || 'unknown') + '): ' + why)
|
|
2767
|
+
reportDirty = true
|
|
2768
|
+
return cid
|
|
2769
|
+
}
|
|
2770
|
+
function paperWriterVerdictNow(force) {
|
|
2771
|
+
return paperWriterVerdict({ inFlight: !!paperInFlight, force: !!force, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, childInRegistry: paperInFlight ? (agentRegistry[paperInFlight] !== undefined) : true })
|
|
2772
|
+
}
|
|
2773
|
+
function paperOneLine(s, cap) { return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, cap || 400) }
|
|
2774
|
+
/** 证据索引(spec §3.9):只列**确实存在**的文件;绝不含 Paper/ 自己(否则输入哈希会自我污染)。
|
|
2775
|
+
* off 档必须真的是无操作(formal-verify-v2 的语料守卫会断言"off 的提示词里零 Lean 文本"):
|
|
2776
|
+
* formal 未启用时,形式化相关的路径与字段一概不写进材料。 */
|
|
2777
|
+
async function paperEvidenceIndex() {
|
|
2778
|
+
const formalNow = formalOn()
|
|
2779
|
+
const out = ['qs/qs.json']
|
|
2780
|
+
if (formalNow && await paperPathExists('VibeMath_State/formal.json')) out.push('VibeMath_State/formal.json')
|
|
2781
|
+
for (const f of await listFiles('Propos')) out.push('Propos/' + f)
|
|
2782
|
+
for (const f of await listFiles('Verified')) out.push('Verified/' + f)
|
|
2783
|
+
if (formalNow) for (const f of await listFiles('Verified/Lean')) out.push('Verified/Lean/' + f)
|
|
2784
|
+
const dirs = ['Verification_logs', 'Progress_Logs'].concat(formalNow ? ['Formal'] : [])
|
|
2785
|
+
for (let i = 0; i < dirs.length; i++) { const files = await listFiles(dirs[i]); for (let j = 0; j < files.length; j++) out.push(dirs[i] + '/' + files[j]) }
|
|
2786
|
+
return out.filter(function (x) { return x.indexOf('Paper/') !== 0 }).sort()
|
|
2787
|
+
}
|
|
2788
|
+
/** 汇总材料(spec §4):只含既有证据 + 未决/被否证的显式标注。不含时间戳 ⇒ 哈希稳定。 */
|
|
2789
|
+
async function buildPaperDigest() {
|
|
2790
|
+
const L = []
|
|
2791
|
+
L.push('PRESET: vibe-math-v2 (single-author)')
|
|
2792
|
+
L.push('PROJECT: ' + currentProject)
|
|
2793
|
+
const qs = await getQs()
|
|
2794
|
+
L.push('')
|
|
2795
|
+
L.push('[ORIGINAL PROBLEMS] (qs/qs.json)')
|
|
2796
|
+
if (qs.length === 0) L.push('- (none)')
|
|
2797
|
+
for (let i = 0; i < qs.length; i++) {
|
|
2798
|
+
const q = qs[i]
|
|
2799
|
+
L.push('- id=' + q.id + ' | 已解决=' + (q.已解决 === true) + ' | 优先级=' + q.优先级 + ' | 概述=' + paperOneLine(q.概述, 800))
|
|
2800
|
+
const sols = q.解法列表 || []
|
|
2801
|
+
if (sols.length === 0) L.push(' · 解法:无')
|
|
2802
|
+
for (let j = 0; j < sols.length; j++) {
|
|
2803
|
+
const s = sols[j]
|
|
2804
|
+
L.push(' · 解法#' + j + ' 正确概率=' + s.正确概率 + ' 已验=' + (s.已验 === true) + ' 证据=Verification_logs/ | ' + paperOneLine(s.完整解法, 1200))
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
const props = await getPropos()
|
|
2808
|
+
L.push('')
|
|
2809
|
+
L.push('[PROPOSITIONS] (Propos/*.json)')
|
|
2810
|
+
if (props.length === 0) L.push('- (none)')
|
|
2811
|
+
for (let i = 0; i < props.length; i++) {
|
|
2812
|
+
const p = props[i]
|
|
2813
|
+
const rec = formalOn() ? formalOf(p.id) : null // off 档不把形式化字段写进材料(off 是真无操作)
|
|
2814
|
+
L.push('- id=' + p.id + ' | 布尔估计=' + p.布尔估计 + ' | 状态=' + (p.已验证 ? '已验证' : '未定论') + ' | 优先级=' + p.优先级 + ' | 价值/关键性=' + p['价值/关键性'] + (rec ? (' | 形式化=' + ((rec && rec.status) || 'none') + (rec && rec.proof ? ('(' + rec.proof + ')') : '')) : '') + ' | 概述=' + paperOneLine(p.概述, 800))
|
|
2815
|
+
const sides = [['证明', p.证明列表 || []], ['证伪', p.证伪列表 || []]]
|
|
2816
|
+
for (let k = 0; k < sides.length; k++) {
|
|
2817
|
+
const arr = sides[k][1]
|
|
2818
|
+
for (let j = 0; j < arr.length; j++) if (arr[j] && arr[j].正确概率 === 1) L.push(' · 已检验通过:' + sides[k][0] + '#' + j + '(正确概率=1) | ' + paperOneLine(arr[j].完整过程, 900))
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
L.push('')
|
|
2822
|
+
L.push('[UNRESOLVED / REFUTED — 论文里必须显式标注,不得当成已成立的结论]')
|
|
2823
|
+
let any = false
|
|
2824
|
+
for (let i = 0; i < qs.length; i++) {
|
|
2825
|
+
const sols = qs[i].解法列表 || []
|
|
2826
|
+
for (let j = 0; j < sols.length; j++) if (!(sols[j].正确概率 === 1 || sols[j].正确概率 === 0)) { any = true; L.push('- 未定论:问题 ' + qs[i].id + ' 的解法#' + j + '(正确概率=' + sols[j].正确概率 + ')') }
|
|
2827
|
+
}
|
|
2828
|
+
for (let i = 0; i < props.length; i++) {
|
|
2829
|
+
if (!(props[i].布尔估计 === 1 || props[i].布尔估计 === 0)) { any = true; L.push('- 未定论:命题 ' + props[i].id + '(布尔估计=' + props[i].布尔估计 + ')') }
|
|
2830
|
+
else if (props[i].布尔估计 === 0) L.push('- 已被否证:命题 ' + props[i].id + '(布尔估计=0)')
|
|
2831
|
+
}
|
|
2832
|
+
if (!any) L.push('- (none — 所有对象均已定论)')
|
|
2833
|
+
L.push('')
|
|
2834
|
+
L.push('[EVIDENCE INDEX] (only files that exist)')
|
|
2835
|
+
const ev = await paperEvidenceIndex()
|
|
2836
|
+
for (let i = 0; i < ev.length; i++) L.push('- ' + ev[i])
|
|
2837
|
+
return L.join('\n')
|
|
2838
|
+
}
|
|
2839
|
+
/** 「论文撰写」子代理提示词(单作者;v2/v3 同一形状)。 */
|
|
2840
|
+
function paperWriterPrompt(digest, o) {
|
|
2841
|
+
const langName = o.lang === 'en' ? 'English' : '中文(Chinese)'
|
|
2842
|
+
return 'You are the DEDICATED PAPER WRITER (single-author mode) of a math research run that has just CONVERGED.\n' +
|
|
2843
|
+
'Write its final paper in ' + langName + ', using ONLY the evidence in the MATERIAL section below.\n\n' +
|
|
2844
|
+
'HARD RULES:\n' +
|
|
2845
|
+
'- NEVER invent content: no new proposition, no new computation, no citation that is not in the MATERIAL.\n' +
|
|
2846
|
+
'- Unresolved or refuted items MUST be explicitly labelled (「未定论」/「已被否证」, or "unresolved"/"refuted" in English); never present them as established.\n' +
|
|
2847
|
+
'- Fixed 9-section skeleton — provide bodies for these EXACT `## ` headings (the framework writes the headings, author/date and the evidence index itself):\n' +
|
|
2848
|
+
PAPER_SKELETON.map(function (s, i) { return ' ' + (i + 1) + '. ' + s.key + ' — ' + s.spec }).join('\n') + '\n' +
|
|
2849
|
+
'- A section with no evidence must be exactly 「' + PAPER_NO_EVIDENCE + '」 (do not pad it).\n' +
|
|
2850
|
+
'- Markdown subset only: `#`/`##`/`###`, `- ` lists, `**bold**`, `*em*`, `` `code` ``, and inline math as `$...$`. No tables, images, footnotes or raw HTML.\n\n' +
|
|
2851
|
+
'OUTPUT CONTRACT — respond with ONLY one ```json code fence, no prose:\n' +
|
|
2852
|
+
'{"title":"<paper title>","abstract":"<original problem + main results>","sections":[{"name":"<one of the 9 headings>","body":"<markdown>"}, ...]}\n\n' +
|
|
2853
|
+
'MATERIAL (evidence only — do not add anything beyond it):\n' + digest
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* LaTeX 检测(spec §5 + 修订 §D):`paperLatexCommand` 非空时只用它(不猜);
|
|
2857
|
+
* 否则中文优先 xelatex、英文优先 pdflatex/latexmk。缺 subprocess/resolveExecutable 一律
|
|
2858
|
+
* 视为"未检测到"(照 LEAN_NOT_FOUND 的降级方式,永不抛错)。
|
|
2859
|
+
*/
|
|
2860
|
+
async function paperDetectLatex(lang) {
|
|
2861
|
+
const sub = subprocessOf()
|
|
2862
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { available: [], reason: 'no-subprocess' }
|
|
2863
|
+
if (typeof sub.resolveExecutable !== 'function') return { available: [], reason: 'no-resolveExecutable' }
|
|
2864
|
+
const forced = String(params.paperLatexCommand || '').trim()
|
|
2865
|
+
if (forced) {
|
|
2866
|
+
try { const p = await sub.resolveExecutable(forced); return p ? { available: [{ name: forced, path: String(p) }], reason: 'ok', forced: true } : { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
2867
|
+
catch (e) { return { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
2868
|
+
}
|
|
2869
|
+
const order = lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH
|
|
2870
|
+
const available = []
|
|
2871
|
+
for (let i = 0; i < order.length; i++) {
|
|
2872
|
+
try { const p = await sub.resolveExecutable(order[i]); if (p) available.push({ name: order[i], path: String(p) }) } catch (e) { /* 未安装 */ }
|
|
2873
|
+
}
|
|
2874
|
+
return { available: available, reason: available.length ? 'ok' : 'none' }
|
|
2875
|
+
}
|
|
2876
|
+
function paperArgvFor(engine) {
|
|
2877
|
+
if (engine.name === 'latexmk') return [engine.path, '-pdf', '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
2878
|
+
if (engine.name === 'tectonic') return [engine.path, '--keep-logs', 'paper.tex']
|
|
2879
|
+
return [engine.path, '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
2880
|
+
}
|
|
2881
|
+
/** 一次编译(nonstopmode),带主动超时 terminate(与 leanRunFile 同一模式)。 */
|
|
2882
|
+
async function paperRunOnce(engine, dirAbs, capMs) {
|
|
2883
|
+
const sub = subprocessOf()
|
|
2884
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, code: 'NO_SUBPROCESS' }
|
|
2885
|
+
let handle
|
|
2886
|
+
try { handle = sub.spawn({ argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
|
|
2887
|
+
catch (e) { return { ok: false, exitCode: null, code: 'SPAWN_FAILED', message: String((e && e.message) || e) } }
|
|
2888
|
+
let timedOut = false, timer = null, outcome
|
|
2889
|
+
try {
|
|
2890
|
+
outcome = await Promise.race([
|
|
2891
|
+
handle.done,
|
|
2892
|
+
new Promise(function (resolve) { timer = setTimeout(function () { timedOut = true; try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ } resolve({ exitCode: null }) }, capMs) }),
|
|
2893
|
+
])
|
|
2894
|
+
} catch (e) { if (timer !== null) clearTimeout(timer); return { ok: false, exitCode: null, code: 'RUN_FAILED', message: String((e && e.message) || e) } }
|
|
2895
|
+
if (timer !== null) clearTimeout(timer)
|
|
2896
|
+
let err = ''
|
|
2897
|
+
try { if (handle.collected && handle.collected.stderr) err = String(handle.collected.stderr.readFrom(0).text || '') } catch (e) { /* best effort */ }
|
|
2898
|
+
return { ok: !timedOut && !!outcome && outcome.exitCode === 0, exitCode: outcome ? outcome.exitCode : null, timedOut: timedOut, stderr: err.slice(-1200) }
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* 编译 paper.tex(spec §5 + 修订 §D/§E):检测 → 主引擎(nonstopmode,两遍)→ 换引擎 → 去不支持
|
|
2902
|
+
* 宏包 → 最小模板再试一次;然后报告并降级(保留 tex+md,不改写论文内容)。
|
|
2903
|
+
* **永不删除/覆盖已有 pdf**(§D):旧的 paper.pdf 原样保留,成功判据 = 本次运行 exit 0 **且** pdf 存在。
|
|
2904
|
+
*/
|
|
2905
|
+
async function paperCompile(id, o, texPrimary, bodyTex) {
|
|
2906
|
+
const relDir = paperDir(id), absDir = paperAbsDir(id)
|
|
2907
|
+
const attempts = []
|
|
2908
|
+
if (!o.compilePdf) return { compile: 'skipped', reason: 'paperCompilePdf=false', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
2909
|
+
// §E:tex 未产出(paperFormat=md)时**跳过**编译,且不得报"缺 tex 无法编译"的警告。
|
|
2910
|
+
if (o.format === 'md') return { compile: 'skipped', reason: 'paperFormat=md(不产出 tex,跳过编译)', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
2911
|
+
const det = await paperDetectLatex(o.lang)
|
|
2912
|
+
if (det.available.length === 0) {
|
|
2913
|
+
await paperAppendLog(id, 'latex', '未检测到任何 LaTeX 引擎(' + det.reason + ')→ 只保留 paper.tex + paper.md,不编译 pdf(spec §5:不阻塞定稿)')
|
|
2914
|
+
return { compile: 'not-detected', reason: det.reason, engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
2915
|
+
}
|
|
2916
|
+
await paperAppendLog(id, 'latex', '检测到引擎 ' + det.available.map(function (x) { return x.name }).join(', ') + (det.forced ? '(paperLatexCommand 指定)' : ('(顺序 ' + (o.lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH).join(' > ') + ')')))
|
|
2917
|
+
const plan = [{ engine: det.available[0], tex: texPrimary, mode: 'primary' }]
|
|
2918
|
+
if (det.available[1]) plan.push({ engine: det.available[1], tex: texPrimary, mode: 'engine-fallback' })
|
|
2919
|
+
plan.push({ engine: det.available[0], tex: paperSanitizeTex(texPrimary), mode: 'drop-unsupported-packages' })
|
|
2920
|
+
plan.push({ engine: det.available[0], tex: paperMinimalTexDoc({ lang: o.lang, bodyTex: bodyTex }), mode: 'minimal-template' })
|
|
2921
|
+
const cap = 120000
|
|
2922
|
+
for (let i = 0; i < plan.length; i++) {
|
|
2923
|
+
const step = plan[i]
|
|
2924
|
+
await writeText(relDir + '/paper.tex', step.tex)
|
|
2925
|
+
const runs = []
|
|
2926
|
+
for (let pass = 0; pass < 2; pass++) runs.push(await paperRunOnce(step.engine, absDir, cap)) // spec §5:跑两遍
|
|
2927
|
+
const last = runs[runs.length - 1]
|
|
2928
|
+
const pdf = !!last.ok && await paperPathExists(relDir + '/paper.pdf')
|
|
2929
|
+
attempts.push({ engine: step.engine.name, mode: step.mode, ok: pdf, exitCode: last.exitCode, timedOut: !!last.timedOut, stderr: last.stderr || '' })
|
|
2930
|
+
await paperAppendLog(id, 'compile', step.mode + ' engine=' + step.engine.name + ' → ' + (pdf ? 'ok(paper.pdf 已生成)' : ('failed(exit=' + String(last.exitCode) + (last.timedOut ? ', timeout' : '') + ')')))
|
|
2931
|
+
if (pdf) return { compile: (i === 0) ? 'ok' : 'repaired', engine: step.engine.name, mode: step.mode, attempts: attempts, pdfPreserved: false }
|
|
2932
|
+
}
|
|
2933
|
+
await writeText(relDir + '/paper.tex', texPrimary) // 失败时保留**主** tex(信息量最大;spec §5 保留 tex+md)
|
|
2934
|
+
const preserved = await paperPathExists(relDir + '/paper.pdf') // §D:已有的 pdf 绝不删除
|
|
2935
|
+
await paperAppendLog(id, 'compile', '所有尝试均失败:保留 paper.tex + paper.md' + (preserved ? '(paper.pdf 是**上一次成功编译**留下的,未被覆盖)' : ',未生成 paper.pdf') + '(不阻塞定稿)')
|
|
2936
|
+
logActivity('paper', '论文 pdf 编译失败(已尝试换引擎/去不支持宏包/最小模板),已保留 tex+md:' + relDir)
|
|
2937
|
+
reportDirty = true
|
|
2938
|
+
return { compile: 'failed', engine: det.available[0].name, attempts: attempts, pdfPreserved: preserved }
|
|
2939
|
+
}
|
|
2940
|
+
/** 落盘 md/tex/pdf/meta(幂等:内容相同则不重写;spec §2/§5)。 */
|
|
2941
|
+
async function paperEmitArtifacts(id, o, info) {
|
|
2942
|
+
const relDir = paperDir(id)
|
|
2943
|
+
const dateStr = fmtTime(now()).slice(0, 10)
|
|
2944
|
+
const author = paperAuthorLine()
|
|
2945
|
+
const evidence = info.evidence || (await paperEvidenceIndex())
|
|
2946
|
+
const title = String(info.title || '').trim() || ('研究报告:' + currentProject)
|
|
2947
|
+
const md = paperBuildMarkdown({ title: title, author: author, date: dateStr, abstract: info.abstract, sections: info.sections, evidence: evidence })
|
|
2948
|
+
const artifacts = { md: false, tex: false, pdf: false }
|
|
2949
|
+
let compile = 'skipped', engine = null, attempts = [], pdfPreserved = false
|
|
2950
|
+
if (o.format === 'md' || o.format === 'both') { await writeText(relDir + '/paper.md', md); artifacts.md = true }
|
|
2951
|
+
if (o.format === 'tex' || o.format === 'both') {
|
|
2952
|
+
const bodyTex = paperMdToTexBody(md)
|
|
2953
|
+
const tex = paperTexDoc({ title: title, author: author, date: dateStr, lang: o.lang, bodyTex: bodyTex })
|
|
2954
|
+
await writeText(relDir + '/paper.tex', tex)
|
|
2955
|
+
artifacts.tex = true
|
|
2956
|
+
const c = await paperCompile(id, o, tex, bodyTex)
|
|
2957
|
+
compile = c.compile; engine = c.engine || null; attempts = c.attempts || []
|
|
2958
|
+
pdfPreserved = !!c.pdfPreserved
|
|
2959
|
+
if (compile === 'ok' || compile === 'repaired') artifacts.pdf = await paperPathExists(relDir + '/paper.pdf')
|
|
2960
|
+
}
|
|
2961
|
+
const filled = PAPER_SKELETON.map(function (s) { return s.key }).filter(function (k) { return !!(info.sections && String(info.sections[k] || '').trim()) })
|
|
2962
|
+
const meta = {
|
|
2963
|
+
id: id, dir: relDir, preset: 'vibe-math-v2', singleAuthor: true, project: currentProject,
|
|
2964
|
+
title: title, author: author, date: dateStr, abstract: String(info.abstract || ''),
|
|
2965
|
+
sectionBodies: info.sections || {}, sections: PAPER_SKELETON.map(function (s) { return s.key }), filledSections: filled,
|
|
2966
|
+
raw: String(info.raw == null ? '' : info.raw), notes: Array.isArray(info.notes) ? info.notes : [],
|
|
2967
|
+
trigger: info.trigger || 'manual', runStartedAt: Number(scheduler.startedAt) || 0,
|
|
2968
|
+
params: { finalPaper: params.finalPaper !== false, paperFormat: o.format, paperLanguage: o.lang, paperCompilePdf: !!o.compilePdf, paperLatexCommand: String(params.paperLatexCommand || '') },
|
|
2969
|
+
finalizedAt: now(), finalizedAtText: fmtTime(now()),
|
|
2970
|
+
artifacts: artifacts, compile: compile, compileEngine: engine, compileAttempts: attempts, pdfPreserved: pdfPreserved,
|
|
2971
|
+
material: info.material || null, evidence: evidence,
|
|
2972
|
+
}
|
|
2973
|
+
// meta **最后**写:它是"这次定稿已提交"的唯一标记(修订 §C 的原子提交点)。
|
|
2974
|
+
await writeJson(relDir + '/paper.meta.json', meta)
|
|
2975
|
+
return meta
|
|
2976
|
+
}
|
|
2977
|
+
async function paperMissingArtifacts(id, o) {
|
|
2978
|
+
const relDir = paperDir(id)
|
|
2979
|
+
const need = []
|
|
2980
|
+
if (o.format === 'md' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.md'))) need.push('paper.md') }
|
|
2981
|
+
if (o.format === 'tex' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.tex'))) need.push('paper.tex') }
|
|
2982
|
+
return need
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* 派遣一次(内部):材料摘要只作记录(修订 §C 不再用稳定哈希);激活上限拒绝 → 排队重试(§A2)。
|
|
2986
|
+
*/
|
|
2987
|
+
async function paperDispatch(trigger, o, id, prevTries) {
|
|
2988
|
+
const relDir = paperDir(id)
|
|
2989
|
+
const digest = await buildPaperDigest()
|
|
2990
|
+
let childId
|
|
2991
|
+
try {
|
|
2992
|
+
childId = await spawnChild('paper-writer:' + id, paperWriterPrompt(digest, o), { role: 'paper', paperId: id, trigger: trigger, lang: o.lang, format: o.format, compilePdf: o.compilePdf })
|
|
2993
|
+
} catch (e) {
|
|
2994
|
+
const message = String((e && e.message) || e)
|
|
2995
|
+
const limit = activationLimitFrom(message)
|
|
2996
|
+
if (limit !== undefined || /ACTIVATION_LIMIT_REACHED|active child limit/i.test(message)) {
|
|
2997
|
+
const tries = Number(prevTries || 0) + 1
|
|
2998
|
+
if (tries > PAPER_MAX_RETRIES) {
|
|
2999
|
+
paperPending = null
|
|
3000
|
+
await paperAppendLog(id, 'dispatch-failed', '宿主激活上限连续拒绝 ' + PAPER_MAX_RETRIES + ' 次,已放弃自动派遣:' + message)
|
|
3001
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限连续拒绝(' + PAPER_MAX_RETRIES + ' 次),已放弃自动派遣(稍后可 /vibe paper force 重试):' + message)
|
|
3002
|
+
console.error('vibe-math-v2: paper writer refused by the host activation limit ' + PAPER_MAX_RETRIES + ' times; giving up (retry later with /vibe paper force) — ' + message)
|
|
3003
|
+
await paperLockRelease(id)
|
|
3004
|
+
return { ok: false, message: 'paper writer refused by the activation limit ' + PAPER_MAX_RETRIES + ' times: ' + message, id: id, dir: relDir, limit: limit }
|
|
3005
|
+
}
|
|
3006
|
+
paperPending = { tries: tries, retryAt: now() + PAPER_RETRY_MS, reason: message, limit: limit, trigger: trigger, opts: { lang: o.lang, format: o.format, compilePdf: o.compilePdf } }
|
|
3007
|
+
if (tries === 1 || tries % 3 === 0) {
|
|
3008
|
+
await paperAppendLog(id, 'dispatch-queued', '宿主激活上限(' + (limit === undefined ? '?' : limit) + ')拒绝派遣(第 ' + tries + ' 次):' + (PAPER_RETRY_MS / 1000) + 's 后重试')
|
|
3009
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限拒绝(第 ' + tries + ' 次),已排队重试:' + message)
|
|
3010
|
+
console.error('vibe-math-v2: paper writer queued after an activation-limit refusal (try ' + tries + '): ' + message)
|
|
3011
|
+
}
|
|
3012
|
+
reportDirty = true
|
|
3013
|
+
return { ok: true, queued: true, reason: 'activation-limit-reached', tries: tries, retryAt: paperPending.retryAt, limit: limit, id: id, dir: relDir }
|
|
3014
|
+
}
|
|
3015
|
+
await paperAppendLog(id, 'dispatch-failed', message)
|
|
3016
|
+
logActivity('paper', '「论文撰写」子代理派遣失败:' + message)
|
|
3017
|
+
await paperLockRelease(id)
|
|
3018
|
+
return { ok: false, message: 'paper writer dispatch failed: ' + message, id: id, dir: relDir }
|
|
3019
|
+
}
|
|
3020
|
+
paperInFlight = childId
|
|
3021
|
+
paperInFlightAt = now()
|
|
3022
|
+
// 只有"非自动重试"的派遣才重置回收计数:auto-retry 必须保留计数,否则卡死重派会无限循环。
|
|
3023
|
+
if (trigger !== 'auto-retry') paperReaps = 0
|
|
3024
|
+
paperPending = null
|
|
3025
|
+
const material = paperMaterialSummary(digest)
|
|
3026
|
+
await paperAppendLog(id, 'dispatch', 'trigger=' + trigger + ' child=' + childId + ' format=' + o.format + ' lang=' + o.lang + ' compilePdf=' + o.compilePdf + (o.force ? ' force=true' : '') + ' run=' + (Number(scheduler.startedAt) || 0) + ' material=' + material.chars + ' chars/' + material.lines + ' lines')
|
|
3027
|
+
logActivity('paper', '已派遣「论文撰写」子代理(' + trigger + ',' + o.format + '/' + o.lang + ')→ ' + relDir)
|
|
3028
|
+
reportDirty = true
|
|
3029
|
+
return { ok: true, dispatched: true, childId: childId, id: id, dir: relDir, trigger: trigger, format: o.format, lang: o.lang, compilePdf: o.compilePdf }
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* 触发入口(spec §2/§3 + 修订 §A2/§A3):自动(收口)与手动命令共用。
|
|
3033
|
+
* 幂等 = **同一 run(scheduler.startedAt)+ finalizedAt + 逐产物存在性**(修订 §C);force 跳过全部检查。
|
|
3034
|
+
* 已有定稿但缺产物 → 用已存内容补写(不重新派遣作者)。别的会话持有 paper 锁 → 跳过并留痕。
|
|
3035
|
+
*/
|
|
3036
|
+
async function maybeWritePaper(trigger, opts) {
|
|
3037
|
+
const o = paperOpts(opts)
|
|
3038
|
+
const id = paperId(opts)
|
|
3039
|
+
const relDir = paperDir(id)
|
|
3040
|
+
const meta = await paperReadMeta(id)
|
|
3041
|
+
const sameRun = !!meta && Number(meta.runStartedAt || 0) === (Number(scheduler.startedAt) || 0)
|
|
3042
|
+
if (!o.force && meta && meta.finalizedAt && sameRun) {
|
|
3043
|
+
const missing = await paperMissingArtifacts(id, o)
|
|
3044
|
+
if (missing.length === 0) {
|
|
3045
|
+
await paperAppendLog(id, 'skip', '幂等:同一 run(' + (Number(scheduler.startedAt) || 0) + ')已有定稿且产物齐全 → 不重复撰写(/vibe paper force 可重写)')
|
|
3046
|
+
return { ok: true, skipped: true, reason: 'already-finalized-this-run', id: id, dir: relDir, finalizedAt: meta.finalizedAt, artifacts: meta.artifacts || {}, compile: meta.compile, note: '同一 run 只写一次;重复触发只补写缺失产物(spec §2)' }
|
|
3047
|
+
}
|
|
3048
|
+
await paperAppendLog(id, 'fill', '幂等:已有定稿但缺少 ' + missing.join(', ') + ' → 用已存的定稿内容补写(不重新派遣作者)')
|
|
3049
|
+
const filled = await paperEmitArtifacts(id, o, { title: meta.title, abstract: meta.abstract, sections: meta.sectionBodies || {}, evidence: meta.evidence, raw: meta.raw, notes: (meta.notes || []).concat(['补写缺失产物:' + missing.join(', ')]), trigger: meta.trigger || trigger, material: meta.material })
|
|
3050
|
+
return { ok: true, skipped: true, reason: 'filled-missing-artifacts', filled: missing, id: id, dir: relDir, finalizedAt: filled.finalizedAt, artifacts: filled.artifacts, compile: filled.compile }
|
|
3051
|
+
}
|
|
3052
|
+
if (paperInFlight) {
|
|
3053
|
+
const verdict = paperWriterVerdictNow(o.force)
|
|
3054
|
+
if (verdict === 'reap') {
|
|
3055
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
3056
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
3057
|
+
// 回收后继续走下面的 paper 锁 + 派遣路径(force 与收口重试都是这个语义)
|
|
3058
|
+
} else if (verdict === 'refuse') {
|
|
3059
|
+
const ageMs = now() - Number(paperInFlightAt || 0)
|
|
3060
|
+
await paperAppendLog(id, 'force-refused', '仍有在途撰写者 ' + paperInFlight + '(' + Math.round(ageMs / 1000) + 's):超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟才会被 force 放弃')
|
|
3061
|
+
return { ok: false, reason: 'writer-in-flight', childId: paperInFlight, ageMs: ageMs, staleMs: PAPER_STALE_MS, id: id, dir: relDir, message: '已有在途的论文撰写子代理 ' + paperInFlight + '(已 ' + Math.round(ageMs / 1000) + 's 未返回)。等它返回,或等超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟后 force 会自动回收并重派(心跳也会自动回收)。' }
|
|
3062
|
+
} else {
|
|
3063
|
+
return { ok: true, skipped: true, reason: 'writer-in-flight', childId: paperInFlight, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, id: id, dir: relDir }
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
if (paperPending) return { ok: true, queued: true, reason: 'retry-queued', retryAt: paperPending.retryAt, tries: paperPending.tries, id: id, dir: relDir }
|
|
3067
|
+
const lock = await paperLockAcquire(id)
|
|
3068
|
+
if (!lock.ok) {
|
|
3069
|
+
await paperAppendLog(id, 'skip', 'paper 锁由会话 ' + lock.holder + ' 持有(未过期)→ 本次不派遣(修订 §A3 的 paper 作用域锁)')
|
|
3070
|
+
return { ok: true, skipped: true, reason: 'locked-by-another-session', holder: lock.holder, id: id, dir: relDir }
|
|
3071
|
+
}
|
|
3072
|
+
return await paperDispatch(trigger, o, id, 0)
|
|
3073
|
+
}
|
|
3074
|
+
/** apply 级心跳用:有排队的论文派遣且到点了吗(同步判断,便宜)。 */
|
|
3075
|
+
function paperRetryDue() { return !!(paperPending && now() >= Number(paperPending.retryAt || 0)) }
|
|
3076
|
+
/**
|
|
3077
|
+
* 心跳(修订 §1):在途 → 续租 paper 锁;**卡死则自动回收**(child 不在注册表,或超过卡死窗口),
|
|
3078
|
+
* 并自动重派(上限 PAPER_MAX_AUTO_REAPS,之后给出可执行提示交给 /vibe paper force);有排队则重试派遣。
|
|
3079
|
+
*/
|
|
3080
|
+
async function runPaperRetry() {
|
|
3081
|
+
const id = paperId()
|
|
3082
|
+
if (paperInFlight) {
|
|
3083
|
+
if (paperWriterVerdictNow(false) === 'reap') {
|
|
3084
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
3085
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
3086
|
+
const meta = await paperReadMeta(id)
|
|
3087
|
+
if (params.finalPaper !== false && !(meta && meta.finalizedAt) && Number(paperReaps || 0) < PAPER_MAX_AUTO_REAPS) {
|
|
3088
|
+
paperReaps = Number(paperReaps || 0) + 1
|
|
3089
|
+
await paperAppendLog(id, 'retry-after-reap', '第 ' + paperReaps + ' 次自动重派(上限 ' + PAPER_MAX_AUTO_REAPS + ')')
|
|
3090
|
+
await paperDispatch('auto-retry', paperOpts({}), id, 0)
|
|
3091
|
+
} else {
|
|
3092
|
+
await paperAppendLog(id, 'reap-giveup', '自动重派已达上限 ' + PAPER_MAX_AUTO_REAPS + '(或已有定稿):不再自动派遣,请用 /vibe paper force 手动重写')
|
|
3093
|
+
logActivity('paper', '论文撰写子代理连续卡死/已放弃:不再自动重派,可用 /vibe paper force 手动重写')
|
|
3094
|
+
}
|
|
3095
|
+
return
|
|
3096
|
+
}
|
|
3097
|
+
try { await paperLockAcquire(id) } catch (e) { /* 续租失败不致命 */ }
|
|
3098
|
+
return
|
|
3099
|
+
}
|
|
3100
|
+
if (!paperRetryDue()) return
|
|
3101
|
+
const p = paperPending
|
|
3102
|
+
paperPending = null
|
|
3103
|
+
const o = paperOpts(p.opts)
|
|
3104
|
+
await paperDispatch(p.trigger || 'auto-retry', o, id, Number(p.tries || 0))
|
|
3105
|
+
}
|
|
3106
|
+
/** `/vibe paper [lang=zh|en] [format=both|md|tex] [force]`(spec §2/§6)。非法参数 → ok:false(命令层转 kind:'error')。 */
|
|
3107
|
+
async function paperCommand(args) {
|
|
3108
|
+
const rest = Array.isArray(args) ? args : []
|
|
3109
|
+
const opts = {}
|
|
3110
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3111
|
+
const a = String(rest[i] || '')
|
|
3112
|
+
if (a === 'force') { opts.force = true; continue }
|
|
3113
|
+
const eq = a.indexOf('=')
|
|
3114
|
+
const k = eq > 0 ? a.slice(0, eq).toLowerCase() : ''
|
|
3115
|
+
const v = eq > 0 ? a.slice(eq + 1).toLowerCase() : ''
|
|
3116
|
+
if (k === 'lang') { if (v !== 'zh' && v !== 'en') return { ok: false, message: '/vibe paper lang= 只接受 zh|en(收到 ' + JSON.stringify(a) + ')' }; opts.lang = v; continue }
|
|
3117
|
+
if (k === 'format') { if (v !== 'both' && v !== 'md' && v !== 'tex') return { ok: false, message: '/vibe paper format= 只接受 both|md|tex(收到 ' + JSON.stringify(a) + ')' }; opts.format = v; continue }
|
|
3118
|
+
return { ok: false, message: 'unknown /vibe paper option: ' + a + '(可用 lang=zh|en、format=both|md|tex、force)' }
|
|
3119
|
+
}
|
|
3120
|
+
const r = await maybeWritePaper('manual', opts)
|
|
3121
|
+
if (r.ok === false) return r
|
|
3122
|
+
const autoNote = params.finalPaper === false ? '(自动触发已关闭 finalPaper=false;手动命令仍可用)' : ''
|
|
3123
|
+
const msg = r.dispatched ? ('已派遣「论文撰写」子代理,产物将写入 ' + r.dir + '/')
|
|
3124
|
+
: r.queued ? ('宿主激活上限已满:论文撰写已排队,' + (PAPER_RETRY_MS / 1000) + 's 后自动重试(第 ' + r.tries + ' 次)')
|
|
3125
|
+
: (r.reason === 'already-finalized-this-run' ? '同一 run 已有定稿且产物齐全:不重复撰写(需要重写请用 /vibe paper force)'
|
|
3126
|
+
: (r.reason === 'filled-missing-artifacts' ? ('已用既有定稿补写缺失产物:' + (r.filled || []).join(', ')) : ('未重新派遣:' + (r.reason || ''))))
|
|
3127
|
+
return Object.assign({}, r, { message: msg + autoNote, autoDisabled: params.finalPaper === false })
|
|
3128
|
+
}
|
|
3129
|
+
/** 论文撰写子代理结束(spec §4):解析回复 → 组装 9 节 → 落盘 → 编译 → 记日志 → 释放 paper 锁。 */
|
|
3130
|
+
async function handlePaperWriter(childId, meta, output) {
|
|
3131
|
+
const id = meta.paperId || paperId()
|
|
3132
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0
|
|
3133
|
+
delete paperAbandoned[childId]
|
|
3134
|
+
const o = paperOpts({ lang: meta.lang, format: meta.format, compilePdf: meta.compilePdf })
|
|
3135
|
+
const parsed = parseJson(output)
|
|
3136
|
+
const info = paperSkeletonFromReply(parsed, output)
|
|
3137
|
+
if (!info.title) info.title = paperTitleFromMd(String(output || '')) || ('研究报告:' + currentProject)
|
|
3138
|
+
for (let i = 0; i < info.notes.length; i++) await paperAppendLog(id, 'note', info.notes[i])
|
|
3139
|
+
if (!String(output || '').trim()) await paperAppendLog(id, 'reply', '作者回复为空:只写骨架 + 占位说明(绝不编造内容)')
|
|
3140
|
+
let written
|
|
3141
|
+
try {
|
|
3142
|
+
written = await paperEmitArtifacts(id, o, { title: info.title, abstract: info.abstract, sections: info.sections, raw: String(output || ''), notes: info.notes, trigger: meta.trigger || 'auto', material: paperMaterialSummary(await buildPaperDigest()) })
|
|
3143
|
+
} catch (e) {
|
|
3144
|
+
await paperAppendLog(id, 'finalize-failed', String((e && e.message) || e))
|
|
3145
|
+
logActivity('paper', '论文落盘失败:' + String((e && e.message) || e))
|
|
3146
|
+
await paperLockRelease(id)
|
|
3147
|
+
return
|
|
3148
|
+
}
|
|
3149
|
+
await paperAppendLog(id, 'finalize', '产物 ' + JSON.stringify(written.artifacts) + ' compile=' + written.compile + (written.compileEngine ? (' engine=' + written.compileEngine) : '') + ' run=' + (Number(scheduler.startedAt) || 0))
|
|
3150
|
+
logActivity('paper', '论文(最终稿)已落盘:' + paperDir(id) + '/(' + Object.keys(written.artifacts).filter(function (k) { return written.artifacts[k] }).join('/') + ',编译=' + written.compile + ')')
|
|
3151
|
+
await paperLockRelease(id)
|
|
3152
|
+
reportDirty = true
|
|
3153
|
+
await saveAll()
|
|
3154
|
+
}
|
|
3155
|
+
/** status 里的论文视图(可观测性;命令与守卫都读它)。 */
|
|
3156
|
+
async function paperStatusView() {
|
|
3157
|
+
const id = paperId()
|
|
3158
|
+
const meta = await paperReadMeta(id)
|
|
3159
|
+
return {
|
|
3160
|
+
id: id, dir: paperDir(id), inFlight: paperInFlight || null,
|
|
3161
|
+
inFlightSince: paperInFlight ? Number(paperInFlightAt || 0) : null,
|
|
3162
|
+
inFlightAgeMs: paperInFlight ? (now() - Number(paperInFlightAt || 0)) : null,
|
|
3163
|
+
staleMs: PAPER_STALE_MS, reapedThisRun: Number(paperReaps || 0),
|
|
3164
|
+
queued: paperPending ? { tries: paperPending.tries, retryAt: paperPending.retryAt, reason: paperPending.reason, limit: paperPending.limit === undefined ? null : paperPending.limit } : null,
|
|
3165
|
+
autoFinalPaper: params.finalPaper !== false, format: params.paperFormat, language: params.paperLanguage, compilePdf: params.paperCompilePdf !== false,
|
|
3166
|
+
finalizedAt: meta ? meta.finalizedAt : null, artifacts: meta ? (meta.artifacts || {}) : {}, compile: meta ? meta.compile : null,
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
|
|
2285
3170
|
// ================= child result dispatch =================
|
|
2286
3171
|
async function onChildEnd(info) {
|
|
2287
3172
|
const meta = agentRegistry[info.id]
|
|
2288
|
-
if (meta === undefined)
|
|
3173
|
+
if (meta === undefined) {
|
|
3174
|
+
// 已放弃(回收)的撰写者事后才返回:输出被丢弃,但必须**留痕**,不能静默消失(修订 §1)。
|
|
3175
|
+
if (paperAbandoned[info.id]) {
|
|
3176
|
+
delete paperAbandoned[info.id]
|
|
3177
|
+
logActivity('paper', '已回收的论文撰写子代理 ' + info.id + ' 事后返回:其输出被丢弃(已重派新的撰写者)')
|
|
3178
|
+
reportDirty = true
|
|
3179
|
+
}
|
|
3180
|
+
return
|
|
3181
|
+
}
|
|
2289
3182
|
const output = blocksToText(info.lastAssistantMessage)
|
|
2290
3183
|
try {
|
|
2291
3184
|
if (meta.role === 'explorer') await handleExplorer(info.id, meta, output)
|
|
2292
3185
|
else if (meta.role === 'solver') await handleSolver(info.id, meta, output, info.stopReason)
|
|
2293
3186
|
else if (meta.role === 'verifier') await handleVerifier(info.id, meta, output, info.stopReason)
|
|
3187
|
+
// 修订 §A2:论文撰写子代理必须有**自己的分支**——此前未知 role 会静默丢失(撰写成果永远落不了盘)。
|
|
3188
|
+
else if (meta.role === 'paper') await handlePaperWriter(info.id, meta, output)
|
|
3189
|
+
else console.error('vibe-math-v2: subagent ' + info.id + ' ended with an unknown role "' + String(meta.role) + '" — its output is dropped')
|
|
2294
3190
|
} catch (e) { console.error('vibe-math-v2 onChildEnd error: ' + String((e && e.stack) || e)) }
|
|
2295
3191
|
await saveAll()
|
|
2296
3192
|
scheduleTick()
|
|
@@ -2312,6 +3208,10 @@ export function apply(ctx) {
|
|
|
2312
3208
|
if (Object.keys(agentRegistry).length > 0 || Object.keys(tasks).length > 0) {
|
|
2313
3209
|
logActivity(fresh ? 'start' : 'resume', 'cleared ' + Object.keys(agentRegistry).length + ' agent(s) and ' + Object.keys(tasks).length + ' task(s) (' + (fresh ? 'restart' : 'stale from previous process') + ')')
|
|
2314
3210
|
agentRegistry = {}; tasks = {}
|
|
3211
|
+
// 论文的派遣态是本进程内存态:子代理已被中断/丢弃,排队与在途标记必须一起清掉,
|
|
3212
|
+
// 否则 paperInFlight 会永久挡住后续 /vibe paper(修订 §A2 的"静默消失"同类问题)。
|
|
3213
|
+
if (paperInFlight || paperPending) logActivity('paper', '重启/跨进程恢复:清空论文撰写态(inFlight=' + (paperInFlight || '-') + (paperPending ? ', queued' : '') + ')')
|
|
3214
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
2315
3215
|
}
|
|
2316
3216
|
// 并发计数由 agentRegistry 推导:清空 registry 后自然归零,无需显式赋值。
|
|
2317
3217
|
}
|
|
@@ -2319,10 +3219,10 @@ export function apply(ctx) {
|
|
|
2319
3219
|
await saveAll()
|
|
2320
3220
|
return { ok: true }
|
|
2321
3221
|
}
|
|
2322
|
-
async function startScheduler() { const r = await init(true); if (!r.ok) return r; scheduler.running = true; scheduler.startedAt = now();
|
|
2323
|
-
async function resumeScheduler() { const r = await init(false); if (!r.ok) return r; scheduler
|
|
3222
|
+
async function startScheduler() { const r = await init(true); if (!r.ok) return r; abandonGatedDecision('scheduler restarted'); scheduler.running = true; scheduler.startedAt = now(); logActivity('start', 'scheduler started for project ' + currentProject); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3223
|
+
async function resumeScheduler() { const r = await init(false); if (!r.ok) return r; abandonGatedDecision('scheduler resumed (离开那次运行,挂起的节点不再等待)'); scheduler.running = true; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
2324
3224
|
async function pauseScheduler() { scheduler.running = false; logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
|
|
2325
|
-
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
3225
|
+
async function abortScheduler() { scheduler.running = false; abandonGatedDecision('scheduler aborted'); const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
2326
3226
|
// auto 模式语义 = 无人值守自动通过关键节点:切回 auto 时把仍挂起的人工决策按自动策略放行
|
|
2327
3227
|
async function autoResolvePending() {
|
|
2328
3228
|
const pending = decisionQueue.filter(function (d) { return d.status === 'pending' })
|
|
@@ -2330,7 +3230,7 @@ export function apply(ctx) {
|
|
|
2330
3230
|
const d = pending[i]
|
|
2331
3231
|
try {
|
|
2332
3232
|
if (d.node === 'spawn') { await spawnChild(d.data.label, d.data.promptText, d.data.meta); d.status = 'resolved'; d.resolution = { action: 'approve', auto: true } }
|
|
2333
|
-
else if (d.node === 'verdict') { await settleVerdict(d.data.task, d.data.verdict); delete tasks[d.data.task.id]; d.status = 'resolved'; d.resolution = { action: 'approve', auto: true } }
|
|
3233
|
+
else if (d.node === 'verdict') { const applied = await settleVerdict(d.data.task, d.data.verdict); delete tasks[d.data.task.id]; d.status = 'resolved'; d.resolution = { action: 'approve', auto: true, applied: applied } }
|
|
2334
3234
|
} catch (e) {
|
|
2335
3235
|
// 副作用失败必须把该决策落到终态,否则它会永远保持 pending:此后每次切 auto 都在同一个
|
|
2336
3236
|
// 决策上重新抛错,而 gate 又指向它 —— 调度永久卡死。标为 resolved(auto-failed) 并让它过去,
|
|
@@ -2354,7 +3254,7 @@ export function apply(ctx) {
|
|
|
2354
3254
|
propositions: { total: propos.length, resolved: propos.filter(function (p) { return p.布尔估计 === 1 || p.布尔估计 === 0 }).length },
|
|
2355
3255
|
pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,
|
|
2356
3256
|
registeredAgents: Object.keys(agentRegistry).length,
|
|
2357
|
-
recentActivity: activityLog.slice(-Math.min(
|
|
3257
|
+
recentActivity: activityLog.slice(-Math.min(ACTIVITY_REPORT_MAX, Number(params.activityLogCap) || 100)), params: params,
|
|
2358
3258
|
formal: {
|
|
2359
3259
|
mode: formalMode(), required: formalRequired(),
|
|
2360
3260
|
leanCommand: params.leanCommand, leanArgs: params.leanArgs, leanTimeoutMs: params.leanTimeoutMs,
|
|
@@ -2362,6 +3262,8 @@ export function apply(ctx) {
|
|
|
2362
3262
|
todo: formalTodo(),
|
|
2363
3263
|
paths: { project: 'Formal/', lib: vibeRoot() + '/Formal/Lib/', proved: vibeRoot() + '/Formal/Proved/', proofs: 'Verified/Lean/' },
|
|
2364
3264
|
},
|
|
3265
|
+
// 最终论文的可观测面(spec §2/§6):自动开关、在途/排队、定稿时间、产物与编译结果。
|
|
3266
|
+
paper: await paperStatusView(),
|
|
2365
3267
|
}
|
|
2366
3268
|
}
|
|
2367
3269
|
|
|
@@ -2371,9 +3273,10 @@ export function apply(ctx) {
|
|
|
2371
3273
|
const exists = (await listDirsAt(vibeRoot(), 'Projects')).indexOf(slug) !== -1
|
|
2372
3274
|
if (!create && !exists) return { ok: false, message: 'project not found: ' + slug }
|
|
2373
3275
|
if (scheduler.running) await abortScheduler()
|
|
3276
|
+
else abandonGatedDecision('project switched')
|
|
2374
3277
|
currentProject = slug; await writeCurrentProject(); await ensureDirs()
|
|
2375
3278
|
if ((await readJson('qs/qs.json')) === undefined) await writeJson('qs/qs.json', [])
|
|
2376
|
-
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; lastReportWrite = 0; lastPushReport = 0; reportDirty = false
|
|
3279
|
+
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
2377
3280
|
formalState = { records: {}, todo: [] } // 形式化记录随项目切换(loadState 会读新项目的 formal.json)
|
|
2378
3281
|
await loadSettings(); await migrateLegacyParams(); await loadState(); await saveAll()
|
|
2379
3282
|
return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
|
|
@@ -2398,7 +3301,7 @@ export function apply(ctx) {
|
|
|
2398
3301
|
registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
|
|
2399
3302
|
registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
|
|
2400
3303
|
registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), async function (args) { params.mode = args.mode; await saveAll(); await saveSettings(); if (params.mode === 'auto') await autoResolvePending(); return { ok: true, mode: params.mode } })
|
|
2401
|
-
registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean
|
|
3304
|
+
registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, 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' }, paperLatexCommand: { type: 'string' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
|
|
2402
3305
|
registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), async function () { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } })
|
|
2403
3306
|
registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), async function () { return await saveSettings() })
|
|
2404
3307
|
registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
|
|
@@ -2407,7 +3310,7 @@ export function apply(ctx) {
|
|
|
2407
3310
|
const p = { id: args.id, 概述: args.概述, 布尔估计: clamp01(args.布尔估计 != null ? args.布尔估计 : 0.5), 细类型: (args.细类型 && typeof args.细类型 === 'object') ? args.细类型 : { 未分类: {} }, 证明列表: [], 证伪列表: [], 优先级: (args.优先级 != null) ? args.优先级 : 1, '价值/关键性': clamp01(args['价值/关键性'] != null ? args['价值/关键性'] : 0.5), progress: { 来源: 'user', 说明: '用户手动添加。' } }
|
|
2408
3311
|
await upsertProposition(p); scheduleTick(); return { ok: true, proposition: p, file: proposFile(categoryOf(p)) }
|
|
2409
3312
|
})
|
|
2410
|
-
registerTool('vibe_math_list_propositions', 'List propositions from Propos/ (summary index: id, 概述, 布尔估计, 优先级, 价值/关键性, category).', objParams({}), async function () { const all = await getPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 概述: p.概述, 布尔估计: p.布尔估计, 优先级: p.优先级, '价值/关键性': p['价值/关键性'], category: p._category } }) } })
|
|
3313
|
+
registerTool('vibe_math_list_propositions', 'List propositions from Propos/ (summary index: id, 概述, 布尔估计, 优先级, 价值/关键性, category, 依赖假设).', objParams({}), async function () { const all = await getPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 概述: p.概述, 布尔估计: p.布尔估计, 优先级: p.优先级, '价值/关键性': p['价值/关键性'], category: p._category, 依赖假设: p.依赖假设, 依赖假设已证伪: p.依赖假设已证伪 } }) } })
|
|
2411
3314
|
registerTool('vibe_math_new_project', 'Create a new math project folder and switch to it.', objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, true) })
|
|
2412
3315
|
registerTool('vibe_math_set_project', 'Switch the current math project.', objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, false) })
|
|
2413
3316
|
registerTool('vibe_math_list_projects', 'List math projects.', objParams({}), async function () { return { ok: true, current: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects') } })
|
|
@@ -2417,9 +3320,9 @@ export function apply(ctx) {
|
|
|
2417
3320
|
registerTool('vibe_math_message_agent', 'Send a message to a tracked child agent (next turn).', objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), async function (args) { if (!agentRegistry[args.childId]) return { ok: false, message: 'unknown childId' }; await followupChild(args.childId, args.message); return { ok: true, message: 'message delivered' } })
|
|
2418
3321
|
registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
|
|
2419
3322
|
// ---- Lean 形式化验证(契约 §5)----
|
|
2420
|
-
registerTool('vibe_math_lean_run', 'Execute the Lean toolchain on one .lean file inside the VibeMath root and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.', objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), async function (args, agent) { return await leanRunTool(memberIdOf(agent), args) })
|
|
2421
|
-
registerTool('vibe_math_lean_archive', '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']), async function (args, agent) { return await leanArchive(memberIdOf(agent), args) })
|
|
2422
|
-
registerTool('vibe_math_lean_lib', '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 function (args) {
|
|
3323
|
+
registerTool('vibe_math_lean_run', '(member) Execute the Lean toolchain on one .lean file inside the VibeMath root and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.', objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), async function (args, agent) { return await leanRunTool(memberIdOf(agent), args) })
|
|
3324
|
+
registerTool('vibe_math_lean_archive', '(member) 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']), async function (args, agent) { return await leanArchive(memberIdOf(agent), args) })
|
|
3325
|
+
registerTool('vibe_math_lean_lib', '(member) 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 function (args) {
|
|
2423
3326
|
const noRefresh = !!(args && args.refresh === false)
|
|
2424
3327
|
const r = noRefresh ? { lib: null, proved: null, objects: Object.keys(formalRecords()).length } : await rebuildLeanLibIndexes()
|
|
2425
3328
|
return {
|
|
@@ -2453,7 +3356,8 @@ export function apply(ctx) {
|
|
|
2453
3356
|
}
|
|
2454
3357
|
if (cmd === 'decisions') return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) }
|
|
2455
3358
|
if (cmd === 'agents') { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round }) } return { ok: true, agents: out } }
|
|
2456
|
-
|
|
3359
|
+
if (cmd === 'paper') return await paperCommand(args)
|
|
3360
|
+
return { ok: false, usage: 'start | resume | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | project [list|new <name>|<name>] | decisions | agents | paper [lang=zh|en] [format=both|md|tex] [force]', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
|
|
2457
3361
|
}
|
|
2458
3362
|
|
|
2459
3363
|
// ================= session surface =================
|
|
@@ -2468,6 +3372,20 @@ export function apply(ctx) {
|
|
|
2468
3372
|
// 每次工具调用前同步当前项目(按会话读 current.json;多会话互不干扰)
|
|
2469
3373
|
refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
|
|
2470
3374
|
getRunning: function () { return scheduler.running },
|
|
3375
|
+
// apply 级心跳用(修订 §A2):running=false 之后 scheduleTick() 是空操作,论文的排队重试
|
|
3376
|
+
// (激活上限)与 paper 锁续租必须由**独立于调度器**的心跳驱动。
|
|
3377
|
+
paperRetryDue: paperRetryDue,
|
|
3378
|
+
runPaperRetry: runPaperRetry,
|
|
3379
|
+
// childOwner 裁剪用:这个会话当前仍"可能再发 subagent/end"的 child(在册的 + 任务正在等的)。
|
|
3380
|
+
referencedChildIds: function () {
|
|
3381
|
+
const out = Object.keys(agentRegistry)
|
|
3382
|
+
const ids = Object.keys(tasks)
|
|
3383
|
+
for (let i = 0; i < ids.length; i++) {
|
|
3384
|
+
const t = tasks[ids[i]]
|
|
3385
|
+
if (t && Array.isArray(t.children)) for (let j = 0; j < t.children.length; j++) out.push(t.children[j])
|
|
3386
|
+
}
|
|
3387
|
+
return out
|
|
3388
|
+
},
|
|
2471
3389
|
// Lean 形式化验证(docs/formal-verification.md):状态与工具的内部入口,
|
|
2472
3390
|
// 供状态/报告与测试直接读取,不必绕过工具层。
|
|
2473
3391
|
formalMode: formalMode, formalOn: formalOn, formalRequired: formalRequired, formalGateOk: formalGateOk,
|
|
@@ -2503,13 +3421,13 @@ export function apply(ctx) {
|
|
|
2503
3421
|
registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
|
|
2504
3422
|
registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), 'vibe_math_report')
|
|
2505
3423
|
registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
|
|
2506
|
-
registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean
|
|
3424
|
+
registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, 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' }, paperLatexCommand: { type: 'string' } }), 'vibe_math_set_params')
|
|
2507
3425
|
registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), 'vibe_math_setup')
|
|
2508
3426
|
registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), 'vibe_math_save_settings')
|
|
2509
3427
|
registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
|
|
2510
3428
|
registerTool('vibe_math_add_problem', 'Add a problem to the current project qs/qs.json.', objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' } }, ['id', 'description']), 'vibe_math_add_problem')
|
|
2511
3429
|
registerTool('vibe_math_add_proposition', 'Add a proposition to Propos/ (with 概述, 布尔估计, 细类型, 优先级, 价值/关键性).', objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 布尔估计: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 细类型: { type: 'object' } }, ['id', '概述']), 'vibe_math_add_proposition')
|
|
2512
|
-
registerTool('vibe_math_list_propositions', 'List propositions from Propos/ (summary index: id, 概述, 布尔估计, 优先级, 价值/关键性, category).', objParams({}), 'vibe_math_list_propositions')
|
|
3430
|
+
registerTool('vibe_math_list_propositions', 'List propositions from Propos/ (summary index: id, 概述, 布尔估计, 优先级, 价值/关键性, category, 依赖假设).', objParams({}), 'vibe_math_list_propositions')
|
|
2513
3431
|
registerTool('vibe_math_new_project', 'Create a new math project folder and switch to it.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_new_project')
|
|
2514
3432
|
registerTool('vibe_math_set_project', 'Switch the current math project.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_set_project')
|
|
2515
3433
|
registerTool('vibe_math_list_projects', 'List math projects.', objParams({}), 'vibe_math_list_projects')
|
|
@@ -2523,15 +3441,15 @@ export function apply(ctx) {
|
|
|
2523
3441
|
// 三个工具**无条件注册**:工具注册是静态的(动态注册会依赖运行时开关,破坏既有
|
|
2524
3442
|
// `ctx.effect` 纪律),而档位只决定"框架是否告诉成员它们存在"。off 档下它们照常工作,
|
|
2525
3443
|
// 人/代理主动调用时一样可用。
|
|
2526
|
-
registerTool('vibe_math_lean_run', 'Execute the Lean toolchain on one .lean file inside the VibeMath root and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.', objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), 'vibe_math_lean_run')
|
|
2527
|
-
registerTool('vibe_math_lean_archive', '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']), 'vibe_math_lean_archive')
|
|
2528
|
-
registerTool('vibe_math_lean_lib', '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' } }), 'vibe_math_lean_lib')
|
|
3444
|
+
registerTool('vibe_math_lean_run', '(member) Execute the Lean toolchain on one .lean file inside the VibeMath root and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.', objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), 'vibe_math_lean_run')
|
|
3445
|
+
registerTool('vibe_math_lean_archive', '(member) 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']), 'vibe_math_lean_archive')
|
|
3446
|
+
registerTool('vibe_math_lean_lib', '(member) 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' } }), 'vibe_math_lean_lib')
|
|
2529
3447
|
|
|
2530
3448
|
// /vibe slash command (registered once; routed per session)
|
|
2531
3449
|
ctx.effect(() => commands.register({
|
|
2532
3450
|
name: 'vibe',
|
|
2533
3451
|
description: 'control the Vibe Math V2 solver (start/pause/projects/setup/save/decisions/agents/propositions)',
|
|
2534
|
-
input: { hint: '[start|resume|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|project [list|new <name>|<name>]|decisions|agents]' },
|
|
3452
|
+
input: { hint: '[start|resume|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|project [list|new <name>|<name>]|decisions|agents|paper [lang=zh|en] [format=both|md|tex] [force]]' },
|
|
2535
3453
|
handler: async function (invocation) {
|
|
2536
3454
|
const s = getSession(invocation && invocation.agent)
|
|
2537
3455
|
if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
|
|
@@ -2550,17 +3468,35 @@ export function apply(ctx) {
|
|
|
2550
3468
|
|
|
2551
3469
|
// subagent/end (registered once; routed to the owning session via childOwner)
|
|
2552
3470
|
ctx.on('subagent/end', function (info) {
|
|
3471
|
+
// 记下"这个 child 最近一次 end"的时间:裁剪 childOwner 的宽限期从这里算(见 pruneChildOwner)。
|
|
3472
|
+
// **不要**在这里直接回收那条映射:同一个 child 会因辩论/续轮再次 end,丢了映射这次事件就没人路由,
|
|
3473
|
+
// 实测后果是 verdict 收口失效("problem solved after verdict 1")。回收交给 pruneChildOwner 的
|
|
3474
|
+
// 引用 + 宽限期判断。
|
|
3475
|
+
lastChildEndAt.set(info.id, now())
|
|
2553
3476
|
const sid = childOwner.get(info.id)
|
|
2554
3477
|
const s = sid !== undefined ? sessions.get(sid) : undefined
|
|
2555
3478
|
if (s) s.onChildEnd(info).catch(function (e) { console.error('vibe-math-v2 onChildEnd reject: ' + String((e && e.stack) || e)) })
|
|
2556
|
-
// 注意:这里**不要**回收 childOwner 条目。这条映射在子代理 end 之后仍会被后续事件路由
|
|
2557
|
-
// 用到:曾试过在此处回收、也试过在 onChildEnd 末尾回收,两次都导致 e2e-regression 的
|
|
2558
|
-
// verdict 收口失效("problem solved after verdict 1")。代价是每个历史子代理留下一条
|
|
2559
|
-
// 小记录(有界增长,实测不影响功能),远小于"验证无法收口"的代价。
|
|
2560
3479
|
})
|
|
2561
3480
|
|
|
2562
3481
|
// tick timer (registered once; ticks every running session at its own pace)
|
|
2563
|
-
ctx.effect(() => {
|
|
3482
|
+
ctx.effect(() => {
|
|
3483
|
+
let beat = 0
|
|
3484
|
+
const t = setInterval(function () {
|
|
3485
|
+
for (const s of sessions.values()) {
|
|
3486
|
+
if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick()
|
|
3487
|
+
// 论文心跳(修订 §A2):**独立于 scheduler.running** —— 收口后 running=false,但被激活上限
|
|
3488
|
+
// 拒绝的撰写派遣仍要重试,在途时还要续租 paper 锁(否则别的会话 2 分钟后可接管同一棵树)。
|
|
3489
|
+
// runPaperRetry() 在两种情形都不成立时立即返回(一次布尔判断)。
|
|
3490
|
+
if (typeof s.runPaperRetry === 'function') s.runPaperRetry().catch(function (e) { console.error('vibe-math-v2: paper retry failed: ' + String((e && e.message) || e)) })
|
|
3491
|
+
}
|
|
3492
|
+
// childOwner / sessions 裁剪:每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
|
|
3493
|
+
if ((++beat % 30) === 0) {
|
|
3494
|
+
try { pruneChildOwner() } catch (e) { console.error('vibe-math-v2: pruneChildOwner failed: ' + String((e && e.message) || e)) }
|
|
3495
|
+
try { pruneSessions() } catch (e) { console.error('vibe-math-v2: pruneSessions failed: ' + String((e && e.message) || e)) }
|
|
3496
|
+
}
|
|
3497
|
+
}, 1000)
|
|
3498
|
+
return () => clearInterval(t)
|
|
3499
|
+
})
|
|
2564
3500
|
}
|
|
2565
3501
|
|
|
2566
3502
|
// ---- test seam: pure, stateless helpers --------------------------------
|
|
@@ -2576,6 +3512,261 @@ export function apply(ctx) {
|
|
|
2576
3512
|
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
2577
3513
|
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
2578
3514
|
// byte-identical to the previous in-`apply` declarations.
|
|
3515
|
+
// ============================================================================================
|
|
3516
|
+
// 最终论文(规格:docs/final-paper.md)—— **纯函数部分**(module scope,v2/v3 逐字同构;
|
|
3517
|
+
// 只有数据来源不同)。会话相关的落盘/派遣/编译在 makeSession 里(见 paperCommand/maybeWritePaper)。
|
|
3518
|
+
// ============================================================================================
|
|
3519
|
+
/** 固定 9 节骨架(spec §3)。key 同时是 md 的 `## ` 标题与 tex 的 `\section{}`。 */
|
|
3520
|
+
const PAPER_SKELETON = [
|
|
3521
|
+
{ key: '摘要', spec: '1 标题、作者、日期、摘要(原问题 + 主要结论)' },
|
|
3522
|
+
{ key: '引言与问题背景', spec: '2 原问题的完整陈述' },
|
|
3523
|
+
{ key: '原问题的完整解法', spec: '3 最终答案 + 完整推理链' },
|
|
3524
|
+
{ key: '已检验通过的命题', spec: '4 逐条列出,含判定为真的估计值与证据来源' },
|
|
3525
|
+
{ key: '已解决的子问题与中间成果', spec: '5' },
|
|
3526
|
+
{ key: '创造或发现的有价值之物', spec: '6 方法、理论、思想、有价值经验、数学理解' },
|
|
3527
|
+
{ key: '规律总结', spec: '7 从上述条目归纳出的可复用规律' },
|
|
3528
|
+
{ key: '讨论、局限与展望', spec: '8' },
|
|
3529
|
+
{ key: '附录:证据与文件索引', spec: '9 Verified/、Logs/、关键卡片路径' },
|
|
3530
|
+
]
|
|
3531
|
+
const PAPER_NO_EVIDENCE = '(本节暂无证据支持的内容——不编造。)'
|
|
3532
|
+
const PAPER_EVIDENCE_HEADING = '证据与文件索引'
|
|
3533
|
+
/** LaTeX 引擎检测顺序(spec §5):中文优先 xelatex,英文优先 pdflatex/latexmk。 */
|
|
3534
|
+
const PAPER_ENGINE_ORDER_ZH = ['xelatex', 'latexmk', 'pdflatex', 'lualatex', 'tectonic']
|
|
3535
|
+
const PAPER_ENGINE_ORDER_EN = ['pdflatex', 'latexmk', 'xelatex', 'lualatex', 'tectonic']
|
|
3536
|
+
/** 只用常见宏包(spec §5);修复阶段会丢弃不在这个名单里的 \usepackage。 */
|
|
3537
|
+
const PAPER_ALLOWED_PACKAGES = ['amsmath', 'amssymb', 'amsthm', 'geometry', 'hyperref', 'longtable', 'booktabs']
|
|
3538
|
+
/** tex 必须转义的 10 个字符(spec §5):\ _ % & # $ { } ~ ^ ——一次替换,避免二次转义。 */
|
|
3539
|
+
const PAPER_TEX_MAP = { '\\': '\\textbackslash{}', '_': '\\_', '%': '\\%', '&': '\\&', '#': '\\#', '$': '\\$', '{': '\\{', '}': '\\}', '~': '\\textasciitilde{}', '^': '\\textasciicircum{}' }
|
|
3540
|
+
function texEscape(s) { return String(s == null ? '' : s).replace(/[\\_%&#${}~^]/g, function (c) { return PAPER_TEX_MAP[c] }) }
|
|
3541
|
+
/** 输入材料摘要(仅供 paper.log.md/meta 记录规模,**不**用于幂等判定:spec v2 §C 用 run id + finalizedAt + 逐产物存在性)。 */
|
|
3542
|
+
function paperMaterialSummary(digest) {
|
|
3543
|
+
const t = String(digest == null ? '' : digest)
|
|
3544
|
+
return { chars: t.length, lines: t.split('\n').length }
|
|
3545
|
+
}
|
|
3546
|
+
/**
|
|
3547
|
+
* 撰写者卡死判定(纯函数,便于守卫覆盖两条分支):
|
|
3548
|
+
* 'none' 正常在途(还没到卡死窗口,且 force 未要求放弃)
|
|
3549
|
+
* 'reap' 必须回收:child 已不在 agentRegistry(end 事件丢失 / 宿主丢弃,它永远不会再回来),
|
|
3550
|
+
* 或已超过卡死窗口——force 与心跳都据此自动降级,论文不能被一个死掉的撰写者永久卡住
|
|
3551
|
+
* 'refuse' force 但还没到窗口:调用方必须给出**可执行**的报错(告诉调用者还要等多久)
|
|
3552
|
+
*/
|
|
3553
|
+
function paperWriterVerdict(o) {
|
|
3554
|
+
if (!(o && o.inFlight)) return 'none'
|
|
3555
|
+
if (o.childInRegistry === false) return 'reap'
|
|
3556
|
+
const age = Number((o && o.ageMs) || 0)
|
|
3557
|
+
const stale = Math.max(1, Number((o && o.staleMs) || 0))
|
|
3558
|
+
if (age >= stale) return 'reap'
|
|
3559
|
+
return (o && o.force) ? 'refuse' : 'none'
|
|
3560
|
+
}
|
|
3561
|
+
/** 行内 md → tex:``code`` / **bold** / *em* 转成命令,其余文本按 spec §5 转义;`$...$` 原样保留(数学模式由作者书写)。 */
|
|
3562
|
+
function paperInlineToTex(s) {
|
|
3563
|
+
const t = String(s == null ? '' : s)
|
|
3564
|
+
let out = ''
|
|
3565
|
+
let i = 0
|
|
3566
|
+
while (i < t.length) {
|
|
3567
|
+
const c = t[i]
|
|
3568
|
+
if (c === '$') { const j = t.indexOf('$', i + 1); if (j > i) { out += t.slice(i, j + 1); i = j + 1; continue } }
|
|
3569
|
+
if (c === '`') { const j = t.indexOf('`', i + 1); if (j > i) { out += '\\texttt{' + texEscape(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
3570
|
+
if (c === '*' && t[i + 1] === '*') { const j = t.indexOf('**', i + 2); if (j > i) { out += '\\textbf{' + paperInlineToTex(t.slice(i + 2, j)) + '}'; i = j + 2; continue } }
|
|
3571
|
+
if (c === '*') { const j = t.indexOf('*', i + 1); if (j > i) { out += '\\emph{' + paperInlineToTex(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
3572
|
+
// 普通字符:按 10 个字符的映射逐字转义(一次一个字符,天然不会二次转义)
|
|
3573
|
+
out += PAPER_TEX_MAP[c] !== undefined ? PAPER_TEX_MAP[c] : c
|
|
3574
|
+
i++
|
|
3575
|
+
}
|
|
3576
|
+
return out
|
|
3577
|
+
}
|
|
3578
|
+
/** md 正文 → tex 正文(标题/列表/段落;只认 compose 侧约定的最小语法)。 */
|
|
3579
|
+
function paperMdToTexBody(md) {
|
|
3580
|
+
const out = []
|
|
3581
|
+
let inList = false
|
|
3582
|
+
let buf = []
|
|
3583
|
+
const flush = function () { if (buf.length) { out.push(paperInlineToTex(buf.join(' '))); buf = [] } }
|
|
3584
|
+
const closeList = function () { if (inList) { out.push('\\end{itemize}'); inList = false } }
|
|
3585
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
3586
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3587
|
+
const line = lines[i].replace(/\s+$/, '')
|
|
3588
|
+
const h = /^(#{1,4})\s+(.*)$/.exec(line)
|
|
3589
|
+
if (h) {
|
|
3590
|
+
flush(); closeList()
|
|
3591
|
+
const txt = paperInlineToTex(h[2])
|
|
3592
|
+
const lv = h[1].length
|
|
3593
|
+
out.push(lv <= 1 ? ('\\section*{' + txt + '}') : lv === 2 ? ('\\section{' + txt + '}') : lv === 3 ? ('\\subsection{' + txt + '}') : ('\\subsubsection{' + txt + '}'))
|
|
3594
|
+
continue
|
|
3595
|
+
}
|
|
3596
|
+
const li = /^\s*[-*]\s+(.*)$/.exec(line)
|
|
3597
|
+
if (li) { flush(); if (!inList) { out.push('\\begin{itemize}'); inList = true } out.push(' \\item ' + paperInlineToTex(li[1])); continue }
|
|
3598
|
+
if (!line.trim()) { flush(); closeList(); out.push(''); continue }
|
|
3599
|
+
buf.push(line.trim())
|
|
3600
|
+
}
|
|
3601
|
+
flush(); closeList()
|
|
3602
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
3603
|
+
}
|
|
3604
|
+
/** 主模板(spec §5):中文 ctexart / 英文 article,只加常见宏包。 */
|
|
3605
|
+
function paperTexDoc(o) {
|
|
3606
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
3607
|
+
return [
|
|
3608
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
3609
|
+
'\\usepackage[margin=2.5cm]{geometry}',
|
|
3610
|
+
'\\usepackage{amsmath,amssymb,amsthm}',
|
|
3611
|
+
'\\usepackage{hyperref}',
|
|
3612
|
+
'\\usepackage{longtable,booktabs}',
|
|
3613
|
+
'\\title{' + texEscape(o && o.title) + '}',
|
|
3614
|
+
'\\author{' + texEscape(o && o.author) + '}',
|
|
3615
|
+
'\\date{' + texEscape(o && o.date) + '}',
|
|
3616
|
+
'\\begin{document}',
|
|
3617
|
+
'\\maketitle',
|
|
3618
|
+
String((o && o.bodyTex) || ''),
|
|
3619
|
+
'\\end{document}',
|
|
3620
|
+
].join('\n') + '\n'
|
|
3621
|
+
}
|
|
3622
|
+
/** 修复阶段的**最小模板**(spec §5):只留 documentclass + 正文(section/itemize/texttt 都在 LaTeX 核心)。 */
|
|
3623
|
+
function paperMinimalTexDoc(o) {
|
|
3624
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
3625
|
+
return [
|
|
3626
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
3627
|
+
'\\begin{document}',
|
|
3628
|
+
String((o && o.bodyTex) || ''),
|
|
3629
|
+
'\\end{document}',
|
|
3630
|
+
].join('\n') + '\n'
|
|
3631
|
+
}
|
|
3632
|
+
/** 修复阶段:丢弃不在常见宏包名单里的 \usepackage(其余原样保留)。 */
|
|
3633
|
+
function paperSanitizeTex(tex) {
|
|
3634
|
+
const keep = []
|
|
3635
|
+
const lines = String(tex == null ? '' : tex).split('\n')
|
|
3636
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3637
|
+
const m = /^\s*\\usepackage(\[[^\]]*\])?\{([^}]*)\}\s*$/.exec(lines[i])
|
|
3638
|
+
if (m) {
|
|
3639
|
+
const pkgs = String(m[2]).split(',').map(function (x) { return x.trim() }).filter(Boolean)
|
|
3640
|
+
const ok = pkgs.filter(function (p) { return PAPER_ALLOWED_PACKAGES.indexOf(p) >= 0 })
|
|
3641
|
+
if (ok.length === 0) continue
|
|
3642
|
+
keep.push('\\usepackage' + (m[1] || '') + '{' + ok.join(',') + '}')
|
|
3643
|
+
continue
|
|
3644
|
+
}
|
|
3645
|
+
keep.push(lines[i])
|
|
3646
|
+
}
|
|
3647
|
+
return keep.join('\n')
|
|
3648
|
+
}
|
|
3649
|
+
/** 骨架名匹配:允许带序号/前缀,或只写节名的一部分(例如「附录」)。 */
|
|
3650
|
+
function paperSkeletonKey(name) {
|
|
3651
|
+
const n = String(name == null ? '' : name).replace(/^#+\s*/, '').trim()
|
|
3652
|
+
if (!n) return undefined
|
|
3653
|
+
for (const s of PAPER_SKELETON) if (n === s.key) return s.key
|
|
3654
|
+
for (const s of PAPER_SKELETON) if (n.indexOf(s.key) !== -1 || s.key.indexOf(n) !== -1) return s.key
|
|
3655
|
+
return undefined
|
|
3656
|
+
}
|
|
3657
|
+
/** 按 `## ` 把任意 markdown 切成 [{name,text}](无标题时 name='')。 */
|
|
3658
|
+
function paperSplitByHeadings(md) {
|
|
3659
|
+
const out = []
|
|
3660
|
+
let cur = { name: '', text: [] }
|
|
3661
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
3662
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3663
|
+
const h = /^##\s+(.*?)\s*$/.exec(lines[i])
|
|
3664
|
+
if (h) { if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() }); cur = { name: h[1], text: [] }; continue }
|
|
3665
|
+
cur.text.push(lines[i])
|
|
3666
|
+
}
|
|
3667
|
+
if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() })
|
|
3668
|
+
return out
|
|
3669
|
+
}
|
|
3670
|
+
/**
|
|
3671
|
+
* 把「论文撰写」子代理的回复映射到 9 节骨架上(spec §3/§4)。
|
|
3672
|
+
* 接受契约 JSON(title/abstract/sections[])或一整段 md(按 `## ` 切);**绝不编造**:
|
|
3673
|
+
* 没写到的节由调用方填 PAPER_NO_EVIDENCE,不属于骨架的节并入第 8 节并记一条说明。
|
|
3674
|
+
*/
|
|
3675
|
+
function paperSkeletonFromReply(reply, fallbackText) {
|
|
3676
|
+
const sections = {}
|
|
3677
|
+
const notes = []
|
|
3678
|
+
let title = ''
|
|
3679
|
+
let abstract = ''
|
|
3680
|
+
const put = function (key, body) {
|
|
3681
|
+
const b = String(body == null ? '' : body).trim()
|
|
3682
|
+
if (!b) return
|
|
3683
|
+
sections[key] = sections[key] ? (sections[key] + '\n\n' + b) : b
|
|
3684
|
+
}
|
|
3685
|
+
const absorbUnknown = function (name, text) {
|
|
3686
|
+
if (!name && !String(text || '').trim()) return
|
|
3687
|
+
notes.push('作者写了一节「' + (name || '(无名)') + '」,不属于固定 9 节骨架,已并入「讨论、局限与展望」')
|
|
3688
|
+
put('讨论、局限与展望', (name ? ('**' + name + '**\n\n') : '') + text)
|
|
3689
|
+
}
|
|
3690
|
+
const obj = (reply && typeof reply === 'object' && !Array.isArray(reply)) ? reply : null
|
|
3691
|
+
if (obj) {
|
|
3692
|
+
title = String(obj.title || obj.标题 || '').trim()
|
|
3693
|
+
abstract = String(obj.abstract || obj.摘要 || '').trim()
|
|
3694
|
+
const list = Array.isArray(obj.sections) ? obj.sections : (Array.isArray(obj.章节) ? obj.章节 : [])
|
|
3695
|
+
for (let i = 0; i < list.length; i++) {
|
|
3696
|
+
const s = list[i]
|
|
3697
|
+
if (!s) continue
|
|
3698
|
+
const name = String(s.name || s.key || s.heading || s.标题 || '')
|
|
3699
|
+
const body = String(s.body != null ? s.body : (s.content != null ? s.content : (s.内容 != null ? s.内容 : '')))
|
|
3700
|
+
const key = paperSkeletonKey(name)
|
|
3701
|
+
if (key === undefined) absorbUnknown(name.trim(), body)
|
|
3702
|
+
else put(key, body)
|
|
3703
|
+
}
|
|
3704
|
+
const bodyField = String(obj.body || obj.content || obj.md || obj.正文 || '')
|
|
3705
|
+
if (bodyField.trim()) {
|
|
3706
|
+
const parts = paperSplitByHeadings(bodyField)
|
|
3707
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3708
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
3709
|
+
if (key === undefined) { if (!parts[i].name && !abstract && !sections['摘要']) abstract = parts[i].text; else if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
3710
|
+
else put(key, parts[i].text)
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
} else {
|
|
3714
|
+
// 非契约 JSON:把模型**原样**写出的 md 当正文(不编造任何内容;识别不了就整体并入第 8 节)
|
|
3715
|
+
const parts = paperSplitByHeadings(String(fallbackText == null ? '' : fallbackText))
|
|
3716
|
+
let matched = 0
|
|
3717
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3718
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
3719
|
+
if (key === undefined) { if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
3720
|
+
else { put(key, parts[i].text); matched++ }
|
|
3721
|
+
}
|
|
3722
|
+
if (matched === 0) {
|
|
3723
|
+
const raw = String(fallbackText == null ? '' : fallbackText).trim()
|
|
3724
|
+
if (raw) { notes.push('作者回复不是契约 JSON,也没有可识别的 `## ` 节标题;其原样正文已记入「讨论、局限与展望」'); put('讨论、局限与展望', raw) }
|
|
3725
|
+
else notes.push('作者回复为空:没有任何内容可写(不得编造)')
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return { title: title, abstract: abstract, sections: sections, notes: notes }
|
|
3729
|
+
}
|
|
3730
|
+
/** 组装 md(spec §3):9 节固定骨架 + 确定性证据索引;缺证据的节写占位说明。 */
|
|
3731
|
+
function paperBuildMarkdown(o) {
|
|
3732
|
+
const sec = (o && o.sections) || {}
|
|
3733
|
+
const ev = ((o && o.evidence) || []).slice()
|
|
3734
|
+
const L = []
|
|
3735
|
+
const ttl = String((o && o.title) || '').trim() || '研究报告'
|
|
3736
|
+
L.push('# ' + ttl)
|
|
3737
|
+
L.push('')
|
|
3738
|
+
L.push('- 作者:' + String((o && o.author) || ''))
|
|
3739
|
+
L.push('- 日期:' + String((o && o.date) || ''))
|
|
3740
|
+
L.push('')
|
|
3741
|
+
for (let i = 0; i < PAPER_SKELETON.length; i++) {
|
|
3742
|
+
const key = PAPER_SKELETON[i].key
|
|
3743
|
+
L.push('## ' + key)
|
|
3744
|
+
L.push('')
|
|
3745
|
+
let body = String(sec[key] || '').trim()
|
|
3746
|
+
if (key === '摘要') { const ab = String((o && o.abstract) || '').trim(); if (ab) body = body ? (ab + '\n\n' + body) : ab }
|
|
3747
|
+
if (key === '附录:证据与文件索引' && ev.length) {
|
|
3748
|
+
const idx = '### ' + PAPER_EVIDENCE_HEADING + '\n\n' + ev.map(function (x) { return '- `' + String(x) + '`' }).join('\n')
|
|
3749
|
+
body = body ? (body + '\n\n' + idx) : idx
|
|
3750
|
+
}
|
|
3751
|
+
L.push(body || PAPER_NO_EVIDENCE)
|
|
3752
|
+
L.push('')
|
|
3753
|
+
}
|
|
3754
|
+
return L.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
|
|
3755
|
+
}
|
|
3756
|
+
/** paper.log.md 的**唯一格式**(v2/v3 逐字一致,spec §4)。 */
|
|
3757
|
+
function paperLogHead(id) {
|
|
3758
|
+
return ['# 论文写作日志|' + String(id), '',
|
|
3759
|
+
'> 单作者流程(v2/v3 同一格式):收口触发 → 派遣「论文撰写」子代理 → 落盘 md/tex → 编译 pdf。', '']
|
|
3760
|
+
}
|
|
3761
|
+
function paperLogLine(at, tag, text) { return '- ' + fmtTime(at) + ' [' + String(tag) + '] ' + String(text == null ? '' : text) }
|
|
3762
|
+
/** 论文目录 id:只允许安全字符(同一把归一化 ⇒ 不可能写出 Paper/ 之外,spec §6 越权用例)。 */
|
|
3763
|
+
function paperDirId(raw) { return (safeId(String(raw == null ? '' : raw).trim()) || 'run') }
|
|
3764
|
+
/** 从回复里抽出可能存在的标题行(md 非契约路径的兜底)。 */
|
|
3765
|
+
function paperTitleFromMd(md) {
|
|
3766
|
+
const m = /^#\s+(.*?)\s*$/m.exec(String(md == null ? '' : md))
|
|
3767
|
+
return m ? m[1].trim() : ''
|
|
3768
|
+
}
|
|
3769
|
+
|
|
2579
3770
|
export const __testHelpers = {
|
|
2580
3771
|
uuid,
|
|
2581
3772
|
shortId,
|
|
@@ -2590,6 +3781,27 @@ export const __testHelpers = {
|
|
|
2590
3781
|
parseProgress,
|
|
2591
3782
|
sanitizeToolFilter,
|
|
2592
3783
|
registeredToolsFromError,
|
|
3784
|
+
// final paper(spec §6 的纯函数守卫面)
|
|
3785
|
+
PAPER_SKELETON,
|
|
3786
|
+
PAPER_NO_EVIDENCE,
|
|
3787
|
+
PAPER_ALLOWED_PACKAGES,
|
|
3788
|
+
PAPER_ENGINE_ORDER_ZH,
|
|
3789
|
+
PAPER_ENGINE_ORDER_EN,
|
|
3790
|
+
texEscape,
|
|
3791
|
+
paperMaterialSummary,
|
|
3792
|
+
paperWriterVerdict,
|
|
3793
|
+
paperInlineToTex,
|
|
3794
|
+
paperMdToTexBody,
|
|
3795
|
+
paperTexDoc,
|
|
3796
|
+
paperMinimalTexDoc,
|
|
3797
|
+
paperSanitizeTex,
|
|
3798
|
+
paperSkeletonKey,
|
|
3799
|
+
paperSkeletonFromReply,
|
|
3800
|
+
paperBuildMarkdown,
|
|
3801
|
+
paperLogHead,
|
|
3802
|
+
paperLogLine,
|
|
3803
|
+
paperDirId,
|
|
3804
|
+
paperTitleFromMd,
|
|
2593
3805
|
}
|
|
2594
3806
|
|
|
2595
3807
|
function now() { return Date.now() }
|