dsh-vibe-math 2.4.1 → 2.5.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 +14 -14
- package/README.md +15 -14
- package/cordis.patch.yml +33 -10
- package/docs/AUDIT-CHECKLIST.md +25 -6
- package/docs/COMPAT-AUDIT-ROUND3.md +16 -1
- package/docs/release-notes/RELEASE-NOTES-2.5.0.md +173 -0
- package/docs/test-timing.md +73 -40
- package/installer.js +225 -45
- package/package.json +3 -2
- package/preset-declaration.js +38 -7
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +67 -21
- package/prompt-corpus-v2/formal-verify-v2.json +116 -92
- package/prompt-corpus-v2/formal-verify-v2.md +240 -93
- package/prompt-corpus-v3/formal-verify-v3.json +81 -81
- package/prompt-corpus-v3/formal-verify-v3.md +288 -1111
- 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 +11 -4
- package/tests/audit-persona-surface.test.mjs +5 -1
- package/tests/audit-v5-sensitivity.mjs +47 -6
- package/tests/e2e-v5-round2.test.mjs +354 -0
- package/tests/formal-verify-v2.test.mjs +54 -2
- package/tests/formal-verify-v3.test.mjs +63 -2
- package/tests/formal-verify-v4.test.mjs +120 -4
- package/tests/run-tests.mjs +104 -23
- package/vibe-math-v2/agent.cordis.yml +6 -0
- package/vibe-math-v2/vibe-math-v2.js +503 -99
- 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 +26 -10
- package/vibe-math-v3/vibe-math-v3.js +555 -175
- 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 +26 -6
- package/vibe-math-v4/vibe-math-v4.js +404 -51
- 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 +8 -4
- package/vibe-math-v5/vibe-math-v5.js +513 -105
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +73 -40
|
@@ -64,6 +64,25 @@ export function apply(ctx) {
|
|
|
64
64
|
// ================= per-session registry =================
|
|
65
65
|
const sessions = new Map() // rootAgentId -> Session
|
|
66
66
|
const childOwner = new Map() // childId -> rootAgentId (route subagent/end back to its session)
|
|
67
|
+
// childOwner 的**有界化**(审计 L1)。这条映射只在"这个 child 还可能再发 subagent/end"时有价值,
|
|
68
|
+
// 但(与 v2 同一结论)不能在事件回调里立即回收:辩论/续轮会在同一个 child 上再次 end,丢了映射这次
|
|
69
|
+
// 事件就没人路由,verdict 收口随之失效。所以改为**引用 + 宽限期**:仍被任务或 agentRegistry 引用的
|
|
70
|
+
// 一律保留,没有引用且距最近一次 end 已超过宽限期才回收。
|
|
71
|
+
const CHILD_OWNER_GRACE_MS = 5 * 60 * 1000
|
|
72
|
+
const lastChildEndAt = new Map() // childId -> 最近一次 subagent/end 的时间
|
|
73
|
+
function pruneChildOwner() {
|
|
74
|
+
const keep = new Set()
|
|
75
|
+
for (const s of sessions.values()) {
|
|
76
|
+
try { for (const id of s.referencedChildIds()) keep.add(id) } catch (e) { /* 尽力而为 */ }
|
|
77
|
+
}
|
|
78
|
+
const cutoff = now() - CHILD_OWNER_GRACE_MS
|
|
79
|
+
for (const cid of Array.from(childOwner.keys())) {
|
|
80
|
+
if (keep.has(cid)) continue
|
|
81
|
+
const ended = lastChildEndAt.get(cid)
|
|
82
|
+
if (ended === undefined || ended > cutoff) continue
|
|
83
|
+
childOwner.delete(cid); lastChildEndAt.delete(cid)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
const fileOwner = {} // 进程级写锁:fileKey -> { childId, sessionId, at } —— 防任何代理(跨会话)并发写同一 md 文件
|
|
68
87
|
// Process epoch: PROCESS-level (one per apply, shared by every session), written to
|
|
69
88
|
// State/process_epoch.json at init; a DIFFERENT persisted epoch means a previous DSH
|
|
@@ -71,6 +90,11 @@ export function apply(ctx) {
|
|
|
71
90
|
// same-process pause→resume (children may still be alive). Kept at apply level so two
|
|
72
91
|
// sessions in one process never treat each other as a stale previous process.
|
|
73
92
|
const processEpoch = String(Date.now()) + '-' + Math.random().toString(36).slice(2, 8)
|
|
93
|
+
// 项目锁 LEASE 的续租轮询间隔(apply 级定时器用,见 apply() 末尾):它必须**明显快于**续租判据
|
|
94
|
+
// (projectLockTimeoutMs/4),否则"多久检查一次"会成为租约年龄的下界——实测 2500ms 轮询配 625ms
|
|
95
|
+
// 判据时最坏年龄仍有 2467ms,几乎顶到 2500ms 的租约上限。250ms 轮询下最坏租约年龄 ≈
|
|
96
|
+
// max(250ms, projectLockTimeoutMs/4),即上限的 1/4 再加一次轮询的抖动。
|
|
97
|
+
const LOCK_POLL_MS = 250
|
|
74
98
|
|
|
75
99
|
function sessionIdOf(agent) { try { return (agent && agent.id) ? String(agent.id) : undefined } catch (e) { return undefined } }
|
|
76
100
|
// Walk up the durable session lineage to the top-level (root) agent of this session,
|
|
@@ -111,7 +135,7 @@ export function apply(ctx) {
|
|
|
111
135
|
directionsPerSolver: 1, // directions shown per solver prompt (1 = own only)
|
|
112
136
|
verifierCount: 3, // independent reviewers per verification
|
|
113
137
|
debateMaxRounds: 5, // debate round cap
|
|
114
|
-
verdictMode: 'forced', // flat = 均衡(0.5) | forced =
|
|
138
|
+
verdictMode: 'forced', // flat = 均衡(0.5) | forced = 对每票**等权**取均值(每票按自己报出的概率贡献;严格 1/0 是绝对投票,其影响通过数值本身拉向端点)。**不再**按"历史准确率"加权:那个统计量统计的是"与本批裁决的一致度",没有后续真值可纠正它。两者都先做近共识判定(同侧且均值≥0.85/≤0.15取均值)
|
|
115
139
|
provider: '',
|
|
116
140
|
model: '',
|
|
117
141
|
solverPersona: '',
|
|
@@ -143,7 +167,7 @@ export function apply(ctx) {
|
|
|
143
167
|
plannerEnabled: true, // false = 完全走内置启发式(规划代理禁用)
|
|
144
168
|
plannerProvider: '', // 规划代理模型 provider(空 = 继承)
|
|
145
169
|
plannerModel: '', // 规划代理模型 id(空 = 继承)
|
|
146
|
-
planMinIntervalMs: 30000, //
|
|
170
|
+
planMinIntervalMs: 30000, // 每一次规划调用的最小间隔(含空计划与「空闲但有工作」)
|
|
147
171
|
plannerMaxFails: 3, // 规划代理连续失败达此值 → 自动降级启发式
|
|
148
172
|
methodKeepIntervalMs: 0, // Method Keeper 定时整理间隔(0 = 事件驱动)
|
|
149
173
|
methodKeepEvery: 5, // 每积累 N 个待沉淀发明/新命题触发一次整理
|
|
@@ -161,6 +185,14 @@ export function apply(ctx) {
|
|
|
161
185
|
let agentRegistry = {}
|
|
162
186
|
let decisionQueue = []
|
|
163
187
|
let verifierAccuracy = {}
|
|
188
|
+
// 验证者票数统计的保留上限(审计 L1):它按**一次性的 childId** 记,每次验证都新增键,
|
|
189
|
+
// 长跑进程里无界增长并整表落盘。它已经不再参与任何加权(见 finalVerdict 的 M8 说明),
|
|
190
|
+
// 只是审计轨迹,所以按插入顺序保留最近 N 条即可。
|
|
191
|
+
const VERIFIER_ACC_KEEP = 200
|
|
192
|
+
function pruneVerifierAccuracy() {
|
|
193
|
+
const keys = Object.keys(verifierAccuracy)
|
|
194
|
+
for (let i = 0; i < keys.length - VERIFIER_ACC_KEEP; i++) delete verifierAccuracy[keys[i]]
|
|
195
|
+
}
|
|
164
196
|
let tasks = {} // verify tasks keyed by 'verify:<rId>'
|
|
165
197
|
let activityLog = []
|
|
166
198
|
let lastReportWrite = 0
|
|
@@ -223,9 +255,9 @@ export function apply(ctx) {
|
|
|
223
255
|
{ name: 'maxParallelThreshold', type: 'integer', description: '全局最大并发子代理轮数(新派发前须满足 active < 阈值)', suggestion: 4 },
|
|
224
256
|
{ name: 'solverMaxRounds', type: 'integer', description: '每个求解方向的最大迭代轮数', suggestion: 3 },
|
|
225
257
|
{ name: 'directionsPerSolver', type: 'integer', description: '每个 solver 提示词附带的其他活跃方向摘要数量:1 = 只看自己方向', suggestion: 1 },
|
|
226
|
-
{ name: 'verifierCount', type: 'integer', description: '
|
|
258
|
+
{ name: 'verifierCount', type: 'integer', description: '每个验证对象的独立验证器数量(实际下界为 2:少于 2 份独立评审一律不定论,见 minVotes)', suggestion: 3 },
|
|
227
259
|
{ name: 'debateMaxRounds', type: 'integer', description: '验证辩论(交流群)最大轮数', suggestion: 5 },
|
|
228
|
-
{ name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: '裁决模式:flat =
|
|
260
|
+
{ name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: '裁决模式:flat = 均衡机制(分歧时 0.5);forced = 强制裁决(对每票等权取均值;严格 1/0 是绝对投票,其影响通过数值本身拉向端点。**不按"历史准确率"加权**——该统计量统计的是与本批裁决的一致度,没有后续真值可纠正);两者都先做近共识判定(同侧且均值≥0.85/≤0.15取均值,修复 v2 flat 误判)', suggestion: 'forced' },
|
|
229
261
|
{ name: 'provider', type: 'string', description: '子代理模型 provider(空 = 继承根代理)', suggestion: '' },
|
|
230
262
|
{ name: 'model', type: 'string', description: '子代理模型 id(空 = 继承根代理)', suggestion: '' },
|
|
231
263
|
{ name: 'solverPersona', type: 'string', description: '注入每个求解器提示词开头的人格/要求', suggestion: '' },
|
|
@@ -256,7 +288,7 @@ export function apply(ctx) {
|
|
|
256
288
|
{ name: 'plannerEnabled', type: 'boolean', description: 'false = 完全走内置启发式调度(规划代理禁用)', suggestion: true },
|
|
257
289
|
{ name: 'plannerProvider', type: 'string', description: '规划代理模型 provider(空 = 继承根代理)', suggestion: '' },
|
|
258
290
|
{ name: 'plannerModel', type: 'string', description: '规划代理模型 id(空 = 继承根代理)', suggestion: '' },
|
|
259
|
-
{ name: 'planMinIntervalMs', type: 'integer', description: '
|
|
291
|
+
{ name: 'planMinIntervalMs', type: 'integer', description: '每一次规划调用的最小间隔(毫秒);对**每一次**规划调用都生效(含空计划、含「空闲但仍有工作」,不再有空闲绕过)', suggestion: 30000 },
|
|
260
292
|
{ name: 'plannerMaxFails', type: 'integer', description: '规划代理连续失败达此值 → 自动降级启发式', suggestion: 3 },
|
|
261
293
|
{ name: 'methodKeepIntervalMs', type: 'integer', description: 'Method Keeper 定时整理间隔(0 = 事件驱动)', suggestion: 0 },
|
|
262
294
|
{ name: 'methodKeepEvery', type: 'integer', description: '每积累 N 个待沉淀发明/新命题触发一次整理', suggestion: 5 },
|
|
@@ -345,7 +377,10 @@ export function apply(ctx) {
|
|
|
345
377
|
* 与 `Verified/Lean/`(归档证明,与定论卡片同处 Verified/,一眼可见"这条结论的证明在哪"),
|
|
346
378
|
* 以及**项目树之外**的全局可复用库 `<VibeMath 根>/Formal/{Lib,Proved}`(跨项目复用是核心收益)。
|
|
347
379
|
*/
|
|
348
|
-
|
|
380
|
+
// Progress_Logs/ 必须和别的骨架目录**一起**建出来(审计 L8):vibe_math_report 会写
|
|
381
|
+
// `Progress_Logs/report.json`,README 也把它列为布局的一部分,但此前它只靠 writeText 的隐式
|
|
382
|
+
// mkdir 兜底——在宿主不支持删除/创建的路径上(runShell 失败)报告目录就时有时无。
|
|
383
|
+
async function ensureDirs() { const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Progress_Logs', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Verified/Lean', 'Formal', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
|
|
349
384
|
async function removeFile(rel) { const base = frameworkRoot(); return await runShell(rmCmd(base + '/' + rel)) }
|
|
350
385
|
|
|
351
386
|
// ================= settings =================
|
|
@@ -424,7 +459,11 @@ export function apply(ctx) {
|
|
|
424
459
|
// ================= md soft-spec helpers =================
|
|
425
460
|
// 软规范:对象 md 头部锚点行(唯一强制部分)+ 正文自由叙述。调度器只解析
|
|
426
461
|
// 头部锚点与条目标题行(### 解法/证明/证伪 N|标题|概率X|状态Y),从不解析正文散文。
|
|
427
|
-
function anchorLine(k, v) {
|
|
462
|
+
function anchorLine(k, v) {
|
|
463
|
+
// 锚点行是**逐行**格式,parseAnchors 的 `.*$` 不跨行(无 `s` 标志):值里含换行会让这一行被
|
|
464
|
+
// 劈成两行,第二行既不是锚点也会污染下一个锚点/段落的解析。compose 侧统一折成空格(审计 L6)。
|
|
465
|
+
return '- ' + k + ': ' + String(v == null ? '' : v).replace(/[\r\n]+/g, ' ')
|
|
466
|
+
}
|
|
428
467
|
// 解析条目标题行 + 其后正文,直到下一个 ### / ## 标题。返回 [{heading, text}]
|
|
429
468
|
/**
|
|
430
469
|
* 按顺序切出正文里的所有 `## ` 顶层段。用于"无损往返":compose 只重新生成自己管理的
|
|
@@ -460,6 +499,12 @@ export function apply(ctx) {
|
|
|
460
499
|
lines.push(anchorLine('依赖', JSON.stringify(p.依赖 || [])))
|
|
461
500
|
lines.push(anchorLine('被依赖', JSON.stringify(p.被依赖 || [])))
|
|
462
501
|
lines.push(anchorLine('来源', p.来源 || '原始'))
|
|
502
|
+
// 关联锚点(H4):这两行此前**只读不写** ⇒ 任何一次重载(resume / vibe_math_index /
|
|
503
|
+
// set_project / 进程重启都会按文件名重新解析卡片)之后,判断问题与它的临时假设命题
|
|
504
|
+
// 全部失联:processStatusUpdates 的"收口僵尸判断问题"分支、settleVerdict 的"回写源命题"
|
|
505
|
+
// 分支、Verified 卡的来源字段都只在同进程内有效。写出来才是真正的持久关联。
|
|
506
|
+
if (p.判断命题) lines.push(anchorLine('判断命题', p.判断命题))
|
|
507
|
+
if (p.来源命题) lines.push(anchorLine('来源命题', p.来源命题))
|
|
463
508
|
lines.push(anchorLine('计划', p.计划 || ''))
|
|
464
509
|
lines.push('')
|
|
465
510
|
lines.push('## 陈述')
|
|
@@ -471,7 +516,7 @@ export function apply(ctx) {
|
|
|
471
516
|
if (sols.length === 0) lines.push('(暂无解法候选)')
|
|
472
517
|
else for (let i = 0; i < sols.length; i++) {
|
|
473
518
|
const s = sols[i]
|
|
474
|
-
lines.push('### 解法 ' + (i + 1) + '|' + (s.title || '解法' + (i + 1)) + '|概率' + (s.prob != null ? s.prob : 0.5) + '|状态' + (s.status || '未定论'))
|
|
519
|
+
lines.push('### 解法 ' + (i + 1) + '|' + escField(s.title || '解法' + (i + 1)) + '|概率' + probText(s.prob != null ? s.prob : 0.5) + '|状态' + escField(s.status || '未定论'))
|
|
475
520
|
lines.push(s.text || '')
|
|
476
521
|
lines.push('')
|
|
477
522
|
}
|
|
@@ -480,7 +525,7 @@ export function apply(ctx) {
|
|
|
480
525
|
function parseProblemMd(id, text) {
|
|
481
526
|
const { head, body } = splitHeader(text)
|
|
482
527
|
const a = parseAnchors(head)
|
|
483
|
-
const sols = parseEntries(body,
|
|
528
|
+
const sols = parseEntries(body, entryRe('解法')).map(function (e) { return { title: unescField(e.title), prob: clamp01(e.prob), status: unescField(e.status), text: e.text } })
|
|
484
529
|
return {
|
|
485
530
|
id: id, 标题: a['标题'] || id, 状态: a['状态'] || '求解中',
|
|
486
531
|
优先级: (a['优先级'] === 'never' ? 'never' : (Number(a['优先级']) || 1)),
|
|
@@ -505,6 +550,10 @@ export function apply(ctx) {
|
|
|
505
550
|
if (formalAnchorLine(p.id)) lines.push(anchorLine('形式化', formalAnchorLine(p.id)))
|
|
506
551
|
lines.push(anchorLine('优先级', p.优先级 == null ? 1 : p.优先级))
|
|
507
552
|
lines.push(anchorLine('依赖', JSON.stringify(p.依赖 || [])))
|
|
553
|
+
// 来源锚点(H4):与问题卡的 判断命题/来源命题 对称——不写盘的话,重载后
|
|
554
|
+
// 「引理来自哪个问题/方向」丢失,Verified 命题卡的 `- 来源:` 也会永久为空。
|
|
555
|
+
if (p.来源问题) lines.push(anchorLine('来源问题', p.来源问题))
|
|
556
|
+
if (p.来源方向) lines.push(anchorLine('来源方向', p.来源方向))
|
|
508
557
|
if (p.价值关键性 != null) lines.push(anchorLine('价值/关键性', p.价值关键性))
|
|
509
558
|
lines.push('')
|
|
510
559
|
lines.push('## 陈述')
|
|
@@ -515,7 +564,7 @@ export function apply(ctx) {
|
|
|
515
564
|
if (proofs.length === 0) lines.push('(暂无证明尝试)')
|
|
516
565
|
else for (let i = 0; i < proofs.length; i++) {
|
|
517
566
|
const s = proofs[i]
|
|
518
|
-
lines.push('### 证明 ' + (i + 1) + '|' + (s.title || '证明' + (i + 1)) + '|概率' + (s.prob != null ? s.prob : 0.5) + '|状态' + (s.status || '未定论'))
|
|
567
|
+
lines.push('### 证明 ' + (i + 1) + '|' + escField(s.title || '证明' + (i + 1)) + '|概率' + probText(s.prob != null ? s.prob : 0.5) + '|状态' + escField(s.status || '未定论'))
|
|
519
568
|
lines.push(s.text || '')
|
|
520
569
|
lines.push('')
|
|
521
570
|
}
|
|
@@ -524,7 +573,7 @@ export function apply(ctx) {
|
|
|
524
573
|
if (refutes.length === 0) lines.push('(暂无证伪尝试)')
|
|
525
574
|
else for (let i = 0; i < refutes.length; i++) {
|
|
526
575
|
const s = refutes[i]
|
|
527
|
-
lines.push('### 证伪 ' + (i + 1) + '|' + (s.title || '证伪' + (i + 1)) + '|概率' + (s.prob != null ? s.prob : 0.5) + '|状态' + (s.status || '未定论'))
|
|
576
|
+
lines.push('### 证伪 ' + (i + 1) + '|' + escField(s.title || '证伪' + (i + 1)) + '|概率' + probText(s.prob != null ? s.prob : 0.5) + '|状态' + escField(s.status || '未定论'))
|
|
528
577
|
lines.push(s.text || '')
|
|
529
578
|
lines.push('')
|
|
530
579
|
}
|
|
@@ -533,8 +582,8 @@ export function apply(ctx) {
|
|
|
533
582
|
function parsePropositionMd(id, text) {
|
|
534
583
|
const { head, body } = splitHeader(text)
|
|
535
584
|
const a = parseAnchors(head)
|
|
536
|
-
const proofs = parseEntries(body,
|
|
537
|
-
const refutes = parseEntries(body,
|
|
585
|
+
const proofs = parseEntries(body, entryRe('证明')).map(function (e) { return { title: unescField(e.title), prob: clamp01(e.prob), status: unescField(e.status), text: e.text } })
|
|
586
|
+
const refutes = parseEntries(body, entryRe('证伪')).map(function (e) { return { title: unescField(e.title), prob: clamp01(e.prob), status: unescField(e.status), text: e.text } })
|
|
538
587
|
return {
|
|
539
588
|
id: id, 标题: a['标题'] || id, 状态: a['状态'] || '未定论',
|
|
540
589
|
概率: clamp01(a['概率'] != null ? Number(a['概率']) : 0.5),
|
|
@@ -768,6 +817,8 @@ export function apply(ctx) {
|
|
|
768
817
|
function allProblems() { return Array.from(problems.values()) }
|
|
769
818
|
function allPropos() { return Array.from(propos.values()) }
|
|
770
819
|
function depResolved(id) {
|
|
820
|
+
// M9:对象键一律是 idSafe(id)(见各写入点),依赖是模型写的原始 id ⇒ 查表前必须走同一把归一化。
|
|
821
|
+
id = idSafe(id)
|
|
771
822
|
if (problems.has(id)) { const q = problems.get(id); return q.状态 === '已解决' || q.优先级 === 'never' }
|
|
772
823
|
if (propos.has(id)) { const p = propos.get(id); return p.概率 === 1 || p.概率 === 0 || p.优先级 === 'never' } // never = 主动弃权,视为依赖已满足,避免等待依赖死锁
|
|
773
824
|
return true // unknown dependency: treat as resolved (conservative)
|
|
@@ -804,7 +855,14 @@ export function apply(ctx) {
|
|
|
804
855
|
const va = await readJson('State/verifier_accuracy.json'); if (va) verifierAccuracy = va
|
|
805
856
|
const tk = await readJson('State/tasks.json'); if (tk) tasks = tk
|
|
806
857
|
const er = await readJson('State/explorer_retries.json'); if (er) explorerRetries = er
|
|
807
|
-
const pq = await readJson('State/plans.json')
|
|
858
|
+
const pq = await readJson('State/plans.json')
|
|
859
|
+
// 计划队列的**跨进程**语义(审计 L9):planQueue 是"本进程内跨 tick"的队列,里面的动作指的是
|
|
860
|
+
// 当时那个进程/那次运行的对象与状态。上一进程留下的队列在恢复时重放,轻则对已不存在的 target
|
|
861
|
+
// 空转,重则把过期计划的动作施加到新状态上。进程 epoch 是权威判据:只有本进程写下的队列才恢复。
|
|
862
|
+
if (pq && Array.isArray(pq.queued)) {
|
|
863
|
+
if (pq.epoch === processEpoch) planQueue = pq.queued
|
|
864
|
+
else if (pq.queued.length > 0) { planQueue = []; logActivity('plan', '丢弃上一进程留下的 ' + pq.queued.length + ' 条计划动作(跨进程重放会施加过期计划)') }
|
|
865
|
+
}
|
|
808
866
|
const ml = await readJson('State/method_log.json'); if (ml) methodLog = Object.assign({ pendingInventions: [], keepCount: 0, lastKeepAt: 0 }, ml)
|
|
809
867
|
const pl = await readJson('State/project_lock.json'); if (pl) projectLock = Object.assign({ sessionId: '', at: 0 }, pl)
|
|
810
868
|
const lp = await readJson('State/last_plan.json'); if (lp) lastPlanSummary = lp
|
|
@@ -825,7 +883,7 @@ export function apply(ctx) {
|
|
|
825
883
|
await writeJson('State/verifier_accuracy.json', verifierAccuracy)
|
|
826
884
|
await writeJson('State/tasks.json', tasks)
|
|
827
885
|
await writeJson('State/explorer_retries.json', explorerRetries)
|
|
828
|
-
await writeJson('State/plans.json', { queued: planQueue })
|
|
886
|
+
await writeJson('State/plans.json', { queued: planQueue, epoch: processEpoch })
|
|
829
887
|
await writeJson('State/method_log.json', methodLog)
|
|
830
888
|
await writeJson('State/project_lock.json', projectLock)
|
|
831
889
|
await writeJson('State/archived_journals.json', archivedJ)
|
|
@@ -927,7 +985,10 @@ export function apply(ctx) {
|
|
|
927
985
|
',问题 ' + report.problems.solved + '/' + report.problems.total + ' 已解决,命题 ' + report.propositions.resolved + '/' + report.propositions.total + ' 已定论,' +
|
|
928
986
|
'活跃代理轮数=' + report.activeCount + ',待人工决策=' + report.pendingDecisions.length + ',待执行计划=' + report.queuedPlanActions + '。' +
|
|
929
987
|
'请调用 vibe_math_report 汇总当前进展及各代理状态,并用人话简要汇报(不打断用户,简短即可)。'
|
|
930
|
-
|
|
988
|
+
// 来源 kind 必须是**已声明**的:`MessageSourceMap` 是 merge-extensible 联合,但没有共享的
|
|
989
|
+
// catch-all `plugin` kind(审计 L5/dsh-llm message.d.ts),{kind:'plugin'} 是契约外形状。
|
|
990
|
+
// role 本来就是 'user',正文自带 "[Vibe Math V3] 进度更新" 的真署名,故用核心声明的 {kind:'user'}。
|
|
991
|
+
rootAgent.followup({ id: uuid(), role: 'user', content: [textBlock(text)], source: { kind: 'user' } })
|
|
931
992
|
lastPushReport = now()
|
|
932
993
|
} catch (e) {
|
|
933
994
|
console.error('vibe-math-v3: push report failed: ' + String((e && e.message) || e))
|
|
@@ -1053,7 +1114,8 @@ export function apply(ctx) {
|
|
|
1053
1114
|
function kcObjectModels() {
|
|
1054
1115
|
return '\n2) OBJECT MODELS(md 卡片,软规范:头部锚点行 + 正文自由叙述):\n' +
|
|
1055
1116
|
'- 问题卡 Problems/<id>.md:{ 标题, ID, 类型:问题, 状态:原始|求解中|等待依赖|已解决|死路, 优先级, 依赖:[], 被依赖:[], 来源:原始|后生, 计划(由调度器按规划代理的计划自动更新:一句话说明下一轮安排), ## 陈述(完整问题陈述,每个记号/对象都要完整定义), ## 来源与动机(后生问题:产生流程/动机/如何回填主线), ## 解法候选(### 解法 N|标题|概率X|状态Y + 叙述式完整解法)}。\n' +
|
|
1056
|
-
'- 命题卡 Propos/<分类>/<id>.md:{ 标题, ID, 类型:命题, 状态:未定论|已验证·真|已验证·假, 概率, 优先级, 依赖:[], ## 陈述(完整), ## 证明尝试(### 证明 N|…|概率X|状态Y), ## 证伪尝试(### 证伪 N|…|概率X|状态Y)}。\n' +
|
|
1117
|
+
'- 命题卡 Propos/<分类>/<id>.md:{ 标题, ID, 类型:命题, 状态:未定论|已验证·真|已验证·假, 概率, 优先级, 依赖:[], 价值/关键性, 来源问题, 来源方向, ## 陈述(完整), ## 证明尝试(### 证明 N|…|概率X|状态Y), ## 证伪尝试(### 证伪 N|…|概率X|状态Y)}。\n' +
|
|
1118
|
+
'- `价值/关键性 ∈ [0,1]`(M1):这条命题对**整个项目主线**有多关键——若它成立/被证伪,能改变多少后续方向。≥ promoteValueThreshold(默认 0.7)且仍未定论(概率∈(0,1))的命题会被自动晋升成「判断下述命题是否成立:…」问题,由求解器专门证明/证伪并把结果回写源命题。你不填就默认 0.5(永不晋升)。**只有你(写卡的代理)能设这个值**——请在你认为"这条引理/猜想值得单独立项"时显式写上。\n' +
|
|
1057
1119
|
'- 证明/证伪尝试语义:`## 证明尝试`=为证实而写的论证;`## 证伪尝试`=专门反驳/反例的论证。**失败的"找反例未果"/sanity check 是支持性证据,不属于证伪尝试**;不要写入 `## 证伪尝试`(否则系统会当作待验证的反驳去验证)。对仍未完成的证明/证伪,明确标注缺口而非伪装完成。\n' +
|
|
1058
1120
|
'- 方法卡 Methods/<id>.md:{ 标题, ID, 类型:方法, 状态:经验|应用验证|含已验证断言, 可信断言:[](只允许已进 Verified/ 的 ID), 上级体系/子方法/相关, 适用场景, ## 核心内容, ## 定义与记号, ## 应用记录, ## 改进历史 }。\n' +
|
|
1059
1121
|
'- 收口规则:某个解法/证明/证伪 概率=1 → 问题已解决 / 命题已验证(状态/概率锚点由调度器改写)。\n'
|
|
@@ -1079,7 +1141,7 @@ export function apply(ctx) {
|
|
|
1079
1141
|
function kcSolverFiles() {
|
|
1080
1142
|
return '你的归属文件:\n' +
|
|
1081
1143
|
'- 求解器:把该方向的完整叙述(本轮进展/子路线/可行性信号/教训/完整解法文本)写进 `Progress/<问题id>/<方向id>.md`;聚合索引 `Progress/<问题id>.md` 由调度器维护,不要动它。\n' +
|
|
1082
|
-
'- 新引理:写一张完整命题卡到 `Propos/<分类>/<p-id>.md`,含锚点 `- 标题:`、`- ID
|
|
1144
|
+
'- 新引理:写一张完整命题卡到 `Propos/<分类>/<p-id>.md`,含锚点 `- 标题:`、`- ID/类型/状态/概率/优先级/价值关键性` 与 `## 陈述`;证明写进 `### 证明 1|标题|概率X|状态Y` 段落(完整证明文本是验证必需,否则验证器只能验裸命题)。`- 价值/关键性: <0..1>` 表示这条引理对项目主线的关键程度(≥ 阈值会被自动晋升为独立问题),别省。\n'
|
|
1083
1145
|
}
|
|
1084
1146
|
// 方法整理代理专属:方法卡
|
|
1085
1147
|
function kcKeeperFiles() {
|
|
@@ -1207,7 +1269,7 @@ export function apply(ctx) {
|
|
|
1207
1269
|
head += '\nLEMMA RULES: every lemma you register MUST carry a complete proof in `lemmas[].proof` (and in the card\'s `## 证明尝试`). If a claim is only partly argued, do NOT register it as a finished lemma — either prove it fully or record it as an explicit gap/conjecture stating the missing step, so the verifier knows exactly what is (and is not) being claimed. Incomplete "lemmas" waste verification and can mislead.\n'
|
|
1208
1270
|
head += '\nOUTPUT CONTRACT — pick ONE channel. Write content into Markdown; only lightweight scheduling metadata (and verification-required proofs) cross the machine reply.\n' +
|
|
1209
1271
|
'CHANNEL A (recommended, you can write files): write the full round narrative into `Progress/' + q.id + '/' + dir.id + '.md` and each new lemma card into `Propos/<分类>/<id>.md`, then reply ONLY this metadata object:\n' +
|
|
1210
|
-
'{"meta":{"kind":"solver","qid":"' + q.id + '","dirId":"' + dir.id + '","round":' + round + ',"survival":0.5,"status":"continue|success|dead-end","dead_end_reason":"... or null","lemmas":[{"id":"p-...","title":"...","statement":"...","proof":"<完整证明文本,供验证器核验>","prob":0.6,"分类":"<引理卡目录名,必须与你要写入的 Propos/<分类>/ 目录严格一致>","优先级":1}],"methods_used":[{"id":"m-...","效果":"...","建议":"..."}],"new_inventions":[{"类型":"...","标题":"...","内容描述":"...","是否已入库":false}],"solution_prob":0.85,"solution_text":"<完整解法文本,或 null>","sub_questions":[{"q_sub_title":"...","q_sub_statement":"完整问题陈述(含所有对象/定义)","assumption_title":"p_{q-tmp} 标题","assumption_statement":"完整假设陈述(含所有定义)"}]}}\n' +
|
|
1272
|
+
'{"meta":{"kind":"solver","qid":"' + q.id + '","dirId":"' + dir.id + '","round":' + round + ',"survival":0.5,"status":"continue|success|dead-end","dead_end_reason":"... or null","lemmas":[{"id":"p-...","title":"...","statement":"...","proof":"<完整证明文本,供验证器核验>","prob":0.6,"价值/关键性":0.5,"分类":"<引理卡目录名,必须与你要写入的 Propos/<分类>/ 目录严格一致>","优先级":1}],"methods_used":[{"id":"m-...","效果":"...","建议":"..."}],"new_inventions":[{"类型":"...","标题":"...","内容描述":"...","是否已入库":false}],"solution_prob":0.85,"solution_text":"<完整解法文本,或 null>","sub_questions":[{"q_sub_title":"...","q_sub_statement":"完整问题陈述(含所有对象/定义)","assumption_title":"p_{q-tmp} 标题","assumption_statement":"完整假设陈述(含所有定义)"}]}}\n' +
|
|
1211
1273
|
'CHANNEL B (your file tools are unavailable): put the content you would have written into __writes and carry the same meta:\n' +
|
|
1212
1274
|
'{"__writes":[{"path":"Progress/' + q.id + '/' + dir.id + '.md","content":"<完整本轮叙述>"}],"meta":{"kind":"solver","qid":"' + q.id + '","dirId":"' + dir.id + '",...同上 meta 字段...}}\n' +
|
|
1213
1275
|
'区分规则:methods_used 只能填**已存在的方法卡 ID**(m-…,来自 AVAILABLE METHODS 列表)——引用你自己刚想出的新方法/新技巧不属于 methods_used,请如实填入 new_inventions(它会由 Method Keeper 蒸馏建卡);不要把方法名/标题当 id 填进 methods_used。'
|
|
@@ -1281,7 +1343,24 @@ export function apply(ctx) {
|
|
|
1281
1343
|
}
|
|
1282
1344
|
|
|
1283
1345
|
// ================= decisions (manual/auto) =================
|
|
1284
|
-
|
|
1346
|
+
// 已结清决策的保留条数(审计 L1):decisionQueue 以前只把 status 置 resolved、永不裁剪,
|
|
1347
|
+
// 且整表落盘到 State/decision_queue.json,长跑进程里它是第二处无界增长。保留最近 N 条已结清记录
|
|
1348
|
+
// 作为审计轨迹(list_decisions 只列 pending,所以裁剪不影响任何读取路径),pending 一律不裁。
|
|
1349
|
+
const RESOLVED_DECISION_KEEP = 50
|
|
1350
|
+
function pruneDecisionQueue() {
|
|
1351
|
+
let resolved = 0
|
|
1352
|
+
for (let i = 0; i < decisionQueue.length; i++) if (decisionQueue[i] && decisionQueue[i].status !== 'pending') resolved++
|
|
1353
|
+
let drop = resolved - RESOLVED_DECISION_KEEP
|
|
1354
|
+
if (drop <= 0) return
|
|
1355
|
+
const kept = []
|
|
1356
|
+
for (let i = 0; i < decisionQueue.length; i++) {
|
|
1357
|
+
const d = decisionQueue[i]
|
|
1358
|
+
if (d && d.status !== 'pending' && drop > 0) { drop--; continue }
|
|
1359
|
+
kept.push(d)
|
|
1360
|
+
}
|
|
1361
|
+
decisionQueue = kept
|
|
1362
|
+
}
|
|
1363
|
+
function enqueueDecision(node, contextText, data) { const d = { id: uuid(), node: node, context: contextText, data: data, status: 'pending', resolution: null, createdAt: now() }; decisionQueue.push(d); pruneDecisionQueue(); return d }
|
|
1285
1364
|
async function maybeGate(node, contextText, data, autoFn) { if (params.mode === 'auto') return await autoFn(data); const d = enqueueDecision(node, contextText, data); setGate(d, node); logActivity('gate', node + ': ' + contextText); await saveAll(); return { gated: true, decisionId: d.id } }
|
|
1286
1365
|
/**
|
|
1287
1366
|
* 设置唯一闸门,并把被它取代的旧决策**立即结清**。
|
|
@@ -1372,6 +1451,8 @@ export function apply(ctx) {
|
|
|
1372
1451
|
await reconcileVerify()
|
|
1373
1452
|
await executePlanQueue()
|
|
1374
1453
|
await maybePlan()
|
|
1454
|
+
// 方法整理排在终止判定之前:到期的方法整理是**真实工作**,先把它派出去,
|
|
1455
|
+
// 再让 checkTermination 看到"有在途的 method-keeper"从而不误判停机(H1 修复的配套)。
|
|
1375
1456
|
await maybeMethodKeepFallback()
|
|
1376
1457
|
await maybePushReport(false)
|
|
1377
1458
|
await maybeWriteReport(false)
|
|
@@ -1570,7 +1651,9 @@ export function apply(ctx) {
|
|
|
1570
1651
|
}
|
|
1571
1652
|
function verifyTaskBusy(rId) { if (tasks['verify:' + rId]) return true; return Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.role === 'verifier' && m.rId === rId }) }
|
|
1572
1653
|
async function createVerifyTask(c) {
|
|
1573
|
-
|
|
1654
|
+
// M9:验证 id 由对象 id 派生(`r-<对象id>[-sN]`),对象键已归一化 ⇒ rId 也归一化,
|
|
1655
|
+
// 否则原始 id 里的路径分隔符会被拼进 Logs/Verification/<rId>_<ts>.json 造出子目录。
|
|
1656
|
+
const rId = idSafe(c.rId)
|
|
1574
1657
|
if (verifyTaskBusy(rId)) return false
|
|
1575
1658
|
// require 门禁的**防空转**(契约 §2「对象留在原库,可形式化后再次提议」):已经被记为
|
|
1576
1659
|
// 「形式化待办」、而形式化又还没补齐的对象不再重复表决——重复表决只会一次又一次被同一道
|
|
@@ -1657,15 +1740,18 @@ export function apply(ctx) {
|
|
|
1657
1740
|
// 规划代理在途时不再重复调用
|
|
1658
1741
|
const plannerInFlight = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.role === 'planner' })
|
|
1659
1742
|
if (plannerInFlight) return
|
|
1660
|
-
// 有"可调度工作"= 有待解问题方向可推进 或 有验证候选 或
|
|
1661
|
-
//
|
|
1662
|
-
//
|
|
1743
|
+
// 有"可调度工作"= 有待解问题方向可推进 或 有验证候选 或 有**到期**的方法整理 或 有待执行计划。
|
|
1744
|
+
// H1 修复:这里必须用真正的到期判定 methodKeepDue(),而不是"待沉淀发明非空"。
|
|
1745
|
+
// 旧条件在 pendingInventions ∈ [1, methodKeepEvery-1] 时恒真,于是**没有任何工作**的每一
|
|
1746
|
+
// 个 tick 都派生一个 planner:空计划不留痕(lastPlanSummary 不写)、冷却又被"无在途工作"
|
|
1747
|
+
// 绕过、终止判定又被刚派出的 planner 挡掉 ⇒ 按 tick 计费的 planner 热循环永不终止。
|
|
1748
|
+
// 待沉淀发明到期时由 maybeMethodKeepFallback() 处理,不需要(也不应该)叫 planner。
|
|
1663
1749
|
const verifyWork = (await buildVerifyCandidates()).length > 0
|
|
1664
|
-
if (!hasSchedulableWork() && !verifyWork &&
|
|
1665
|
-
//
|
|
1750
|
+
if (!hasSchedulableWork() && !verifyWork && !methodKeepDue() && planQueue.length === 0) return
|
|
1751
|
+
// 冷却对**每一次**规划调用生效(含空计划):空计划同样是一次真实的模型调用,必须退避。
|
|
1752
|
+
// 旧代码用 hasInflight 跳过冷却,本意是"空闲且有工作时不空转",但那恰好是 H1 的放大器。
|
|
1666
1753
|
const cooldown = Number(params.planMinIntervalMs) || 0
|
|
1667
|
-
|
|
1668
|
-
if (cooldown > 0 && hasInflight && (now() - lastPlanAt) < cooldown) return
|
|
1754
|
+
if (cooldown > 0 && (now() - lastPlanAt) < cooldown) return
|
|
1669
1755
|
await callPlanner()
|
|
1670
1756
|
}
|
|
1671
1757
|
async function callPlanner() {
|
|
@@ -1742,7 +1828,7 @@ export function apply(ctx) {
|
|
|
1742
1828
|
if (act === 'spawn') {
|
|
1743
1829
|
const role = String(a.role || '')
|
|
1744
1830
|
if (role === 'explorer') {
|
|
1745
|
-
const q = problems.get(String(a.target || ''))
|
|
1831
|
+
const q = problems.get(idSafe(String(a.target || ''))) // M9:问题键已归一化
|
|
1746
1832
|
if (!q || q.状态 === '已解决' || q.优先级 === 'never') continue
|
|
1747
1833
|
const dirs = getDirState(q.id)
|
|
1748
1834
|
const allExhausted = dirs.length > 0 && dirs.every(function (d) { return d.status === 'dead-end' || d.status === 'success' })
|
|
@@ -1751,7 +1837,7 @@ export function apply(ctx) {
|
|
|
1751
1837
|
if (busy) continue
|
|
1752
1838
|
if ((explorerRetries[q.id] || 0) >= (Number(params.maxExplorerRetries) || 3)) continue
|
|
1753
1839
|
} else if (role === 'solver') {
|
|
1754
|
-
const q = problems.get(String(a.target || ''))
|
|
1840
|
+
const q = problems.get(idSafe(String(a.target || ''))) // M9:问题键已归一化
|
|
1755
1841
|
if (!q || q.状态 === '已解决' || q.优先级 === 'never' || q.状态 === '等待依赖') continue
|
|
1756
1842
|
const dir = (getDirState(q.id) || []).find(function (d) { return d.id === String(a.direction || '') })
|
|
1757
1843
|
if (!dir || dir.status !== 'active') continue
|
|
@@ -1759,11 +1845,13 @@ export function apply(ctx) {
|
|
|
1759
1845
|
const running = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.qid === q.id && m.direction === dir.id && m.role === 'solver' })
|
|
1760
1846
|
if (running) continue
|
|
1761
1847
|
} else if (role === 'verifier') {
|
|
1762
|
-
const rId = String(a.target || '')
|
|
1848
|
+
const rId = idSafe(String(a.target || '')) // M9:与 createVerifyTask 同一把归一化
|
|
1763
1849
|
if (!rId) continue
|
|
1764
1850
|
if (verifyTaskBusy(rId)) continue
|
|
1765
1851
|
const cands = await buildVerifyCandidates()
|
|
1766
|
-
if (!cands.some(function (c) { return c.rId === rId })) continue
|
|
1852
|
+
if (!cands.some(function (c) { return idSafe(c.rId) === rId })) continue
|
|
1853
|
+
out.push({ action: 'spawn', role: role, target: rId, direction: a.direction || '', reason: String(a.reason || '') })
|
|
1854
|
+
continue
|
|
1767
1855
|
} else if (role === 'method-keeper') {
|
|
1768
1856
|
// 无目标;允许(由代码兜底去重:一次只允许一个 method-keeper)
|
|
1769
1857
|
const running = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.role === 'method-keeper' })
|
|
@@ -1775,10 +1863,10 @@ export function apply(ctx) {
|
|
|
1775
1863
|
if (!agentRegistry[cid]) continue
|
|
1776
1864
|
out.push({ action: 'interrupt', childId: cid, reason: String(a.reason || '') })
|
|
1777
1865
|
} else if (act === 'promote') {
|
|
1778
|
-
const p = propos.get(String(a.target || ''))
|
|
1866
|
+
const p = propos.get(idSafe(String(a.target || ''))) // M9:命题键已归一化
|
|
1779
1867
|
if (!p || p.概率 === 1 || p.概率 === 0 || p.优先级 === 'never' || p.在问题清单) continue
|
|
1780
1868
|
if (Number(p.价值关键性) < Number(params.promoteValueThreshold)) continue
|
|
1781
|
-
out.push({ action: 'promote', target:
|
|
1869
|
+
out.push({ action: 'promote', target: p.id, reason: String(a.reason || '') })
|
|
1782
1870
|
} else if (act === 'wait' || act === 'continue' || act === 'stop') {
|
|
1783
1871
|
out.push(Object.assign({ action: act }, a.childId ? { childId: String(a.childId) } : {}, a.target ? { target: String(a.target) } : {}, { reason: String(a.reason || '') }))
|
|
1784
1872
|
}
|
|
@@ -1974,7 +2062,9 @@ export function apply(ctx) {
|
|
|
1974
2062
|
if (parsed.lessons) dir.lessons = (dir.lessons || []).concat(parsed.lessons)
|
|
1975
2063
|
if (parsed.blockers) dir.blockers = (dir.blockers || []).concat(parsed.blockers)
|
|
1976
2064
|
if (parsed.dead_end_reason) dir.dead_end_reason = parsed.dead_end_reason
|
|
2065
|
+
// M11:`survival_probability`(遗留扁平名)与 `survival`(提示词名)都接受。
|
|
1977
2066
|
if (typeof parsed.survival_probability === 'number') dir.survival = clamp01(parsed.survival_probability)
|
|
2067
|
+
else if (typeof parsed.survival === 'number') dir.survival = clamp01(parsed.survival)
|
|
1978
2068
|
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 || '' }) } } }
|
|
1979
2069
|
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) } } }
|
|
1980
2070
|
// journal narrative (论文式续写:把本轮叙述追加进研究日志)
|
|
@@ -1988,11 +2078,16 @@ export function apply(ctx) {
|
|
|
1988
2078
|
await consumeMethodFeedback(parsed, { qid: qid, dirId: dirId })
|
|
1989
2079
|
}
|
|
1990
2080
|
if (status === 'success') {
|
|
1991
|
-
|
|
2081
|
+
// M11:遗留扁平通道的字段名(solution / solution_probability)与提示词教的名字
|
|
2082
|
+
// (solution_text / solution_prob)不一致 ⇒ 严格按提示词回写的 solver 会掉进 else 分支,
|
|
2083
|
+
// 在轮次上限处被判成"claimed success without solution"并把方向写死。两套名字都接受。
|
|
2084
|
+
const flatSolution = (parsed && parsed.solution != null) ? parsed.solution : (parsed ? parsed.solution_text : undefined)
|
|
2085
|
+
const flatSolutionProb = (parsed && parsed.solution_probability != null) ? parsed.solution_probability : (parsed ? parsed.solution_prob : undefined)
|
|
2086
|
+
if (parsed && flatSolution) {
|
|
1992
2087
|
dir.status = 'success'
|
|
1993
2088
|
delete agentRegistry[childId]
|
|
1994
2089
|
logActivity('solver', qid + '/' + dirId + ' success at round ' + meta.round)
|
|
1995
|
-
await addSolution(qid,
|
|
2090
|
+
await addSolution(qid, flatSolution, flatSolutionProb)
|
|
1996
2091
|
} else {
|
|
1997
2092
|
if (meta.round >= params.solverMaxRounds) {
|
|
1998
2093
|
dir.status = 'dead-end'; dir.dead_end_reason = dir.dead_end_reason || 'claimed success without solution at iteration cap'
|
|
@@ -2031,12 +2126,14 @@ export function apply(ctx) {
|
|
|
2031
2126
|
function statusFromStop(stopReason) { return (stopReason === 'completed' || stopReason === 'max-tokens') ? 'continue' : 'dead-end' }
|
|
2032
2127
|
async function addLemmaAsProposition(qid, lemma) {
|
|
2033
2128
|
if (!lemma || !lemma.title) return
|
|
2034
|
-
|
|
2129
|
+
// M11:`prob`(提示词名)与 `布尔估计`(遗留扁平名)都接受;`分类` 与 `细类型` 同理。
|
|
2130
|
+
const rawProb = lemmaProb(lemma)
|
|
2131
|
+
let be = clamp01(rawProb != null ? rawProb : 0.6)
|
|
2035
2132
|
if (be >= 1) be = 0.99; else if (be <= 0) be = 0.01
|
|
2036
2133
|
const p = {
|
|
2037
2134
|
id: 'p-' + shortId(), 标题: lemma.title, 状态: '未定论', 概率: be,
|
|
2038
2135
|
优先级: (lemma.优先级 != null) ? lemma.优先级 : 1, 依赖: [], 价值关键性: clamp01(lemma['价值/关键性'] != null ? lemma['价值/关键性'] : 0.5),
|
|
2039
|
-
分类: categoryOf({ 分类:
|
|
2136
|
+
分类: categoryOf({ 分类: lemmaCategory(lemma) || '未分类' }),
|
|
2040
2137
|
陈述: lemma.statement || lemma.title,
|
|
2041
2138
|
proofs: [{ title: lemma.title + '(证明)', prob: clamp01(0.7), status: '未定论', text: lemma.proof || '' }],
|
|
2042
2139
|
refutes: [], 来源问题: qid, 在问题清单: false,
|
|
@@ -2101,21 +2198,23 @@ export function apply(ctx) {
|
|
|
2101
2198
|
if (Array.isArray(parsed.methods_used)) {
|
|
2102
2199
|
for (const mu of parsed.methods_used) {
|
|
2103
2200
|
if (!mu || !mu.id) continue
|
|
2104
|
-
|
|
2201
|
+
// M9:方法键一律是 idSafe(id),代理给的是原始 id ⇒ 查表前归一化。
|
|
2202
|
+
const muId = idSafe(mu.id)
|
|
2203
|
+
const m = methods.get(muId) || globalMethods.get(muId)
|
|
2105
2204
|
if (!m) {
|
|
2106
2205
|
// 未知 id:solver 引用了一个尚未入卡的方法/技巧 → 作为待沉淀发明记录,防引用丢失(Method Keeper 将据此建卡)
|
|
2107
|
-
methodLog.pendingInventions.push({ at: now(), 来源: ctx.qid ? ('问题 ' + ctx.qid + (ctx.dirId ? ' 方向 ' + ctx.dirId : '')) : '', 类型: '方法', 标题: String(
|
|
2108
|
-
logActivity('method', 'methods_used referenced unknown method ' +
|
|
2206
|
+
methodLog.pendingInventions.push({ at: now(), 来源: ctx.qid ? ('问题 ' + ctx.qid + (ctx.dirId ? ' 方向 ' + ctx.dirId : '')) : '', 类型: '方法', 标题: String(muId), 内容描述: (mu.效果 || '') + (mu.建议 ? ';建议:' + mu.建议 : '') })
|
|
2207
|
+
logActivity('method', 'methods_used referenced unknown method ' + muId + ' → queued as pending invention')
|
|
2109
2208
|
continue
|
|
2110
2209
|
}
|
|
2111
|
-
if (methods.has(
|
|
2210
|
+
if (methods.has(muId)) {
|
|
2112
2211
|
// 只记录"有实际问题/方向上下文"的引用;method-keeper 纯整理时的引用(qid/dirId 皆空)不当作应用,
|
|
2113
2212
|
// 避免把"整理时引用该方法"误记为"实际应用",从而污染应用计数并触发错误的全局晋升。
|
|
2114
2213
|
if (ctx.dirId || ctx.qid) {
|
|
2115
2214
|
m.applications = m.applications || []
|
|
2116
2215
|
m.applications.push({ at: fmtTime(), 问题: ctx.qid || '', 方向: ctx.dirId || '', text: (mu.效果 || '') + (mu.建议 ? ';建议:' + mu.建议 : '') })
|
|
2117
2216
|
await saveMethod(m, false)
|
|
2118
|
-
logActivity('method', 'application record appended to ' +
|
|
2217
|
+
logActivity('method', 'application record appended to ' + muId + ' (问题 ' + ctx.qid + ' 方向 ' + ctx.dirId + ')')
|
|
2119
2218
|
}
|
|
2120
2219
|
}
|
|
2121
2220
|
}
|
|
@@ -2871,7 +2970,15 @@ export function apply(ctx) {
|
|
|
2871
2970
|
const id = formalId(target)
|
|
2872
2971
|
if (!id) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'defect 记录必须写明 target' }
|
|
2873
2972
|
const prev = formalOf(id)
|
|
2874
|
-
|
|
2973
|
+
// 撤回目标(审计 L2):只在**确实有过归档证明**时才撤回。此前 prev.proof 为空时也会拿
|
|
2974
|
+
// 'Verified/Lean/<id>.lean' 去撤,而 withdrawArchivedProof 在文件删不掉/不存在时会**就地覆盖写**
|
|
2975
|
+
// ——于是一个从未有过证明的对象会在"所有人都来这里找证明"的目录里凭空多出一份撤回声明文件。
|
|
2976
|
+
let archived = String(prev.proof || '').trim()
|
|
2977
|
+
if (!archived) {
|
|
2978
|
+
const fallback = 'Verified/Lean/' + id + '.lean'
|
|
2979
|
+
const abs = leanAbsPath(fallback)
|
|
2980
|
+
if (abs !== null && (await readTextAbs(abs)) !== undefined) archived = fallback
|
|
2981
|
+
}
|
|
2875
2982
|
await putFormal(id, Object.assign({}, prev, {
|
|
2876
2983
|
status: 'attempted',
|
|
2877
2984
|
decision: 'defect',
|
|
@@ -2880,7 +2987,7 @@ export function apply(ctx) {
|
|
|
2880
2987
|
updatedAt: now(),
|
|
2881
2988
|
}))
|
|
2882
2989
|
// 归档证明必须消失,否则 Verified/Lean/ 里会留下一份"看起来已通过"的不忠实代码。
|
|
2883
|
-
const withdrawn = await withdrawArchivedProof(archived)
|
|
2990
|
+
const withdrawn = archived ? await withdrawArchivedProof(archived) : 'none'
|
|
2884
2991
|
// 待办条目按 id 去重、就地刷新:note 进入 TODO.md 的 why 列(require 档据此搁置定论)。
|
|
2885
2992
|
const list = formalTodo()
|
|
2886
2993
|
const i = list.findIndex(function (x) { return x && x.id === id })
|
|
@@ -2892,12 +2999,13 @@ export function apply(ctx) {
|
|
|
2892
2999
|
await rebuildLeanLibIndexes()
|
|
2893
3000
|
await saveAll()
|
|
2894
3001
|
// 如实说出**哪一种**撤回发生了:删除成功 / 就地覆盖 / 两者都失败(后者必须让人手动处理,
|
|
2895
|
-
// 否则一份不忠实的代码会静静留在"证明"的路径上而无人知晓)。
|
|
3002
|
+
// 否则一份不忠实的代码会静静留在"证明"的路径上而无人知晓)。'none' = 本来就没有归档证明。
|
|
2896
3003
|
await formalAnnounce('【形式化】' + who + ' 认定 ' + id + ' 的 Lean 形式化存在**忠实性缺陷**:' + note
|
|
2897
3004
|
+ '。这不是"命题为假",而是**形式化不合格**:已撤回其「已通过」状态(降级为 attempted)、'
|
|
2898
|
-
+ (withdrawn === '
|
|
2899
|
-
: withdrawn === '
|
|
2900
|
-
: '归档证明 ' + archived + '
|
|
3005
|
+
+ (withdrawn === 'none' ? '该对象此前没有任何归档证明,无需撤回'
|
|
3006
|
+
: withdrawn === 'deleted' ? '删除归档证明 ' + archived
|
|
3007
|
+
: withdrawn === 'overwritten' ? '归档证明 ' + archived + ' 无法删除(宿主不支持删除),已**就地覆盖为撤回声明**'
|
|
3008
|
+
: '归档证明 ' + archived + ' **未能撤回**(宿主删除与覆盖均失败,请手动删除,不要把它当作该对象的证明)')
|
|
2901
3009
|
+ '、写入 Formal/TODO.md;'
|
|
2902
3010
|
+ (formalMode() === 'require'
|
|
2903
3011
|
? '本次裁定**不定论**,修正形式化并重新跑通(vibe_math_lean_archive kind=\'proof\')后再投票。'
|
|
@@ -2952,6 +3060,26 @@ export function apply(ctx) {
|
|
|
2952
3060
|
}
|
|
2953
3061
|
|
|
2954
3062
|
// ================= verification (验证器) =================
|
|
3063
|
+
/**
|
|
3064
|
+
* 一个判决对象**至少**需要几票。
|
|
3065
|
+
*
|
|
3066
|
+
* H5:此前没有任何下限——`maxParallelThreshold=1` 时 backfillVerifiers 只派出 1 个验证器,
|
|
3067
|
+
* 它一结束 `allReported`(只看"已存在的孩子是否都报了")即为真,`consensus` 对单元素数组
|
|
3068
|
+
* 恒真,于是跳过全部辩论直接 `finalVerdict → 1 → settleVerdict`,写出 `概率:1/已验证·真`
|
|
3069
|
+
* 并落 `Verified/`。`Verified/` 是所有提示词的"绝对可信"层,一票定论 = 一个验证器的偏见
|
|
3070
|
+
* 直接变成下游全部代理的"已知事实"。
|
|
3071
|
+
*
|
|
3072
|
+
* 下限取 max(2, expectedCount):verifierCount=1 不是"允许单票",只是"至少 2 个独立验证器"
|
|
3073
|
+
* (与 createVerifyTask 的 `Math.max(2, ...)` 一致)。
|
|
3074
|
+
*/
|
|
3075
|
+
function minVotes(t) { return Math.max(2, Number(t && t.expectedCount) || 0, Number(params.verifierCount) || 0) }
|
|
3076
|
+
function haveEnoughVotes(t) { return Object.keys(t.childResults).length >= minVotes(t) }
|
|
3077
|
+
/** 所有已派出的验证器都报了本轮的票,**且**票数达到下限——否则不得推进/裁决。 */
|
|
3078
|
+
function allReportedWithQuorum(t, round) {
|
|
3079
|
+
if (!t || !Array.isArray(t.children) || t.children.length === 0) return false
|
|
3080
|
+
if (t.children.length < Math.max(2, Number(t.expectedCount) || 0)) return false
|
|
3081
|
+
return t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === round })
|
|
3082
|
+
}
|
|
2955
3083
|
function consensus(t) { const vs = Object.keys(t.childResults).map(function (cid) { return t.childResults[cid].Result }); if (vs.length === 0) return false; return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
|
|
2956
3084
|
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') }
|
|
2957
3085
|
async function handleVerifier(childId, meta, output, stopReason) {
|
|
@@ -2974,12 +3102,16 @@ export function apply(ctx) {
|
|
|
2974
3102
|
if (t.children.indexOf(childId) === -1) t.children.push(childId)
|
|
2975
3103
|
t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round }
|
|
2976
3104
|
delete agentRegistry[childId]
|
|
2977
|
-
|
|
2978
|
-
|
|
3105
|
+
// H5:推进/裁决的前置条件是"已凑齐下限票数且本轮都已回报",不是"目前存在的那几个孩子都报了"。
|
|
3106
|
+
// 未凑齐时保持 spawning,由 reconcileVerify 在后续 tick 里补派验证器(不裁决、不定论)。
|
|
3107
|
+
if (!allReportedWithQuorum(t, meta.round)) { await saveAll(); return }
|
|
2979
3108
|
await advanceVerification(t, meta.round)
|
|
2980
3109
|
await saveAll()
|
|
2981
3110
|
}
|
|
2982
3111
|
async function advanceVerification(t, round) {
|
|
3112
|
+
// H5 第二道闸门:票数不足下限时**不辩论、不裁决**,回到 spawning 等 reconcileVerify 补派。
|
|
3113
|
+
// 放在最前面:即便 handleVerifier 因为"孩子被删除"等边缘情形推进到这里,也绝不定论。
|
|
3114
|
+
if (!haveEnoughVotes(t)) { t.status = 'spawning'; return }
|
|
2983
3115
|
if (round < params.debateMaxRounds && !consensus(t) && t.children.length > 0) {
|
|
2984
3116
|
if (!scheduler.running) { t.status = 'paused'; return }
|
|
2985
3117
|
if (activeCount() >= params.maxParallelThreshold) { t.status = 'paused'; return }
|
|
@@ -3008,6 +3140,8 @@ export function apply(ctx) {
|
|
|
3008
3140
|
}
|
|
3009
3141
|
}
|
|
3010
3142
|
async function finalizeVerification(t) {
|
|
3143
|
+
// H5 最后一道闸门:票数不足下限时**决不**给出裁决(单票绝不结论)。
|
|
3144
|
+
if (!haveEnoughVotes(t)) { t.status = 'spawning'; return }
|
|
3011
3145
|
const verdict = finalVerdict(t)
|
|
3012
3146
|
if (params.mode === 'manual') {
|
|
3013
3147
|
const d = enqueueDecision('verdict', 'verdict for ' + t.rId + ' (debate finished) = ' + verdict, { rId: t.rId, verdict: verdict, task: JSON.parse(JSON.stringify(t)) })
|
|
@@ -3033,16 +3167,14 @@ export function apply(ctx) {
|
|
|
3033
3167
|
if (mean >= 0.85 || mean <= 0.15) return Math.max(0.01, Math.min(0.99, mean))
|
|
3034
3168
|
}
|
|
3035
3169
|
if (params.verdictMode === 'forced') {
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
}
|
|
3045
|
-
return den > 0 ? Math.max(0.01, Math.min(0.99, num / den)) : 0.5
|
|
3170
|
+
// M8:这里**不再**按 verifierAccuracy 加权。那份统计量统计的是"这一票与该次裁决是否一致",
|
|
3171
|
+
// 而裁决本身就是同一批票的聚合——即"与自己一致"(注:v 为加权均值时 `Result === v` 几乎恒假,
|
|
3172
|
+
// 于是它其实把"从众度"记成 0),且没有任何后续真值(Lean 通过 / 被 defect 撤回 / 命题被反证)
|
|
3173
|
+
// 会纠正它。用这样一个量去加权,放大而非抑制系统性偏见,与"按历史准确率加权"的宣称相反。
|
|
3174
|
+
// 改为**等权**取均值:每票按自己报出的概率贡献,本身就是该验证器的贝叶斯式估计;
|
|
3175
|
+
// 严格的 1/0 是绝对投票,其影响已通过数值本身(拉向端点)自然体现,无需人为加权。
|
|
3176
|
+
let sum = 0; for (let i = 0; i < rs.length; i++) sum += rs[i].Result
|
|
3177
|
+
return Math.max(0.01, Math.min(0.99, sum / rs.length))
|
|
3046
3178
|
}
|
|
3047
3179
|
return 0.5
|
|
3048
3180
|
}
|
|
@@ -3050,12 +3182,17 @@ export function apply(ctx) {
|
|
|
3050
3182
|
const v = clamp01(verdict)
|
|
3051
3183
|
const r = t.r
|
|
3052
3184
|
const cids = Object.keys(t.childResults)
|
|
3185
|
+
// 票数记录保留(Logs/Verification 与 State/verifier_accuracy.json 是审计轨迹),但该统计量
|
|
3186
|
+
// **不再**参与任何加权(见 finalVerdict 的 M8 说明)。这里记的是"该票的实际概率"而非布尔值:
|
|
3187
|
+
// 原来的 `Result === v` 记法在 v 为加权均值时几乎不可能相等,等于把"从众度"记成 0,
|
|
3188
|
+
// 与字段名 correct/total 的含义相去更远。
|
|
3053
3189
|
for (let i = 0; i < cids.length; i++) {
|
|
3054
3190
|
const acc = verifierAccuracy[cids[i]] || { correct: 0, total: 0 }
|
|
3055
3191
|
acc.total += 1
|
|
3056
|
-
if (t.childResults[cids[i]].Result === v) acc.correct += 1
|
|
3192
|
+
if (Number(t.childResults[cids[i]].Result) === v) acc.correct += 1
|
|
3057
3193
|
verifierAccuracy[cids[i]] = acc
|
|
3058
3194
|
}
|
|
3195
|
+
pruneVerifierAccuracy()
|
|
3059
3196
|
await writeJson('Logs/Verification/' + t.rId + '_' + Date.now() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
|
|
3060
3197
|
// ── require 门禁(契约 §8)────────────────────────────────────────────────
|
|
3061
3198
|
// 判定为真(严格证明)或假(严格反驳)之前必须先有 `passed` 或 `blocked`。门禁放在**改
|
|
@@ -3191,10 +3328,43 @@ export function apply(ctx) {
|
|
|
3191
3328
|
if (params.indexAutoRebuild) await rebuildIndex()
|
|
3192
3329
|
return { ok: true }
|
|
3193
3330
|
}
|
|
3194
|
-
|
|
3331
|
+
/**
|
|
3332
|
+
* 项目锁 = 租约(lease),不是一次性写入的时间戳。
|
|
3333
|
+
*
|
|
3334
|
+
* 旧实现只在获取时写一次 `at`,全仓没有任何续租点:运行满 projectLockTimeoutMs(默认 60s)后
|
|
3335
|
+
* 第二个会话就会把锁判为"过期"并直接夺锁,两个进程于是并发写同一棵 md 树——而写锁 fileOwner
|
|
3336
|
+
* 是**进程级**的,跨进程不共享,挡不住这一路。
|
|
3337
|
+
*
|
|
3338
|
+
* 现在:(1) 独立的续租定时器按 LOCK_POLL_MS 轮询、在租约用掉 1/4 时写盘——最坏租约年龄因此远低于
|
|
3339
|
+
* projectLockTimeoutMs,且与 tick 时长完全无关(Round C 修正:此前 renewProjectLock 放在 tick()
|
|
3340
|
+
* 顶部,单个 tick 超过 projectLockTimeoutMs 就会让租约过期);
|
|
3341
|
+
* (2) 夺取**其他会话**持有的锁需要显式 override(`{override:true}` / `/vibe resume override`)。
|
|
3342
|
+
* 唯一保留的隐式接管是"锁租约已明确过期"(持有者进程崩溃、不再续租),以免崩溃后项目永久锁死;
|
|
3343
|
+
* 正常运行中的会话不会被接管。
|
|
3344
|
+
*/
|
|
3345
|
+
// 节流判据取**落盘的那份租约自己的年龄**(`projectLock.at`),不再维护影子时间戳
|
|
3346
|
+
// `projectLockRenewedAt`:实测它连续两次轮询都读到"距上次续租约 5s"(因而每次都判定"不必写"),
|
|
3347
|
+
// 而文件里的 `at` 始终停在会话启动那一刻——影子时间戳与真正的租约可以分叉。
|
|
3348
|
+
// `projectLock.at` 正是 acquireProjectLock() 判过期、也是崩溃恢复所依据的唯一状态。
|
|
3349
|
+
// 因此"多久写一次"由租约年龄自己决定,二者不可能再分叉。
|
|
3350
|
+
async function renewProjectLock() {
|
|
3351
|
+
if (!scheduler.running) return
|
|
3352
|
+
if (projectLock.sessionId !== sessionId) return
|
|
3195
3353
|
const timeout = Number(params.projectLockTimeoutMs) || 60000
|
|
3196
|
-
if (
|
|
3197
|
-
|
|
3354
|
+
if ((now() - (Number(projectLock.at) || 0)) < Math.max(1000, timeout / 4)) return
|
|
3355
|
+
projectLock.at = now()
|
|
3356
|
+
await saveAll()
|
|
3357
|
+
}
|
|
3358
|
+
async function acquireProjectLock(override) {
|
|
3359
|
+
const timeout = Number(params.projectLockTimeoutMs) || 60000
|
|
3360
|
+
const held = projectLock.sessionId && projectLock.sessionId !== sessionId
|
|
3361
|
+
if (held) {
|
|
3362
|
+
const age = now() - (Number(projectLock.at) || 0)
|
|
3363
|
+
// 过期租约 = 持有者进程已不在续租(崩溃) → 允许接管,否则崩溃一次项目就永久锁死。
|
|
3364
|
+
if (age < timeout && !override) {
|
|
3365
|
+
return { ok: false, code: 'PROJECT_LOCKED', message: '项目 "' + currentProject + '" 正被会话 ' + projectLock.sessionId + ' 占用(租约剩余 ' + Math.max(0, timeout - age) + 'ms;确认对方已停止后可显式接管:override=true)' }
|
|
3366
|
+
}
|
|
3367
|
+
logActivity('lock', 'project lock taken over from session ' + projectLock.sessionId + (override ? ' (explicit override)' : ' (lease expired after ' + age + 'ms)'))
|
|
3198
3368
|
}
|
|
3199
3369
|
projectLock = { sessionId: sessionId, at: now() }
|
|
3200
3370
|
await saveAll()
|
|
@@ -3203,8 +3373,8 @@ export function apply(ctx) {
|
|
|
3203
3373
|
async function releaseProjectLock() {
|
|
3204
3374
|
if (projectLock.sessionId === sessionId) { projectLock = { sessionId: '', at: 0 }; await saveAll() }
|
|
3205
3375
|
}
|
|
3206
|
-
async function startScheduler() { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3207
|
-
async function resumeScheduler() { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3376
|
+
async function startScheduler(override) { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3377
|
+
async function resumeScheduler(override) { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3208
3378
|
async function pauseScheduler() { scheduler.running = false; await releaseProjectLock(); logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
|
|
3209
3379
|
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
3210
3380
|
async function autoResolvePending() {
|
|
@@ -3258,12 +3428,16 @@ export function apply(ctx) {
|
|
|
3258
3428
|
// 注意:待沉淀发明(pendingInventions)不应阻塞终止——它是"批量蒸馏"(积够 methodKeepEvery 才触发),
|
|
3259
3429
|
// 少量残留不会再有 method-keeper 触发,若纳入会令 scheduler 永不终止(闲置空转)。
|
|
3260
3430
|
const leftoverVerify = (await buildVerifyCandidates()).length > 0
|
|
3261
|
-
|
|
3431
|
+
// H1:在途的 **planner 不算"活跃工作"**。planner 不产出知识、不持有验证任务,只是"问下一步做什么";
|
|
3432
|
+
// 旧判定把刚派出的 planner 也算成活跃代理,于是本 tick 的停机判定永远被自己派出的 planner 挡掉,
|
|
3433
|
+
// 空闲时形成"每 tick 一个 planner"的活锁。排除它之后,空闲的那一 tick 就能正常收口停机。
|
|
3434
|
+
const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' }).length
|
|
3435
|
+
if (unsolved.length === 0 && !leftoverVerify && realAgents === 0 && Object.keys(tasks).length === 0 && planQueue.length === 0) {
|
|
3262
3436
|
scheduler.running = false
|
|
3263
3437
|
await releaseProjectLock()
|
|
3264
3438
|
logActivity('stop', 'all active problems solved (never-priority excluded) and no active agents/tasks/plans — scheduler stopped (strict termination)')
|
|
3265
3439
|
await saveAll(); await maybeWriteReport(true); await maybePushReport(true)
|
|
3266
|
-
} else if (!hasPendingGate &&
|
|
3440
|
+
} else if (!hasPendingGate && realAgents === 0 && Object.keys(tasks).length === 0 && planQueue.length === 0 && unsolved.length > 0) {
|
|
3267
3441
|
let allBlocked = true
|
|
3268
3442
|
for (let i = 0; i < unsolved.length; i++) {
|
|
3269
3443
|
const q = unsolved[i]
|
|
@@ -3302,47 +3476,50 @@ export function apply(ctx) {
|
|
|
3302
3476
|
function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
|
|
3303
3477
|
const handlers = {}
|
|
3304
3478
|
function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
|
|
3305
|
-
registerTool('vibe_math_start',
|
|
3306
|
-
registerTool('vibe_math_resume',
|
|
3307
|
-
registerTool('vibe_math_pause',
|
|
3308
|
-
registerTool('vibe_math_abort',
|
|
3309
|
-
registerTool('vibe_math_status',
|
|
3310
|
-
registerTool('vibe_math_report',
|
|
3311
|
-
registerTool('vibe_math_set_mode',
|
|
3312
|
-
registerTool('vibe_math_set_params',
|
|
3313
|
-
registerTool('vibe_math_setup',
|
|
3314
|
-
registerTool('vibe_math_save_settings',
|
|
3315
|
-
registerTool('vibe_math_template',
|
|
3316
|
-
registerTool('vibe_math_add_problem',
|
|
3317
|
-
registerTool('vibe_math_add_proposition',
|
|
3318
|
-
|
|
3319
|
-
|
|
3479
|
+
registerTool('vibe_math_start', TOOL_DESC.vibe_math_start, objParams({ override: { type: 'boolean' } }), async function (args) { return await startScheduler(args && args.override) })
|
|
3480
|
+
registerTool('vibe_math_resume', TOOL_DESC.vibe_math_resume, objParams({ override: { type: 'boolean' } }), async function (args) { return await resumeScheduler(args && args.override) })
|
|
3481
|
+
registerTool('vibe_math_pause', TOOL_DESC.vibe_math_pause, objParams({}), async function () { return await pauseScheduler() })
|
|
3482
|
+
registerTool('vibe_math_abort', TOOL_DESC.vibe_math_abort, objParams({}), async function () { return await abortScheduler() })
|
|
3483
|
+
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), async function () { await refreshParams(); return await getStatus() })
|
|
3484
|
+
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
|
|
3485
|
+
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, 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 } })
|
|
3486
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { 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: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
|
|
3487
|
+
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, 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' } })
|
|
3488
|
+
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), async function () { return await saveSettings() })
|
|
3489
|
+
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
|
|
3490
|
+
registerTool('vibe_math_add_problem', TOOL_DESC.vibe_math_add_problem, objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' }, dependencies: { type: 'array', items: { type: 'string' } } }, ['id', 'description']), async function (args) { const id = idSafe(args.id); if (problems.has(id)) return { ok: false, message: 'problem id already exists' }; problems.set(id, { id: id, 标题: id, 状态: '求解中', 优先级: args.priority || 0, 依赖: Array.isArray(args.dependencies) ? args.dependencies : [], 被依赖: [], 来源: '原始', 计划: '待调度', 陈述: String(args.description == null ? '' : args.description), 来源与动机: '', solutions: [], 判断命题: '', 来源命题: '' }); await saveProblem(problems.get(id)); await syncDependencies(); await rebuildIndex(); scheduleTick(); return { ok: true, message: 'problem added', file: problemRel(problems.get(id)) } })
|
|
3491
|
+
registerTool('vibe_math_add_proposition', TOOL_DESC.vibe_math_add_proposition, objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 概率: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 分类: { type: 'string' }, 来源问题: { type: 'string' }, 来源方向: { type: 'string' } }, ['id', '概述']), async function (args) {
|
|
3492
|
+
// M9:内存键、对象 id、文件名必须是**同一个**归一化 id。此前内存键用原始 id、文件名用
|
|
3493
|
+
// idSafe(id)、重载键又来自文件名 ⇒ 同一个对象分裂成两个身份(`p-1/2` 与 `p-1-2`)。
|
|
3494
|
+
const id = idSafe(args.id)
|
|
3495
|
+
if (propos.has(id)) return { ok: false, message: 'proposition id already exists' }
|
|
3496
|
+
const p = { id: id, 标题: id, 状态: '未定论', 概率: clamp01(args.概率 != null ? args.概率 : 0.5), 优先级: (args.优先级 != null) ? args.优先级 : 1, 依赖: [], 价值关键性: clamp01(args['价值/关键性'] != null ? args['价值/关键性'] : 0.5), 分类: args.分类 || '未分类', 陈述: String(args.概述 == null ? '' : args.概述), proofs: [], refutes: [], 来源问题: String(args.来源问题 || ''), 来源方向: String(args.来源方向 || ''), 在问题清单: false }
|
|
3320
3497
|
propos.set(p.id, p); await saveProposition(p); await rebuildIndex(); scheduleTick(); return { ok: true, proposition: p, file: propositionRel(p) }
|
|
3321
3498
|
})
|
|
3322
|
-
registerTool('vibe_math_list_propositions',
|
|
3323
|
-
registerTool('vibe_math_new_project',
|
|
3324
|
-
registerTool('vibe_math_set_project',
|
|
3325
|
-
registerTool('vibe_math_list_projects',
|
|
3326
|
-
registerTool('vibe_math_list_decisions',
|
|
3327
|
-
registerTool('vibe_math_decide',
|
|
3328
|
-
registerTool('vibe_math_list_agents',
|
|
3329
|
-
registerTool('vibe_math_message_agent',
|
|
3330
|
-
registerTool('vibe_math_interrupt_agent',
|
|
3331
|
-
registerTool('vibe_math_plan',
|
|
3332
|
-
registerTool('vibe_math_index',
|
|
3333
|
-
registerTool('vibe_math_method_add',
|
|
3334
|
-
registerTool('vibe_math_method_list',
|
|
3335
|
-
registerTool('vibe_math_lock_status',
|
|
3336
|
-
registerTool('vibe_math_claim_write',
|
|
3337
|
-
registerTool('vibe_math_release_write',
|
|
3338
|
-
registerTool('vibe_math_sync_meta',
|
|
3499
|
+
registerTool('vibe_math_list_propositions', TOOL_DESC.vibe_math_list_propositions, objParams({}), async function () { const all = allPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 标题: p.标题, 概率: p.概率, 状态: p.状态, 优先级: p.优先级, 分类: categoryOf(p) } }) } })
|
|
3500
|
+
registerTool('vibe_math_new_project', TOOL_DESC.vibe_math_new_project, objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, true) })
|
|
3501
|
+
registerTool('vibe_math_set_project', TOOL_DESC.vibe_math_set_project, objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, false) })
|
|
3502
|
+
registerTool('vibe_math_list_projects', TOOL_DESC.vibe_math_list_projects, objParams({}), async function () { return { ok: true, current: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects') } })
|
|
3503
|
+
registerTool('vibe_math_list_decisions', TOOL_DESC.vibe_math_list_decisions, objParams({}), async function () { 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 } }) } })
|
|
3504
|
+
registerTool('vibe_math_decide', TOOL_DESC.vibe_math_decide, objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), async function (args) { const d = decisionQueue.find(function (x) { return x.id === args.id }); if (!d) return { ok: false, message: 'decision not found' }; if (d.status !== 'pending') return { ok: false, message: 'decision already resolved' }; const resolution = { action: args.action, verdict: args.verdict }; const applied = await applyDecision(d.node, d.data, resolution); const r = await resolveDecision(args.id, resolution); return Object.assign({ ok: true, applied: applied }, r) })
|
|
3505
|
+
registerTool('vibe_math_list_agents', TOOL_DESC.vibe_math_list_agents, objParams({}), async function () { 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, rId: m.rId }) } return { ok: true, agents: out, count: out.length } })
|
|
3506
|
+
registerTool('vibe_math_message_agent', TOOL_DESC.vibe_math_message_agent, 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' } })
|
|
3507
|
+
registerTool('vibe_math_interrupt_agent', TOOL_DESC.vibe_math_interrupt_agent, objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
|
|
3508
|
+
registerTool('vibe_math_plan', TOOL_DESC.vibe_math_plan, objParams({ force: { type: 'boolean' } }), async function (args) { if (args && args.force && scheduler.running && !scheduler.gate) { await callPlanner(); return { ok: true, message: 'planning triggered', queued: planQueue.length } } return { ok: true, queued: planQueue, lastPlan: lastPlanSummary } })
|
|
3509
|
+
registerTool('vibe_math_index', TOOL_DESC.vibe_math_index, objParams({}), async function () { await loadKnowledgeBase(); const r = await rebuildIndex(); return { ok: true, index: r, project: currentProject } })
|
|
3510
|
+
registerTool('vibe_math_method_add', TOOL_DESC.vibe_math_method_add, objParams({ id: { type: 'string' }, 标题: { type: 'string' }, 类型: { type: 'string' }, 核心内容: { type: 'string' }, 适用场景: { type: 'string' } }, ['id', '标题']), async function (args) { const id = idSafe(args.id); if (methods.has(id)) return { ok: false, message: 'method id already exists' }; const m = { id: id, 标题: args.标题, 类型: args.类型 || '方法', 状态: '经验', 可信断言: [], 上级体系: [], 子方法: [], 相关: [], 适用场景: args.适用场景 || '', 核心内容: args.核心内容 || '', 定义与记号: '', applications: [], improvements: [], 来源: 'user' }; methods.set(m.id, m); await saveMethod(m, false); await rebuildIndex(); return { ok: true, method: m, file: methodRel(m, false) } })
|
|
3511
|
+
registerTool('vibe_math_method_list', TOOL_DESC.vibe_math_method_list, objParams({}), async function () { const all = Array.from(methods.values()); const g = Array.from(globalMethods.values()); return { ok: true, count: all.length, globalCount: g.length, methods: all.map(function (m) { return { id: m.id, 标题: m.标题, 类型: m.类型, 状态: m.状态, 可信断言: m.可信断言 || [], applications: (m.applications || []).length, global: false } }).concat(g.map(function (m) { return { id: m.id, 标题: m.标题, 类型: m.类型, 状态: m.状态, 可信断言: m.可信断言 || [], applications: (m.applications || []).length, global: true } })) } })
|
|
3512
|
+
registerTool('vibe_math_lock_status', TOOL_DESC.vibe_math_lock_status, objParams({}), async function () { return { ok: true, project: currentProject, lock: projectLock } })
|
|
3513
|
+
registerTool('vibe_math_claim_write', TOOL_DESC.vibe_math_claim_write, objParams({ target: { type: 'string' } }, ['target']), async function (args, agent) { return await claimWrite(String(args.target || ''), agent) })
|
|
3514
|
+
registerTool('vibe_math_release_write', TOOL_DESC.vibe_math_release_write, objParams({ target: { type: 'string' } }, ['target']), async function (args, agent) { return await releaseWrite(String(args.target || ''), agent) })
|
|
3515
|
+
registerTool('vibe_math_sync_meta', TOOL_DESC.vibe_math_sync_meta, objParams({ meta: { type: 'object' } }, ['meta']), async function (args, agent) { return await syncMeta(args.meta || {}, agent) })
|
|
3339
3516
|
|
|
3340
3517
|
// ---- Lean 形式化验证工具(契约 docs/formal-verification.md §5)----
|
|
3341
3518
|
// **无条件注册**:注册是静态的(与既有 ctx.effect 纪律一致),模式只决定框架是否主动告诉
|
|
3342
3519
|
// 代理它们存在;off 模式下人/代理主动调用时照常工作。
|
|
3343
|
-
registerTool('vibe_math_lean_run',
|
|
3344
|
-
registerTool('vibe_math_lean_archive',
|
|
3345
|
-
registerTool('vibe_math_lean_lib',
|
|
3520
|
+
registerTool('vibe_math_lean_run', TOOL_DESC.vibe_math_lean_run, objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), async function (args, agent) { return await leanRunTool(agent && agent.id, args) })
|
|
3521
|
+
registerTool('vibe_math_lean_archive', TOOL_DESC.vibe_math_lean_archive, 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(agent && agent.id, args) })
|
|
3522
|
+
registerTool('vibe_math_lean_lib', TOOL_DESC.vibe_math_lean_lib, objParams({ refresh: { type: 'boolean' } }), async function (args) {
|
|
3346
3523
|
const a = args || {}
|
|
3347
3524
|
const r = a.refresh === false
|
|
3348
3525
|
? { lib: null, proved: null, objects: Object.keys(formalRecords()).length }
|
|
@@ -3455,11 +3632,13 @@ export function apply(ctx) {
|
|
|
3455
3632
|
if (Array.isArray(meta.lemmas)) {
|
|
3456
3633
|
for (const l of meta.lemmas) {
|
|
3457
3634
|
if (!l || (!l.id && !l.title)) continue
|
|
3458
|
-
|
|
3635
|
+
// M9:代理给的 id 可能与文件名归一化结果不同(`p-1/2` → `p-1-2`)⇒ 键与 id 一并归一化。
|
|
3636
|
+
const pid = l.id ? idSafe(l.id) : ('p-' + shortId())
|
|
3459
3637
|
if (!propos.has(pid)) {
|
|
3460
|
-
const
|
|
3638
|
+
const lProb = lemmaProb(l)
|
|
3639
|
+
const pn = { id: pid, 标题: l.title || pid, 状态: '未定论', 概率: clamp01(lProb != null ? lProb : 0.6), 优先级: l.优先级 != null ? l.优先级 : 1, 依赖: [], 价值关键性: clamp01(l['价值/关键性'] != null ? l['价值/关键性'] : 0.5), 分类: lemmaCategory(l) || '未分类', 陈述: l.statement || l.title || '', proofs: [], refutes: [], 来源问题: qid, 在问题清单: false }
|
|
3461
3640
|
// 引理证明文本(验证必需)由代理在 sync_meta 的 l.proof 上报(结构化,非长叙述);无则验证器只能验裸命题
|
|
3462
|
-
if (l.proof) { pn.proofs = [{ title: (l.title || pid) + '(证明)', prob: clamp01(
|
|
3641
|
+
if (l.proof) { pn.proofs = [{ title: (l.title || pid) + '(证明)', prob: clamp01(lProb != null ? lProb : 0.7), status: '未定论', text: String(l.proof) }] }
|
|
3463
3642
|
propos.set(pid, pn)
|
|
3464
3643
|
// 若代理已直接写了该命题卡,保留其内容(不覆盖);否则写一张标准卡兜底(保证可被索引/验证)
|
|
3465
3644
|
const rel = propositionRel(pn) // 与 saveProposition 同一路径
|
|
@@ -3468,11 +3647,16 @@ export function apply(ctx) {
|
|
|
3468
3647
|
if (!(dir.lemmas || []).some(function (x) { return x.id === pid })) { dir.lemmas = dir.lemmas || []; dir.lemmas.push({ id: pid, title: l.title || pid }) }
|
|
3469
3648
|
}
|
|
3470
3649
|
}
|
|
3471
|
-
// 解法上报(prob 由代理写进 Problems/<qid>.md
|
|
3472
|
-
|
|
3650
|
+
// 解法上报(prob 由代理写进 Problems/<qid>.md;这里只登记)。
|
|
3651
|
+
// M11:提示词教的是 `solution_text` / `solution_prob`,但遗留扁平通道用的是
|
|
3652
|
+
// `parsed.solution` / `parsed.solution_probability`。**两套拼写都接受**,否则一个
|
|
3653
|
+
// 严格按提示词回 `solution_text` 的 solver 会在这里被判成"声称成功却没有解法",方向被写死。
|
|
3654
|
+
const solText = (meta.solution_text != null) ? meta.solution_text : meta.solution
|
|
3655
|
+
const solProb = (meta.solution_prob != null) ? meta.solution_prob : meta.solution_probability
|
|
3656
|
+
if (solProb != null && solText) {
|
|
3473
3657
|
q.solutions = q.solutions || []
|
|
3474
|
-
const p = clamp01(
|
|
3475
|
-
q.solutions.push({ title: '解法 ' + (q.solutions.length + 1), prob: p >= 1 ? 0.99 : (p <= 0 ? 0.01 : p), status: '未定论', text: String(
|
|
3658
|
+
const p = clamp01(solProb)
|
|
3659
|
+
q.solutions.push({ title: '解法 ' + (q.solutions.length + 1), prob: p >= 1 ? 0.99 : (p <= 0 ? 0.01 : p), status: '未定论', text: String(solText).slice(0, 2000) })
|
|
3476
3660
|
}
|
|
3477
3661
|
// 子问题/临时假设(与旧 JSON 路径一致):注册 q_sub 问题 + 判断问题 + p-tmp 假设
|
|
3478
3662
|
if (Array.isArray(meta.sub_questions)) {
|
|
@@ -3491,7 +3675,8 @@ export function apply(ctx) {
|
|
|
3491
3675
|
}
|
|
3492
3676
|
if (kind === 'methods') {
|
|
3493
3677
|
if (Array.isArray(meta.used)) for (const mu of meta.used) await consumeMethodFeedback({ methods_used: mu ? [mu] : [] }, { qid: '', dirId: '' })
|
|
3494
|
-
if (Array.isArray(meta.created)) for (const
|
|
3678
|
+
if (Array.isArray(meta.created)) for (const rawMid of meta.created) {
|
|
3679
|
+
const mid = idSafe(rawMid) // M9:键、id 与文件名同一把归一化
|
|
3495
3680
|
if (!methods.has(mid)) {
|
|
3496
3681
|
const mm = { id: mid, 标题: mid, 类型: '方法', 状态: '经验', 可信断言: [], 上级体系: [], 子方法: [], 相关: [], 适用场景: '', 核心内容: '', 定义与记号: '', applications: [], improvements: [], 来源: 'agent-written' }
|
|
3497
3682
|
methods.set(mid, mm)
|
|
@@ -3503,12 +3688,13 @@ export function apply(ctx) {
|
|
|
3503
3688
|
if (Array.isArray(meta.improvements)) {
|
|
3504
3689
|
for (const imp of meta.improvements) {
|
|
3505
3690
|
if (!imp || !imp.id) continue
|
|
3506
|
-
const
|
|
3507
|
-
|
|
3691
|
+
const impId = idSafe(imp.id) // M9:方法键已归一化,改进目标必须查同一把键
|
|
3692
|
+
const m = methods.get(impId)
|
|
3693
|
+
if (!m) { logActivity('method', 'improvement referenced unknown method ' + impId); continue }
|
|
3508
3694
|
m.improvements = m.improvements || []
|
|
3509
3695
|
m.improvements.push({ v: m.improvements.length + 1, 原因: imp.原因 || '', text: imp.改进内容 || '' })
|
|
3510
3696
|
await saveMethod(m, false)
|
|
3511
|
-
logActivity('method', 'method ' +
|
|
3697
|
+
logActivity('method', 'method ' + impId + ' improved (v' + m.improvements.length + ')')
|
|
3512
3698
|
}
|
|
3513
3699
|
}
|
|
3514
3700
|
await saveAll()
|
|
@@ -3517,10 +3703,26 @@ export function apply(ctx) {
|
|
|
3517
3703
|
return { ok: false, message: 'unknown meta kind: ' + kind }
|
|
3518
3704
|
}
|
|
3519
3705
|
|
|
3706
|
+
// ---- 会话级 handler 表的工具描述(唯一来源)----
|
|
3707
|
+
//
|
|
3708
|
+
// 提示词审计 M3(v3):同一批工具在**两条注册路径**上各写了一份 description——会话 handler 表
|
|
3709
|
+
// (registerTool(name, description, …) 只存 handler)与文件末尾真正的 `tools.register`
|
|
3710
|
+
// (模型实际看到的那一份)。两边的**参数 schema 一致**(I13/I14 守),但描述会漂移:
|
|
3711
|
+
// `vibe_math_list_propositions` / `vibe_math_method_list` / `claim_write` / `release_write` /
|
|
3712
|
+
// `sync_meta` 五处曾对不上,模型看到哪一版取决于哪条路径生效。
|
|
3713
|
+
//
|
|
3714
|
+
// 现在这两条路径共用**模块级**常量 TOOL_DESC(定义在文件末尾的 shared helpers 区),
|
|
3715
|
+
// 任何一侧被改都会同时生效,不可能再漂移。`tests/audit-v3-registration-parity.mjs` 守这条不变式。
|
|
3716
|
+
|
|
3520
3717
|
// ================= slash command /vibe =================
|
|
3718
|
+
// 注意:签名**必须**保持 `dispatchVibeCommand(cmd, args)`——`audit-persona-surface` 的
|
|
3719
|
+
// `commandRegion()` 靠这个字面量把 /vibe 的分支表接进提示词面一致性检查(hint/usage/handler
|
|
3720
|
+
// 三处必须一致)。多一个形参会让那段源码不再被扫到,等于把这条守卫静默关掉。
|
|
3721
|
+
// `override` 通过 args 本身传递(见下面 commands.register 的处理)。
|
|
3521
3722
|
async function dispatchVibeCommand(cmd, args) {
|
|
3522
|
-
|
|
3523
|
-
if (cmd === '
|
|
3723
|
+
const override = args.indexOf('override') !== -1
|
|
3724
|
+
if (cmd === 'start') return await startScheduler(override)
|
|
3725
|
+
if (cmd === 'resume') return await resumeScheduler(override)
|
|
3524
3726
|
if (cmd === 'pause') return await pauseScheduler()
|
|
3525
3727
|
if (cmd === 'abort') return await abortScheduler()
|
|
3526
3728
|
if (cmd === 'status') { await refreshParams(); return await getStatus() }
|
|
@@ -3529,8 +3731,8 @@ export function apply(ctx) {
|
|
|
3529
3731
|
if (cmd === 'setup') { 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' } }
|
|
3530
3732
|
if (cmd === 'save') return await saveSettings()
|
|
3531
3733
|
if (cmd === 'template') return await createTemplate(args[0] === 'project' ? 'project' : 'global')
|
|
3532
|
-
if (cmd === 'add') { const
|
|
3533
|
-
if (cmd === 'add-proposition') { const
|
|
3734
|
+
if (cmd === 'add') { const raw = args[0]; const desc = args.slice(1).join(' '); if (!raw || !desc) return { ok: false, message: 'usage: /vibe add <id> <description>' }; const id = idSafe(raw); if (problems.has(id)) return { ok: false, message: 'problem id already exists' }; problems.set(id, { id: id, 标题: id, 状态: '求解中', 优先级: 0, 依赖: [], 被依赖: [], 来源: '原始', 计划: '待调度', 陈述: desc, 来源与动机: '', solutions: [], 判断命题: '', 来源命题: '' }); await saveProblem(problems.get(id)); await rebuildIndex(); scheduleTick(); return { ok: true, message: 'problem added', file: problemRel(problems.get(id)) } }
|
|
3735
|
+
if (cmd === 'add-proposition') { const raw = args[0]; const desc = args.slice(1).join(' '); if (!raw || !desc) return { ok: false, message: 'usage: /vibe add-proposition <id> <概述>' }; const id = idSafe(raw); const p = { id: id, 标题: id, 状态: '未定论', 概率: 0.5, 优先级: 1, 依赖: [], 价值关键性: 0.5, 分类: '未分类', 陈述: desc, proofs: [], refutes: [], 来源问题: '', 在问题清单: false }; propos.set(p.id, p); await saveProposition(p); await rebuildIndex(); scheduleTick(); return { ok: true, proposition: p, file: propositionRel(p) } }
|
|
3534
3736
|
if (cmd === 'list-propositions') { const all = allPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 标题: p.标题, 概率: p.概率, 状态: p.状态, 优先级: p.优先级, 分类: categoryOf(p) } }) } }
|
|
3535
3737
|
if (cmd === 'methods') { const all = Array.from(methods.values()); return { ok: true, count: all.length, methods: all.map(function (m) { return { id: m.id, 标题: m.标题, 类型: m.类型, 状态: m.状态 } }) } }
|
|
3536
3738
|
if (cmd === 'index') { await loadKnowledgeBase(); return await rebuildIndex() }
|
|
@@ -3543,7 +3745,7 @@ export function apply(ctx) {
|
|
|
3543
3745
|
}
|
|
3544
3746
|
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 } }) }
|
|
3545
3747
|
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 } }
|
|
3546
|
-
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 | methods | index | plan | lock | project [list|new <name>|<name>] | decisions | agents', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
|
|
3748
|
+
return { ok: false, usage: 'start [override] | resume [override] | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | methods | index | plan | lock | project [list|new <name>|<name>] | decisions | agents', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
|
|
3547
3749
|
}
|
|
3548
3750
|
|
|
3549
3751
|
// ================= session surface =================
|
|
@@ -3556,11 +3758,25 @@ export function apply(ctx) {
|
|
|
3556
3758
|
get scheduler() { return scheduler },
|
|
3557
3759
|
get tickInFlight() { return tickInFlight },
|
|
3558
3760
|
scheduleTick: scheduleTick,
|
|
3761
|
+
// Lease renewal for the apply-scope interval (see the ctx.effect below). Kept on the session so
|
|
3762
|
+
// the timer needs no per-session context, and so a mock host WITHOUT ctx.timeout still renews.
|
|
3763
|
+
renewLockIfDue: renewProjectLock,
|
|
3559
3764
|
onChildEnd: onChildEnd,
|
|
3560
3765
|
dispatchVibeCommand: dispatchVibeCommand,
|
|
3561
3766
|
handlers: handlers,
|
|
3562
3767
|
refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
|
|
3563
3768
|
getRunning: function () { return scheduler.running },
|
|
3769
|
+
// childOwner 裁剪用(审计 L1):这个会话当前仍"可能再发 subagent/end"的 child
|
|
3770
|
+
// = 在册子代理 + 任何任务正在等的那几个。
|
|
3771
|
+
referencedChildIds: function () {
|
|
3772
|
+
const out = Object.keys(agentRegistry)
|
|
3773
|
+
const ids = Object.keys(tasks)
|
|
3774
|
+
for (let i = 0; i < ids.length; i++) {
|
|
3775
|
+
const t = tasks[ids[i]]
|
|
3776
|
+
if (t && Array.isArray(t.children)) for (let j = 0; j < t.children.length; j++) out.push(t.children[j])
|
|
3777
|
+
}
|
|
3778
|
+
return out
|
|
3779
|
+
},
|
|
3564
3780
|
tickDue: function () { const iv = Math.max(200, Number(params.tickIntervalMs) || 2000); return (now() - lastTickAt) >= iv },
|
|
3565
3781
|
}
|
|
3566
3782
|
}
|
|
@@ -3581,47 +3797,47 @@ export function apply(ctx) {
|
|
|
3581
3797
|
},
|
|
3582
3798
|
}))
|
|
3583
3799
|
}
|
|
3584
|
-
registerTool('vibe_math_start',
|
|
3585
|
-
registerTool('vibe_math_resume',
|
|
3586
|
-
registerTool('vibe_math_pause',
|
|
3587
|
-
registerTool('vibe_math_abort',
|
|
3588
|
-
registerTool('vibe_math_status',
|
|
3589
|
-
registerTool('vibe_math_report',
|
|
3590
|
-
registerTool('vibe_math_set_mode',
|
|
3591
|
-
registerTool('vibe_math_set_params',
|
|
3592
|
-
registerTool('vibe_math_setup',
|
|
3593
|
-
registerTool('vibe_math_save_settings',
|
|
3594
|
-
registerTool('vibe_math_template',
|
|
3595
|
-
registerTool('vibe_math_add_problem',
|
|
3596
|
-
registerTool('vibe_math_add_proposition',
|
|
3597
|
-
registerTool('vibe_math_list_propositions',
|
|
3598
|
-
registerTool('vibe_math_new_project',
|
|
3599
|
-
registerTool('vibe_math_set_project',
|
|
3600
|
-
registerTool('vibe_math_list_projects',
|
|
3601
|
-
registerTool('vibe_math_list_decisions',
|
|
3602
|
-
registerTool('vibe_math_decide',
|
|
3603
|
-
registerTool('vibe_math_list_agents',
|
|
3604
|
-
registerTool('vibe_math_message_agent',
|
|
3605
|
-
registerTool('vibe_math_interrupt_agent',
|
|
3606
|
-
registerTool('vibe_math_plan',
|
|
3607
|
-
registerTool('vibe_math_index',
|
|
3608
|
-
registerTool('vibe_math_method_add',
|
|
3609
|
-
registerTool('vibe_math_method_list',
|
|
3610
|
-
registerTool('vibe_math_lock_status',
|
|
3611
|
-
registerTool('vibe_math_claim_write',
|
|
3612
|
-
registerTool('vibe_math_release_write',
|
|
3613
|
-
registerTool('vibe_math_sync_meta',
|
|
3800
|
+
registerTool('vibe_math_start', TOOL_DESC.vibe_math_start, objParams({ override: { type: 'boolean' } }), 'vibe_math_start')
|
|
3801
|
+
registerTool('vibe_math_resume', TOOL_DESC.vibe_math_resume, objParams({ override: { type: 'boolean' } }), 'vibe_math_resume')
|
|
3802
|
+
registerTool('vibe_math_pause', TOOL_DESC.vibe_math_pause, objParams({}), 'vibe_math_pause')
|
|
3803
|
+
registerTool('vibe_math_abort', TOOL_DESC.vibe_math_abort, objParams({}), 'vibe_math_abort')
|
|
3804
|
+
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), 'vibe_math_status')
|
|
3805
|
+
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), 'vibe_math_report')
|
|
3806
|
+
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
|
|
3807
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { 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: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), 'vibe_math_set_params')
|
|
3808
|
+
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), 'vibe_math_setup')
|
|
3809
|
+
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), 'vibe_math_save_settings')
|
|
3810
|
+
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
|
|
3811
|
+
registerTool('vibe_math_add_problem', TOOL_DESC.vibe_math_add_problem, objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' }, dependencies: { type: 'array', items: { type: 'string' } } }, ['id', 'description']), 'vibe_math_add_problem')
|
|
3812
|
+
registerTool('vibe_math_add_proposition', TOOL_DESC.vibe_math_add_proposition, objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 概率: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 分类: { type: 'string' }, 来源问题: { type: 'string' }, 来源方向: { type: 'string' } }, ['id', '概述']), 'vibe_math_add_proposition')
|
|
3813
|
+
registerTool('vibe_math_list_propositions', TOOL_DESC.vibe_math_list_propositions, objParams({}), 'vibe_math_list_propositions')
|
|
3814
|
+
registerTool('vibe_math_new_project', TOOL_DESC.vibe_math_new_project, objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_new_project')
|
|
3815
|
+
registerTool('vibe_math_set_project', TOOL_DESC.vibe_math_set_project, objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_set_project')
|
|
3816
|
+
registerTool('vibe_math_list_projects', TOOL_DESC.vibe_math_list_projects, objParams({}), 'vibe_math_list_projects')
|
|
3817
|
+
registerTool('vibe_math_list_decisions', TOOL_DESC.vibe_math_list_decisions, objParams({}), 'vibe_math_list_decisions')
|
|
3818
|
+
registerTool('vibe_math_decide', TOOL_DESC.vibe_math_decide, objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), 'vibe_math_decide')
|
|
3819
|
+
registerTool('vibe_math_list_agents', TOOL_DESC.vibe_math_list_agents, objParams({}), 'vibe_math_list_agents')
|
|
3820
|
+
registerTool('vibe_math_message_agent', TOOL_DESC.vibe_math_message_agent, objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), 'vibe_math_message_agent')
|
|
3821
|
+
registerTool('vibe_math_interrupt_agent', TOOL_DESC.vibe_math_interrupt_agent, objParams({ childId: { type: 'string' } }, ['childId']), 'vibe_math_interrupt_agent')
|
|
3822
|
+
registerTool('vibe_math_plan', TOOL_DESC.vibe_math_plan, objParams({ force: { type: 'boolean' } }), 'vibe_math_plan')
|
|
3823
|
+
registerTool('vibe_math_index', TOOL_DESC.vibe_math_index, objParams({}), 'vibe_math_index')
|
|
3824
|
+
registerTool('vibe_math_method_add', TOOL_DESC.vibe_math_method_add, objParams({ id: { type: 'string' }, 标题: { type: 'string' }, 类型: { type: 'string' }, 核心内容: { type: 'string' }, 适用场景: { type: 'string' } }, ['id', '标题']), 'vibe_math_method_add')
|
|
3825
|
+
registerTool('vibe_math_method_list', TOOL_DESC.vibe_math_method_list, objParams({}), 'vibe_math_method_list')
|
|
3826
|
+
registerTool('vibe_math_lock_status', TOOL_DESC.vibe_math_lock_status, objParams({}), 'vibe_math_lock_status')
|
|
3827
|
+
registerTool('vibe_math_claim_write', TOOL_DESC.vibe_math_claim_write, objParams({ target: { type: 'string' } }, ['target']), 'vibe_math_claim_write')
|
|
3828
|
+
registerTool('vibe_math_release_write', TOOL_DESC.vibe_math_release_write, objParams({ target: { type: 'string' } }, ['target']), 'vibe_math_release_write')
|
|
3829
|
+
registerTool('vibe_math_sync_meta', TOOL_DESC.vibe_math_sync_meta, objParams({ meta: { type: 'object' } }, ['meta']), 'vibe_math_sync_meta')
|
|
3614
3830
|
// Lean 形式化验证(docs/formal-verification.md):三个工具**无条件注册**——注册是静态的,
|
|
3615
3831
|
// 模式只决定框架是否主动告诉代理它们存在。off 模式下人/代理主动调用时照常工作。
|
|
3616
|
-
registerTool('vibe_math_lean_run',
|
|
3617
|
-
registerTool('vibe_math_lean_archive',
|
|
3618
|
-
registerTool('vibe_math_lean_lib',
|
|
3832
|
+
registerTool('vibe_math_lean_run', TOOL_DESC.vibe_math_lean_run, objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), 'vibe_math_lean_run')
|
|
3833
|
+
registerTool('vibe_math_lean_archive', TOOL_DESC.vibe_math_lean_archive, 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')
|
|
3834
|
+
registerTool('vibe_math_lean_lib', TOOL_DESC.vibe_math_lean_lib, objParams({ refresh: { type: 'boolean' } }), 'vibe_math_lean_lib')
|
|
3619
3835
|
|
|
3620
3836
|
// /vibe slash command (registered once; routed per session)
|
|
3621
3837
|
ctx.effect(() => commands.register({
|
|
3622
3838
|
name: 'vibe',
|
|
3623
3839
|
description: 'control the Vibe Math V3 solver (start/pause/projects/setup/save/decisions/agents/methods/index/plan/lock)',
|
|
3624
|
-
input: { hint: '[start|resume|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents]' },
|
|
3840
|
+
input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents]' },
|
|
3625
3841
|
handler: async function (invocation) {
|
|
3626
3842
|
const s = getSession(invocation && invocation.agent)
|
|
3627
3843
|
if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
|
|
@@ -3640,16 +3856,56 @@ export function apply(ctx) {
|
|
|
3640
3856
|
|
|
3641
3857
|
// subagent/end (registered once; routed to the owning session via childOwner)
|
|
3642
3858
|
ctx.on('subagent/end', function (info) {
|
|
3859
|
+
// 记下最近一次 end 的时间:childOwner 的宽限期从这里算(见 pruneChildOwner)。
|
|
3860
|
+
lastChildEndAt.set(info.id, now())
|
|
3643
3861
|
const sid = childOwner.get(info.id)
|
|
3644
3862
|
const s = sid !== undefined ? sessions.get(sid) : undefined
|
|
3645
3863
|
if (s) s.onChildEnd(info).catch(function (e) { console.error('vibe-math-v3 onChildEnd reject: ' + String((e && e.stack) || e)) })
|
|
3646
|
-
//
|
|
3864
|
+
// 注意:这里**不要**立即回收 childOwner 条目(与 v2 同一结论)。该映射在子代理 end 之后仍会
|
|
3647
3865
|
// 被后续事件路由用到;在 v2 上实测过"事件回调里回收"与"onChildEnd 末尾回收"两种写法,
|
|
3648
|
-
// 都会让 verdict
|
|
3866
|
+
// 都会让 verdict 收口失效。回收交给引用 + 宽限期的 pruneChildOwner(审计 L1)。
|
|
3649
3867
|
})
|
|
3650
3868
|
|
|
3869
|
+
// Project-lock LEASE renewal timer (Round C, HIGH): renewal must NOT live in tick().
|
|
3870
|
+
//
|
|
3871
|
+
// Round A put `await renewProjectLock()` at the top of tick(), which left the lease starvable: a
|
|
3872
|
+
// single tick whose awaited work outlives projectLockTimeoutMs (one slow shell/Lean call is enough)
|
|
3873
|
+
// keeps the on-disk `at` stale, and the `if (tickInFlight) return` early-return (:1374) renews
|
|
3874
|
+
// nothing either, so a second session's acquireProjectLock() saw `age >= timeout` and took the
|
|
3875
|
+
// project lock WITHOUT `override` while this session was alive and mid-tick. Measured: 5803 ms of
|
|
3876
|
+
// stale lease against a 2500 ms timeout (probe _oneoff/rb-probe-v3-locklease.mjs).
|
|
3877
|
+
//
|
|
3878
|
+
// An independent interval cannot be delayed by tick duration, tick overlap or a held gate. It is
|
|
3879
|
+
// registered once per preset via ctx.effect (disposed with the plugin) — NOT per startScheduler —
|
|
3880
|
+
// so an `await` inside its callback cannot race plugin disposal, and the scheduler stays mockable
|
|
3881
|
+
// (tests/selfdrive-v3.mjs has no ctx.timeout). The poll runs many times inside one lease window,
|
|
3882
|
+
// so the lease can never approach expiry however long a single tick runs; renewProjectLock() itself
|
|
3883
|
+
// is still the throttled writer and keeps its `scheduler.running` / ownership guards, which preserves
|
|
3884
|
+
// the crash-recovery path exactly: a dead process stops renewing, its lease ages out, and only then
|
|
3885
|
+
// may a new session take it over.
|
|
3886
|
+
// 心跳定时器(每个宿主一次,随插件 fiber 释放)。
|
|
3887
|
+
//
|
|
3888
|
+
// L7:优先用**宿主的 timer 服务**(`ctx.interval`,随宿主暂停/记账、随 fiber 释放),
|
|
3889
|
+
// 只有在宿主没有该服务时(测试 mock / 旧线)才回落到裸 setInterval——回落是必需的:
|
|
3890
|
+
// `inject` 里没有 'timer'(加进去会让缺该服务的主机整个不激活),而 mock 宿主没有 ctx.interval。
|
|
3891
|
+
// 两种实现都返回 disposer,ctx.effect 的清理语义不变。
|
|
3892
|
+
function everyMs(ms, fn) {
|
|
3893
|
+
try { if (typeof ctx.interval === 'function') return ctx.interval(fn, ms) } catch (e) { /* 回落到 setInterval */ }
|
|
3894
|
+
const t = setInterval(fn, ms)
|
|
3895
|
+
return () => clearInterval(t)
|
|
3896
|
+
}
|
|
3897
|
+
ctx.effect(() => everyMs(LOCK_POLL_MS, function () { for (const s of sessions.values()) { s.renewLockIfDue().catch(function (e) { console.error('vibe-math-v3 lock renew error: ' + String((e && e.message) || e)) }) } }))
|
|
3651
3898
|
// tick timer (registered once; ticks every running session at its own pace)
|
|
3652
|
-
ctx.effect(() => {
|
|
3899
|
+
ctx.effect(() => {
|
|
3900
|
+
let beat = 0
|
|
3901
|
+
return everyMs(1000, function () {
|
|
3902
|
+
for (const s of sessions.values()) { if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick() }
|
|
3903
|
+
// childOwner 裁剪(审计 L1):每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
|
|
3904
|
+
if ((++beat % 30) === 0) {
|
|
3905
|
+
try { pruneChildOwner() } catch (e) { console.error('vibe-math-v3: pruneChildOwner failed: ' + String((e && e.message) || e)) }
|
|
3906
|
+
}
|
|
3907
|
+
})
|
|
3908
|
+
})
|
|
3653
3909
|
}
|
|
3654
3910
|
|
|
3655
3911
|
// ---- test seam: pure, stateless helpers --------------------------------
|
|
@@ -3682,6 +3938,11 @@ export const __testHelpers = {
|
|
|
3682
3938
|
parseEntries,
|
|
3683
3939
|
section,
|
|
3684
3940
|
parseBodySections,
|
|
3941
|
+
findSectionHeads,
|
|
3942
|
+
escField,
|
|
3943
|
+
unescField,
|
|
3944
|
+
entryRe,
|
|
3945
|
+
probText,
|
|
3685
3946
|
extraBodySections,
|
|
3686
3947
|
parseAppTitle,
|
|
3687
3948
|
parseMethodMd,
|
|
@@ -3691,12 +3952,74 @@ export const __testHelpers = {
|
|
|
3691
3952
|
|
|
3692
3953
|
function now() { return Date.now() }
|
|
3693
3954
|
|
|
3955
|
+
/**
|
|
3956
|
+
* 工具描述的唯一来源(提示词审计 M3):v3 的每个工具都在**两条路径**上注册——会话 handler 表
|
|
3957
|
+
* (makeSession 内只存 handler)与文件末尾真正生效的 `tools.register`。两条路径必须逐字一致,
|
|
3958
|
+
* 否则模型看到的描述取决于哪条路径生效,任何一侧被改都会静默改变提示词面。
|
|
3959
|
+
* 全部 33 个工具都从此表取描述;`tests/audit-v3-registration-parity.mjs` 逐字比对两条路径。
|
|
3960
|
+
*/
|
|
3961
|
+
const TOOL_DESC = {
|
|
3962
|
+
vibe_math_start: 'Start (or restart) the Vibe Math V3 scheduler for the current project. override=true forcibly takes over the project lock held by another session (only after you verified that session is gone).',
|
|
3963
|
+
vibe_math_resume: 'Resume the Vibe Math V3 scheduler after a checkpoint/restart. override=true forcibly takes over the project lock held by another session (only after you verified that session is gone).',
|
|
3964
|
+
vibe_math_pause: 'Pause the scheduler (in-flight children finish their current turn).',
|
|
3965
|
+
vibe_math_abort: 'Abort the scheduler and interrupt all active children.',
|
|
3966
|
+
vibe_math_status: 'Show scheduler status, params, active agents, projects, and recent activity.',
|
|
3967
|
+
vibe_math_report: 'Return the full progress report and write it to Progress_Logs/report.json + Logs/报告.md.',
|
|
3968
|
+
vibe_math_set_mode: 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.',
|
|
3969
|
+
// set_params 的 schema 现在也收 mode(M10):同一能力既有专用工具 vibe_math_set_mode,也可用这个键;
|
|
3970
|
+
// 两条注册路径共用这一份描述,`--self-probe` 会证明漂移能被抓到。
|
|
3971
|
+
vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。',
|
|
3972
|
+
vibe_math_setup: 'Return the interactive parameter schema for guided configuration.',
|
|
3973
|
+
vibe_math_save_settings: 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.',
|
|
3974
|
+
vibe_math_template: 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.',
|
|
3975
|
+
vibe_math_add_problem: 'Add a problem to the current project (creates Problems/<id>.md).',
|
|
3976
|
+
vibe_math_add_proposition: 'Add a proposition to Propos/ (creates Propos/<分类>/<id>.md).',
|
|
3977
|
+
vibe_math_list_propositions: 'List propositions from Propos/ (summary index: id, 标题, 概率, 状态, 优先级, 分类).',
|
|
3978
|
+
vibe_math_new_project: 'Create a new math project folder and switch to it.',
|
|
3979
|
+
vibe_math_set_project: 'Switch the current math project.',
|
|
3980
|
+
vibe_math_list_projects: 'List math projects.',
|
|
3981
|
+
vibe_math_list_decisions: 'List pending manual decisions.',
|
|
3982
|
+
vibe_math_decide: 'Resolve a pending manual decision (plan: approve|reject; verdict: override with verdict 1|0; spawn: approve|reject; method-promote: approve|reject).',
|
|
3983
|
+
vibe_math_list_agents: 'List tracked sub-agents (child sessions).',
|
|
3984
|
+
vibe_math_message_agent: 'Send a message to a tracked child agent (next turn).',
|
|
3985
|
+
vibe_math_interrupt_agent: 'Interrupt a tracked child agent.',
|
|
3986
|
+
vibe_math_plan: 'Show the queued plan / last plan result, or force a planning round.',
|
|
3987
|
+
vibe_math_index: 'Rebuild the machine index (State/index.json) from the Markdown knowledge base.',
|
|
3988
|
+
vibe_math_method_add: 'Manually add a method card to Methods/ (creates Methods/<id>.md).',
|
|
3989
|
+
vibe_math_method_list: 'List methods from Methods/ (+ global VibeMath/Methods/): id, 标题, 类型, 状态, 可信断言, applications count.',
|
|
3990
|
+
vibe_math_lock_status: 'Show the project lock occupancy.',
|
|
3991
|
+
vibe_math_claim_write: '(member) Acquire the write lock for one target file (relative to the project root). Call before writing a Markdown file directly; a file may only be written by ONE agent at a time. Returns the display path you may write (VibeMath/Projects/<project>/<target>) and a hint.',
|
|
3992
|
+
vibe_math_release_write: '(member) Release the write lock for one target file (relative to the project root). Call after you finished writing it.',
|
|
3993
|
+
vibe_math_sync_meta: '(member) After you write content into Markdown files, report ONLY lightweight scheduling metadata to keep the scheduler state in sync (content stays in the md files). meta.kind must be one of:\n- "directions": {qid, directions:[{id,title,method,core_assumption,feasibility}], methods_used:[{id,效果,建议}], new_inventions:[{类型,标题,内容描述,是否已入库}]}\n- "solver": {qid, dirId, round, survival, status:"continue|success|dead-end", dead_end_reason, lemmas:[{id,title,statement,proof,prob,价值/关键性,分类,优先级}], methods_used, new_inventions, solution_prob, solution_text, sub_questions:[{q_sub_title,q_sub_statement,assumption_title,assumption_statement}]}\n- "methods": {used:[{id,效果,建议}], created:[ids], improvements:[{id,改进内容,原因}]}',
|
|
3994
|
+
vibe_math_lean_run: "(member) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.",
|
|
3995
|
+
vibe_math_lean_archive: '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (VibeMath/Formal/Lib). kind="lemma": a machine-checked lemma → VibeMath/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).',
|
|
3996
|
+
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.",
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3694
3999
|
function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
|
|
3695
4000
|
|
|
3696
4001
|
function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
|
|
3697
4002
|
|
|
3698
4003
|
function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
|
|
3699
4004
|
|
|
4005
|
+
/**
|
|
4006
|
+
* M11:同一个语义两套字段名。提示词教的是 `prob`(`lemmas[].prob`),而遗留扁平 JSON 通道
|
|
4007
|
+
* 用的是 `布尔估计`(`细类型` 同理)。这里统一取值顺序,两套拼写都接受,避免"按提示词回写的
|
|
4008
|
+
* 引理"被当成没有估计值。
|
|
4009
|
+
*/
|
|
4010
|
+
function lemmaProb(l) {
|
|
4011
|
+
if (l == null) return undefined
|
|
4012
|
+
if (l.prob != null) return l.prob
|
|
4013
|
+
if (l['布尔估计'] != null) return l['布尔估计']
|
|
4014
|
+
return undefined
|
|
4015
|
+
}
|
|
4016
|
+
function lemmaCategory(l) {
|
|
4017
|
+
if (l == null) return ''
|
|
4018
|
+
if (l['分类'] != null) return String(l['分类'])
|
|
4019
|
+
if (l['细类型'] != null) return String(Object.keys(l['细类型'] || { 未分类: {} })[0] || '未分类')
|
|
4020
|
+
return ''
|
|
4021
|
+
}
|
|
4022
|
+
|
|
3700
4023
|
function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
|
|
3701
4024
|
|
|
3702
4025
|
function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
|
|
@@ -3744,12 +4067,37 @@ function safeJson(v, fb) { if (v == null || v === '') return fb; try { return JS
|
|
|
3744
4067
|
function stripJsonComments(text) { let out = ''; let inStr = false; let inLine = false; let inBlock = false; let esc = false; for (let i = 0; i < text.length; i++) { const c = text[i]; const n = text[i + 1]; if (inLine) { if (c === '\n') { inLine = false; out += c } continue } if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++ } continue } if (inStr) { out += c; if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue } if (c === '"') { inStr = true; out += c; continue } if (c === '/' && n === '/') { inLine = true; i++; continue } if (c === '/' && n === '*') { inBlock = true; i++; continue } out += c } return out }
|
|
3745
4068
|
|
|
3746
4069
|
function splitHeader(text) {
|
|
3747
|
-
const
|
|
3748
|
-
const
|
|
3749
|
-
const
|
|
4070
|
+
const heads = findSectionHeads(text)
|
|
4071
|
+
const first = (heads.length > 0 && heads[0].from > 0) ? heads[0].from : -1
|
|
4072
|
+
const head = first === -1 ? String(text) : String(text).slice(0, first - 1)
|
|
4073
|
+
const body = first === -1 ? '' : String(text).slice(first)
|
|
3750
4074
|
return { head: head, body: body }
|
|
3751
4075
|
}
|
|
3752
4076
|
|
|
4077
|
+
/**
|
|
4078
|
+
* 顶层 `## ` 段落边界,**跳过围栏代码块**(审计 L4)。
|
|
4079
|
+
*
|
|
4080
|
+
* 此前 splitHeader / section / parseBodySections 都把正文里**任意** `## ` 行当段落边界。代理把
|
|
4081
|
+
* 引用或代码粘进「陈述」段时,围栏里的 `## ` 行会被当成新段落:那半段代码变成"额外的段",
|
|
4082
|
+
* 下一次 compose 还会给它补一个 `## ` 标题(正文被改写、代码块被劈开)。围栏内的 `## ` 不是边界。
|
|
4083
|
+
*/
|
|
4084
|
+
function findSectionHeads(text) {
|
|
4085
|
+
const out = []
|
|
4086
|
+
const src = String(text == null ? '' : text)
|
|
4087
|
+
const lines = src.split('\n')
|
|
4088
|
+
let pos = 0, fence = false
|
|
4089
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4090
|
+
const line = lines[i]
|
|
4091
|
+
if (/^\s*(```|~~~)/.test(line)) { fence = !fence; pos += line.length + 1; continue }
|
|
4092
|
+
if (!fence) {
|
|
4093
|
+
const m = /^##\s+(.+?)\s*$/.exec(line)
|
|
4094
|
+
if (m) out.push({ name: m[1].trim(), from: pos, len: line.length })
|
|
4095
|
+
}
|
|
4096
|
+
pos += line.length + 1
|
|
4097
|
+
}
|
|
4098
|
+
return out
|
|
4099
|
+
}
|
|
4100
|
+
|
|
3753
4101
|
function parseAnchors(head) {
|
|
3754
4102
|
const anchors = {}
|
|
3755
4103
|
const re = /^-\s*([A-Za-z\u4e00-\u9fa5/]+)\s*:\s*(.*)$/gm
|
|
@@ -3758,6 +4106,28 @@ function parseAnchors(head) {
|
|
|
3758
4106
|
return anchors
|
|
3759
4107
|
}
|
|
3760
4108
|
|
|
4109
|
+
/**
|
|
4110
|
+
* md 卡片是**逐行**格式:字段里出现换行会把标题行劈成两行(`parseAnchors` 的 `.*$` 与
|
|
4111
|
+
* `parseEntries` 的逐行扫描都不跨行),出现全角竖线 `|` 则会让
|
|
4112
|
+
* `### 解法 N|标题|概率X|状态Y` 的惰性匹配在**标题内部**提前切段(审计 L6)。
|
|
4113
|
+
* 约定:compose 侧转义 `\`、`|` 与换行(`\n`),parse 侧还原 ⇒ 往返无损,且惰性匹配不可能切进字段。
|
|
4114
|
+
*/
|
|
4115
|
+
function escField(v) { return String(v == null ? '' : v).replace(/\\/g, '\\\\').replace(/|/g, '\\|').replace(/\r?\n/g, '\\n') }
|
|
4116
|
+
function unescField(v) {
|
|
4117
|
+
const s = String(v == null ? '' : v)
|
|
4118
|
+
if (s.indexOf('\\') === -1) return s
|
|
4119
|
+
return s.replace(/\\(.)/g, function (_m, c) { return c === 'n' ? '\n' : c })
|
|
4120
|
+
}
|
|
4121
|
+
// 字段体:接受 `\\.` 转义对,但不接受裸 `|`/裸换行 ⇒ 惰性匹配只在真正的分隔符处停。
|
|
4122
|
+
const ENTRY_FIELD = '(?:[^|\\r\\n\\\\]|\\\\.)*?'
|
|
4123
|
+
/**
|
|
4124
|
+
* 条目标题行正则。概率段用 `[-+0-9.eE]+` 而非 `[0-9.]+`(审计 L3):指数记法(如 `概率5e-7`,
|
|
4125
|
+
* 来自 meta.solution_prob / l.prob 这类 ∈(0,1) 的数值)此前整条匹配失败,条目在重载后被静默丢弃。
|
|
4126
|
+
*/
|
|
4127
|
+
function entryRe(kind) { return new RegExp('^###\\s*' + kind + '\\s*\\d+|(' + ENTRY_FIELD + ')|概率([-+0-9.eE]+)|状态(' + ENTRY_FIELD + ')$') }
|
|
4128
|
+
/** 写出条目标题时的概率文本:非有限值(NaN/Infinity)绝不能写出去,否则那一行永远解析不回来。 */
|
|
4129
|
+
function probText(v) { const n = Number(v); return Number.isFinite(n) ? String(n) : '0.5' }
|
|
4130
|
+
|
|
3761
4131
|
function parseEntries(body, kindRe) {
|
|
3762
4132
|
const out = []
|
|
3763
4133
|
const lines = String(body).split('\n')
|
|
@@ -3783,23 +4153,24 @@ function parseEntries(body, kindRe) {
|
|
|
3783
4153
|
}
|
|
3784
4154
|
|
|
3785
4155
|
function section(body, name) {
|
|
3786
|
-
const
|
|
3787
|
-
const
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
4156
|
+
const text = String(body)
|
|
4157
|
+
const heads = findSectionHeads(text)
|
|
4158
|
+
let start = -1, end = -1
|
|
4159
|
+
for (let i = 0; i < heads.length; i++) {
|
|
4160
|
+
if (start === -1) { if (heads[i].name === name) start = heads[i].from + heads[i].len }
|
|
4161
|
+
else { end = heads[i].from; break }
|
|
4162
|
+
}
|
|
4163
|
+
if (start === -1) return ''
|
|
4164
|
+
return (end === -1 ? text.slice(start) : text.slice(start, end)).trim()
|
|
3792
4165
|
}
|
|
3793
4166
|
|
|
3794
4167
|
function parseBodySections(body) {
|
|
3795
4168
|
const text = String(body || '')
|
|
3796
|
-
const
|
|
4169
|
+
const heads = findSectionHeads(text)
|
|
3797
4170
|
const found = []
|
|
3798
|
-
let
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
const to = i + 1 < found.length ? found[i + 1].from : text.length
|
|
3802
|
-
found[i].text = text.slice(found[i].bodyFrom, to).trim()
|
|
4171
|
+
for (let i = 0; i < heads.length; i++) {
|
|
4172
|
+
const to = i + 1 < heads.length ? heads[i + 1].from : text.length
|
|
4173
|
+
found.push({ name: heads[i].name, from: heads[i].from, bodyFrom: heads[i].from + heads[i].len, text: text.slice(heads[i].from + heads[i].len, to).trim() })
|
|
3803
4174
|
}
|
|
3804
4175
|
return found
|
|
3805
4176
|
}
|
|
@@ -3812,8 +4183,12 @@ function extraBodySections(body, managedNames) {
|
|
|
3812
4183
|
|
|
3813
4184
|
function parseAppTitle(title) {
|
|
3814
4185
|
const t = String(title || '')
|
|
3815
|
-
|
|
3816
|
-
|
|
4186
|
+
// M7:字段名后**必须**有空白才取值为字段,且值里不许含分隔符 `|`。
|
|
4187
|
+
// 旧写法 `/问题\s*(\S+)/` 在"问题为空、方向非空"时把分隔符也吃进去:
|
|
4188
|
+
// `应用 1|<时间>|问题 |方向 d1` → 问题='|方向'(composeMethodMd 恰好会写出这种标题),
|
|
4189
|
+
// 下一次回写就把这个脏值固化进方法卡。
|
|
4190
|
+
const mQ = /(?:^|[\s|])问题[ \t]+([^|\s]+)/.exec(t)
|
|
4191
|
+
const mD = /(?:^|[\s|])方向[ \t]+([^|\s]+)/.exec(t)
|
|
3817
4192
|
// `at` = 第一段(时间戳)。取第一个 '|' 之前的部分。
|
|
3818
4193
|
const at = t.split('|')[0].trim()
|
|
3819
4194
|
return { at: at, 问题: mQ ? mQ[1] : '', 方向: mD ? mD[1] : '' }
|
|
@@ -3828,8 +4203,13 @@ function parseMethodMd(id, text) {
|
|
|
3828
4203
|
})
|
|
3829
4204
|
// 改进历史必须真正解析回来:此前硬编码 [] 导致每次 compose 都写成占位符,
|
|
3830
4205
|
// 于是"下一次用到该方法"就把代理沉淀的改进历史静默销毁(违反「正文只追加,不覆盖」)。
|
|
4206
|
+
//
|
|
4207
|
+
// H3:原因必须从**标题行独立解析**,不能借道 parseEntries 的槽位。约定里 m[1]=标题段、
|
|
4208
|
+
// m[2]=概率段、m[3]=状态段,而改进标题 `### v3(<原因>)` 只有两个捕获组:m[2] 是原因、
|
|
4209
|
+
// m[3] 是 undefined ⇒ `e.status` 恒为 '',每次回写都写成 `### v1()`(原因永久丢失)。
|
|
3831
4210
|
const improvements = parseEntries(body, /^###\s*v(\d+)\s*((.*?))\s*$/).map(function (e) {
|
|
3832
|
-
|
|
4211
|
+
const m = /^###\s*v(\d+)\s*((.*?))\s*$/.exec(e.heading)
|
|
4212
|
+
return { v: m ? Number(m[1]) : 0, 原因: m ? m[2] : '', text: e.text }
|
|
3833
4213
|
})
|
|
3834
4214
|
return {
|
|
3835
4215
|
id: id, 标题: a['标题'] || id, 类型: a['类型'] || '方法', 状态: a['状态'] || '经验',
|