dsh-vibe-math 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +41 -2
- package/README.md +41 -2
- package/cordis.patch.yml +22 -8
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +171 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
- package/docs/test-timing.md +8 -4
- package/package.json +5 -3
- package/prompt-corpus-persona/persona-corpus.json +13 -13
- package/prompt-corpus-persona/persona-corpus.md +49 -21
- package/prompt-corpus-v2/formal-verify-v2.json +12 -0
- package/prompt-corpus-v2/formal-verify-v2.md +144 -53
- package/prompt-corpus-v3/formal-verify-v3.json +36 -0
- package/prompt-corpus-v3/formal-verify-v3.md +405 -78
- package/tests/audit-persona-sensitivity.mjs +60 -7
- package/tests/audit-persona-surface.test.mjs +16 -7
- package/tests/e2e-v5-round2.test.mjs +471 -1
- package/tests/formal-verify-v2.test.mjs +3 -0
- package/tests/formal-verify-v3.test.mjs +4 -0
- package/tests/prompt-v5-integrity.test.mjs +9 -3
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +14 -4
- package/vibe-math-v2/vibe-math-v2.js +819 -11
- package/vibe-math-v3/agent.cordis.yml +10 -4
- package/vibe-math-v3/vibe-math-v3.js +800 -11
- package/vibe-math-v4/agent.cordis.yml +12 -4
- package/vibe-math-v4/vibe-math-v4.js +793 -8
- package/vibe-math-v5/agent.cordis.yml +8 -4
- package/vibe-math-v5/vibe-math-v5.js +1116 -8
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +70 -1
|
@@ -68,6 +68,34 @@ const EV = {
|
|
|
68
68
|
formal: 'vibe5/formal',
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// ── final paper (docs/final-paper.md) ─────────────────────────────────────────
|
|
72
|
+
// The run's LAST deliverable. The permanent staff each write their own part, cross-review
|
|
73
|
+
// each other's part, and the editor named by `paperEditor` finalises — but ONLY after the
|
|
74
|
+
// whole institute has agreed the parts are deliverable, and (when the office edits) only
|
|
75
|
+
// after the office has consulted the institute. The phase runs BEFORE the completion flags
|
|
76
|
+
// are flipped (docs/final-paper.md §3): after `phase='solved'` the machinery refuses to wake
|
|
77
|
+
// members (`wakeIfIdle`), to convene meetings (`startMeeting`) or to dispatch an end.
|
|
78
|
+
const PAPER_MAX_ROUNDS = 3
|
|
79
|
+
// Chinese first (docs/final-paper.md §8): xelatex covers CJK out of the box; English prefers pdflatex.
|
|
80
|
+
const PAPER_ENGINE_ORDER = {
|
|
81
|
+
zh: ['xelatex', 'latexmk', 'pdflatex', 'lualatex', 'tectonic'],
|
|
82
|
+
en: ['pdflatex', 'latexmk', 'lualatex', 'tectonic', 'xelatex'],
|
|
83
|
+
}
|
|
84
|
+
// The repair pass keeps only what the text itself needs; everything else (hyperref,
|
|
85
|
+
// longtable, booktabs, an unknown package) is dropped before the retry.
|
|
86
|
+
const PAPER_REPAIR_CORE = ['amsmath', 'amssymb', 'amsthm', 'geometry', 'ctex']
|
|
87
|
+
const PAPER_SECTIONS = [
|
|
88
|
+
'标题、作者、日期与摘要',
|
|
89
|
+
'引言与问题背景',
|
|
90
|
+
'原问题的完整解法',
|
|
91
|
+
'已检验通过的命题',
|
|
92
|
+
'已解决的子问题 / 中间成果',
|
|
93
|
+
'创造或发现的有价值之物:方法、理论、思想、经验与数学理解',
|
|
94
|
+
'规律总结',
|
|
95
|
+
'讨论、局限与展望',
|
|
96
|
+
'附录:证据与文件索引',
|
|
97
|
+
]
|
|
98
|
+
|
|
71
99
|
// Stable error codes (ported from DSH agent-teams' typed-error discipline).
|
|
72
100
|
function v5err(code, message) {
|
|
73
101
|
const e = new Error(message || code)
|
|
@@ -189,6 +217,9 @@ export function apply(ctx) {
|
|
|
189
217
|
lastProgressAt: now(),
|
|
190
218
|
artifactCount: 0,
|
|
191
219
|
diagnostics: [],
|
|
220
|
+
// The final-paper flow (stage machine + its artifacts state). Durable like everything
|
|
221
|
+
// else, so a restart resumes the same stage instead of writing a second paper.
|
|
222
|
+
paper: null,
|
|
192
223
|
}
|
|
193
224
|
}
|
|
194
225
|
function initState() { return { v: PROJECTION_VERSION, institutes: {}, order: [] } }
|
|
@@ -228,6 +259,16 @@ export function apply(ctx) {
|
|
|
228
259
|
if (patch.runId !== undefined) n.runId = String(patch.runId)
|
|
229
260
|
if (patch.lastProgressAt !== undefined) n.lastProgressAt = Number(patch.lastProgressAt) || 0
|
|
230
261
|
if (patch.artifactCount !== undefined) n.artifactCount = Number(patch.artifactCount) || 0
|
|
262
|
+
// The final-paper state. A FUNCTION is a MUTATION applied inside the fold (the same
|
|
263
|
+
// trick the verify queue uses): the paper stage, its parts and its office-consultation
|
|
264
|
+
// counters are updated from several places (member replies, office messages, meetings),
|
|
265
|
+
// and a whole-object read-modify-write outside the fold would lose updates.
|
|
266
|
+
if (patch.paper !== undefined) {
|
|
267
|
+
const next = typeof patch.paper === 'function'
|
|
268
|
+
? patch.paper(n.paper === undefined ? null : n.paper)
|
|
269
|
+
: patch.paper
|
|
270
|
+
if (next !== undefined) n.paper = next
|
|
271
|
+
}
|
|
231
272
|
return n
|
|
232
273
|
})
|
|
233
274
|
}
|
|
@@ -510,6 +551,22 @@ export function apply(ctx) {
|
|
|
510
551
|
leanCommand: 'lean',
|
|
511
552
|
leanArgs: [],
|
|
512
553
|
leanTimeoutMs: 120000,
|
|
554
|
+
// ── final paper (docs/final-paper.md; the phase runs BEFORE the completion flags) ──
|
|
555
|
+
// finalPaper — write the final paper when the run concludes (manual /v5 paper
|
|
556
|
+
// still works when this is false, and says so).
|
|
557
|
+
// paperFormat — 'both' | 'md' | 'tex'
|
|
558
|
+
// paperLanguage — 'zh' | 'en'
|
|
559
|
+
// paperCompilePdf — compile a PDF when a LaTeX engine is detected
|
|
560
|
+
// paperEditor — 'academician' (default: the only editor reachable unattended) |
|
|
561
|
+
// 'office' (manual /v5 paper only: the office must consult the
|
|
562
|
+
// institute first — messages + at least one meeting)
|
|
563
|
+
// paperLatexCommand — force ONE engine command instead of auto-detection ('' = auto)
|
|
564
|
+
finalPaper: true,
|
|
565
|
+
paperFormat: 'both',
|
|
566
|
+
paperLanguage: 'zh',
|
|
567
|
+
paperCompilePdf: true,
|
|
568
|
+
paperEditor: 'academician',
|
|
569
|
+
paperLatexCommand: '',
|
|
513
570
|
// ── model / tools ────────────────────────────────────────────────────
|
|
514
571
|
provider: '',
|
|
515
572
|
model: '',
|
|
@@ -1248,6 +1305,10 @@ export function apply(ctx) {
|
|
|
1248
1305
|
}
|
|
1249
1306
|
counters.message = n
|
|
1250
1307
|
await putCounters(counters)
|
|
1308
|
+
// The office talking to the institute is HALF of the `paperEditor='office'` consultation
|
|
1309
|
+
// requirement; counting it here (the one place a message really is queued) means the gate
|
|
1310
|
+
// cannot be satisfied by merely intending to consult (docs/final-paper.md §7).
|
|
1311
|
+
if (from === 'office') await notePaperConsult('message')
|
|
1251
1312
|
notifyActivity()
|
|
1252
1313
|
// Kick one scheduling pass so an ADDRESSED message (dm/office/assign) wakes its
|
|
1253
1314
|
// recipient promptly instead of waiting for the digest window. Plain chat stays
|
|
@@ -3311,6 +3372,11 @@ export function apply(ctx) {
|
|
|
3311
3372
|
solveVotes.clear()
|
|
3312
3373
|
await putMeeting({ id, agenda: meeting.agenda, kind: meeting.kind, at: now(), file: 'Shared/Meetings/' + id + '.md' })
|
|
3313
3374
|
await saveChatLine('【会议 ' + id + '】召开:' + meeting.agenda + '(类型:' + meeting.kind + '|召集人:' + meeting.by + ')')
|
|
3375
|
+
// A meeting that ACTUALLY begins and was convened by the office is the other half of the
|
|
3376
|
+
// `paperEditor='office'` consultation requirement (docs/final-paper.md §7). Counted here, not at the
|
|
3377
|
+
// request: a parked/refused request is not a consultation. A FRAMEWORK-convened stall
|
|
3378
|
+
// meeting (`auto:true`) is not the office's own act, so it does not count either.
|
|
3379
|
+
if (meeting.by === 'office' && opts.auto !== true) await notePaperConsult('meeting')
|
|
3314
3380
|
await mkdirs()
|
|
3315
3381
|
await writeTextRel('Shared/Meetings/' + id + '.md', [
|
|
3316
3382
|
'# 会议纪要|' + id + '|' + instituteName,
|
|
@@ -3429,9 +3495,24 @@ export function apply(ctx) {
|
|
|
3429
3495
|
// Stop ONLY on a unanimous true solve-vote from every VOTING member. There is no
|
|
3430
3496
|
// forced/flat/near-consensus closure: any objection keeps the institute working.
|
|
3431
3497
|
async function checkSolved() {
|
|
3498
|
+
if (autoDone) return true
|
|
3432
3499
|
const vs = voters().map((m) => m.id)
|
|
3433
3500
|
if (!vs.length) return false
|
|
3434
3501
|
if (!vs.every((id) => solveVotes.get(id) === true)) return false
|
|
3502
|
+
// ── THE FINAL PAPER PHASE COMES FIRST (docs/final-paper.md §3) ────────────────────────────────
|
|
3503
|
+
// `finishRun` flips phase='solved' / autoDone / running=false, after which the machinery
|
|
3504
|
+
// REFUSES: `wakeIfIdle` returns false, `startMeeting` rejects a concluded institute and
|
|
3505
|
+
// nothing schedules — so the co-writing, the cross-review and the office consultation
|
|
3506
|
+
// could never run. The run therefore stays alive here until the paper is finalised
|
|
3507
|
+
// (or degraded with a report), and `finalizePaper` calls `finishRun` for us.
|
|
3508
|
+
if (params.finalPaper !== false) {
|
|
3509
|
+
const p = paper()
|
|
3510
|
+
if (!p) { await startPaper('run-complete', {}); return true }
|
|
3511
|
+
if (p.status !== 'finalized') {
|
|
3512
|
+
if (!p.completesRun) await mutatePaper((cur) => (cur ? Object.assign({}, cur, { completesRun: true }) : cur))
|
|
3513
|
+
return true
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3435
3516
|
await finishRun('全体有表决权者一致认为原问题已解决')
|
|
3436
3517
|
return true
|
|
3437
3518
|
}
|
|
@@ -3469,6 +3550,916 @@ export function apply(ctx) {
|
|
|
3469
3550
|
notifyActivity()
|
|
3470
3551
|
}
|
|
3471
3552
|
|
|
3553
|
+
// ================= final paper (docs/final-paper.md) ============
|
|
3554
|
+
// THE PHASE RUNS BEFORE `finishRun` (docs/final-paper.md §3). After `phase='solved'` the machinery
|
|
3555
|
+
// refuses: `wakeIfIdle` (:3257) returns false, `startMeeting` refuses a concluded
|
|
3556
|
+
// institute, and a concluded run has no scheduling at all — so the co-writing, the
|
|
3557
|
+
// cross-review and the office consultation could never run. `checkSolved` therefore
|
|
3558
|
+
// enters this phase while the run is still alive and only calls `finishRun` once the
|
|
3559
|
+
// paper is finalised (or degraded with a report).
|
|
3560
|
+
//
|
|
3561
|
+
// Only evidence that ALREADY exists is written down: verified verdicts, the task board,
|
|
3562
|
+
// the Lean records, the members' libraries and the members' own text. The framework never
|
|
3563
|
+
// invents content — sections 3/6/7/8 are the members' own words, 4/5/9 are derived from
|
|
3564
|
+
// durable state, and undecided/refuted items are always marked as such.
|
|
3565
|
+
const PAPER_FILE = (id, file) => 'Paper/' + id + '/' + file
|
|
3566
|
+
function paper() { return inst().paper || null }
|
|
3567
|
+
function paperActive() { const p = paper(); return !!p && p.status !== 'finalized' }
|
|
3568
|
+
function paperParticipants() {
|
|
3569
|
+
return activeMembers().filter((m) => m.kind === 'academician' || m.kind === 'researcher').map((m) => m.id)
|
|
3570
|
+
}
|
|
3571
|
+
function paperActiveParticipants(p) {
|
|
3572
|
+
return ((p && p.participants) || []).filter((id) => { const m = memberById(id); return !!m && m.phase === 'active' })
|
|
3573
|
+
}
|
|
3574
|
+
function paperConfiguredEditor() { return params.paperEditor === 'office' ? 'office' : 'academician' }
|
|
3575
|
+
function paperIdFromState() {
|
|
3576
|
+
const a = idSafe(instituteName)
|
|
3577
|
+
if (a) return a
|
|
3578
|
+
const b = idSafe(String(runId || (inst() && inst().runId) || ''))
|
|
3579
|
+
return b || 'institute'
|
|
3580
|
+
}
|
|
3581
|
+
async function fileExistsAbs(abs) {
|
|
3582
|
+
try { const t = await fsTargetAbs(abs); return (await fs.stat(t)) !== undefined } catch (e) { return false }
|
|
3583
|
+
}
|
|
3584
|
+
function paperAbs(id, file) { return instRoot() + '/' + PAPER_FILE(id, file) }
|
|
3585
|
+
function paperDirRel(id) { return 'Paper/' + id }
|
|
3586
|
+
async function mutatePaper(fn) {
|
|
3587
|
+
await patchInstitute({ paper: (cur) => { const next = fn(cur === undefined ? null : cur); return next === undefined ? (cur === undefined ? null : cur) : next } })
|
|
3588
|
+
return paper()
|
|
3589
|
+
}
|
|
3590
|
+
async function paperLog(title, body) {
|
|
3591
|
+
const p = paper()
|
|
3592
|
+
const id = (p && p.id) || paperIdFromState()
|
|
3593
|
+
const rel = PAPER_FILE(id, 'paper.log.md')
|
|
3594
|
+
const prev = (await readTextRel(rel)) || ('# 最终论文流程日志|' + id + '\n\n')
|
|
3595
|
+
await writeTextRel(rel, prev + '## ' + title + '\n\n' + String(body || '') + '\n\n')
|
|
3596
|
+
return rel
|
|
3597
|
+
}
|
|
3598
|
+
function paperAskEvery() { return posMs(params.activityTimeoutMs, 120000) }
|
|
3599
|
+
function paperNextStep(p) {
|
|
3600
|
+
if (!p) return 'no paper flow yet: finish the run with finalPaper=true, or trigger /v5 paper manually'
|
|
3601
|
+
if (p.status === 'finalized') return 'finalised — /v5 paper force rewrites it'
|
|
3602
|
+
if (p.status === 'writing') return 'waiting for the staff parts (round ' + p.round + '/' + PAPER_MAX_ROUNDS + ')'
|
|
3603
|
+
if (p.status === 'reviewing') return 'waiting for the cross-reviews (each reviewer judges another part)'
|
|
3604
|
+
if (p.status === 'awaiting-editor' && p.editor === 'office') {
|
|
3605
|
+
return 'awaiting the OFFICE: send >=1 office message and convene >=1 meeting, then call vibe_v5_finalize_paper (consultation so far: messages=' +
|
|
3606
|
+
Number((p.consult || {}).messages || 0) + ', meetings=' + Number((p.consult || {}).meetings || 0) + ')'
|
|
3607
|
+
}
|
|
3608
|
+
if (p.status === 'awaiting-editor') return 'waiting for the academician to finalise'
|
|
3609
|
+
return p.status
|
|
3610
|
+
}
|
|
3611
|
+
function paperAutoNote() {
|
|
3612
|
+
return params.finalPaper === false ? '自动已关闭(finalPaper=false);本次为手动触发,仍会生成论文。' : ''
|
|
3613
|
+
}
|
|
3614
|
+
function paperSummary() {
|
|
3615
|
+
const p = paper()
|
|
3616
|
+
if (!p) return null
|
|
3617
|
+
return {
|
|
3618
|
+
id: p.id, status: p.status, stage: p.stage, round: p.round,
|
|
3619
|
+
editor: p.editor, editorFallback: !!p.editorFallback, completesRun: !!p.completesRun,
|
|
3620
|
+
participants: p.participants || [],
|
|
3621
|
+
parts: Object.keys(p.parts || {}).map((id) => ({ member: id, round: (p.parts[id] || {}).round, title: (p.parts[id] || {}).title || '' })),
|
|
3622
|
+
reviews: Object.keys(p.reviews || {}).map((id) => ({ member: id, of: (p.reviews[id] || {}).of || '', round: (p.reviews[id] || {}).round, deliverable: (p.reviews[id] || {}).deliverable === true })),
|
|
3623
|
+
reviewOf: p.reviewOf || {},
|
|
3624
|
+
consult: Object.assign({ messages: 0, meetings: 0 }, p.consult || {}),
|
|
3625
|
+
lang: p.lang, format: p.format, compilePdf: p.pdf === true,
|
|
3626
|
+
compile: p.compile || null, engine: p.engine || '', artifacts: p.artifacts || [],
|
|
3627
|
+
warnings: p.warnings || [], disagreement: p.disagreement || [], skipped: p.skipped || [],
|
|
3628
|
+
forcedAfterCap: !!p.forcedAfterCap, finalizedAt: p.finalizedAt || null,
|
|
3629
|
+
dir: 'Paper/' + p.id + '/', next: paperNextStep(p),
|
|
3630
|
+
meta: p.status === 'finalized' ? 'Paper/' + p.id + '/paper.meta.json' : null,
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
// ---- the three asks ----------------------------------------------------
|
|
3635
|
+
function paperWritePrompt(member, p) {
|
|
3636
|
+
const L = []
|
|
3637
|
+
L.push('【最终论文·撰写 —— ' + kindLabel(member.kind) + ' ' + member.id + '】')
|
|
3638
|
+
L.push('本所对原问题的一致结论已经达成,现在撰写**最终论文**(第 ' + p.round + '/' + PAPER_MAX_ROUNDS + ' 轮)。')
|
|
3639
|
+
L.push('请你**只写你自己库里已有证据支撑**的内容:')
|
|
3640
|
+
L.push('- 直接引用你的卡片(Members/' + member.id + '/Propos|Methods|Subproblems/)、Members/' + member.id + '/Progress/progress.md、你参与的表决记录;')
|
|
3641
|
+
L.push('- **不得编造**:没有证据的推测不要写成结论;未决 / 被否证的条目必须显式标注“未定论 / 已被否证”;')
|
|
3642
|
+
L.push('- 在 evidence 里写清证据路径,附录会逐条索引。')
|
|
3643
|
+
if (p.round > 1) {
|
|
3644
|
+
const rv = (p.reviews || {})[member.id]
|
|
3645
|
+
L.push('')
|
|
3646
|
+
L.push('上一轮互审给你(' + member.id + ')部分的意见:' + (rv ? String(rv.comments || '(无具体意见)') : '(无)'))
|
|
3647
|
+
L.push('请据此修订;若不同意该意见,请在 limits 里写明理由。')
|
|
3648
|
+
}
|
|
3649
|
+
L.push('')
|
|
3650
|
+
L.push('------------')
|
|
3651
|
+
L.push(stateBlock(member))
|
|
3652
|
+
L.push('------------')
|
|
3653
|
+
L.push('结束时只输出一个 JSON 对象:')
|
|
3654
|
+
L.push('{ "paper_part": {')
|
|
3655
|
+
L.push(' "title": "你这部分的标题",')
|
|
3656
|
+
L.push(' "solution": "你对**原问题完整解法**的贡献(推理链与结论,只写有证据的)",')
|
|
3657
|
+
L.push(' "methods": "你创造/发现的方法、理论、思想、有价值经验、数学理解",')
|
|
3658
|
+
L.push(' "rules": "你从这些工作中归纳出的可复用规律",')
|
|
3659
|
+
L.push(' "limits": "局限、未决、被否证之处(必须诚实、显式)",')
|
|
3660
|
+
L.push(' "evidence": ["Members/' + member.id + '/Propos/p-x.md"] } }')
|
|
3661
|
+
return L.join('\n')
|
|
3662
|
+
}
|
|
3663
|
+
function paperReviewPrompt(member, p, ofId) {
|
|
3664
|
+
const part = ofId ? (p.parts || {})[ofId] : null
|
|
3665
|
+
const L = []
|
|
3666
|
+
L.push('【最终论文·互审 —— ' + kindLabel(member.kind) + ' ' + member.id + '】')
|
|
3667
|
+
L.push('请你审阅 **' + (ofId || '(未指定)') + '** 撰写的部分,判断它是否可以交付(deliverable)。')
|
|
3668
|
+
L.push('要点:证据是否充分;是否与已定论的表决一致;有无编造或未标注的未决项;术语与符号是否清楚。')
|
|
3669
|
+
L.push('')
|
|
3670
|
+
L.push('------------ 待审部分(' + (ofId || '') + ')------------')
|
|
3671
|
+
L.push('标题:' + ((part && part.title) || '(无)'))
|
|
3672
|
+
L.push('【完整解法】' + String((part && part.solution) || '(空)').slice(0, 6000))
|
|
3673
|
+
L.push('【方法/理论/思想/经验/理解】' + String((part && part.methods) || '(空)').slice(0, 4000))
|
|
3674
|
+
L.push('【规律】' + String((part && part.rules) || '(空)').slice(0, 3000))
|
|
3675
|
+
L.push('【局限/未决】' + String((part && part.limits) || '(空)').slice(0, 3000))
|
|
3676
|
+
L.push('【声称的证据】' + ((((part && part.evidence) || []).join('、')) || '(无)'))
|
|
3677
|
+
L.push('------------')
|
|
3678
|
+
L.push('')
|
|
3679
|
+
L.push(stateBlock(member))
|
|
3680
|
+
L.push('')
|
|
3681
|
+
L.push('结束时只输出一个 JSON 对象:')
|
|
3682
|
+
L.push('{ "paper_review": { "of": "' + (ofId || '') + '", "deliverable": true, "comments": "具体意见" } }')
|
|
3683
|
+
L.push('deliverable 必须是 true(可交付)或 false(不可交付,需修改)。只有**全体参与成员**都投 true,定稿代表才能定稿。')
|
|
3684
|
+
return L.join('\n')
|
|
3685
|
+
}
|
|
3686
|
+
function paperFinalPrompt(member, p) {
|
|
3687
|
+
const L = []
|
|
3688
|
+
L.push('【最终论文·定稿(院士)—— ' + member.id + '】')
|
|
3689
|
+
L.push('全体参与成员已在互审中表示“可交付”。请你作为定稿代表做**最后一次**把关:')
|
|
3690
|
+
L.push('核对合并稿与互审意见,确认没有编造、没有未标注的未决项、没有与表决记录矛盾之处,然后给出决定。')
|
|
3691
|
+
L.push('')
|
|
3692
|
+
L.push('------------ 合并稿(各成员部分)------------')
|
|
3693
|
+
for (const id of paperActiveParticipants(p)) {
|
|
3694
|
+
const part = (p.parts || {})[id] || {}
|
|
3695
|
+
L.push('### ' + id + '|' + String(part.title || ''))
|
|
3696
|
+
L.push('【完整解法】' + String(part.solution || '').slice(0, 3000))
|
|
3697
|
+
L.push('【方法/规律】' + (String(part.methods || '') + ' ' + String(part.rules || '')).slice(0, 2000))
|
|
3698
|
+
L.push('【局限】' + String(part.limits || '').slice(0, 1500))
|
|
3699
|
+
L.push('')
|
|
3700
|
+
}
|
|
3701
|
+
L.push('------------ 互审结论 ------------')
|
|
3702
|
+
for (const id of Object.keys(p.reviews || {})) {
|
|
3703
|
+
const rv = p.reviews[id] || {}
|
|
3704
|
+
L.push('- ' + id + ' 审 ' + (rv.of || '?') + ':deliverable=' + (rv.deliverable === true) + '|' + String(rv.comments || '').slice(0, 500))
|
|
3705
|
+
}
|
|
3706
|
+
if ((p.disagreement || []).length) L.push('- ⚠ 已达轮次上限仍有分歧:' + JSON.stringify(p.disagreement[p.disagreement.length - 1]).slice(0, 500))
|
|
3707
|
+
L.push('------------')
|
|
3708
|
+
L.push('')
|
|
3709
|
+
L.push(stateBlock(member))
|
|
3710
|
+
L.push('')
|
|
3711
|
+
L.push('结束时只输出一个 JSON 对象:')
|
|
3712
|
+
L.push('{ "paper_final": { "decision": "deliverable" | "revise",')
|
|
3713
|
+
L.push(' "note": "定稿说明:你如何审阅、统一术语与符号、是否发现并纠正了问题",')
|
|
3714
|
+
L.push(' "conclusion": "(可选)定稿代表对原问题的最终结论(只写有证据的)" } }')
|
|
3715
|
+
L.push('decision="revise" 会退回继续修订(有轮次上限)。')
|
|
3716
|
+
return L.join('\n')
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// ---- the stage driver --------------------------------------------------
|
|
3720
|
+
let paperLock = false
|
|
3721
|
+
async function paperStep() {
|
|
3722
|
+
const p = paper()
|
|
3723
|
+
if (!p || p.status === 'finalized' || p.status === 'awaiting-editor') return false
|
|
3724
|
+
const active = paperActiveParticipants(p)
|
|
3725
|
+
if (!active.length) return await paperAdvance(p, active)
|
|
3726
|
+
const parts = p.parts || {}
|
|
3727
|
+
const reviews = p.reviews || {}
|
|
3728
|
+
const missing = p.status === 'writing'
|
|
3729
|
+
? active.filter((id) => !(parts[id] && parts[id].round === p.round))
|
|
3730
|
+
: active.filter((id) => !(reviews[id] && reviews[id].round === p.round))
|
|
3731
|
+
if (!missing.length) return await paperAdvance(p, active)
|
|
3732
|
+
const budget = Math.max(1, Math.floor(Number(params.maxParallel) || 3))
|
|
3733
|
+
let asked = 0
|
|
3734
|
+
for (const id of missing) {
|
|
3735
|
+
if (asked >= budget) break
|
|
3736
|
+
if (busy.has(id)) continue
|
|
3737
|
+
const m = memberById(id)
|
|
3738
|
+
if (!m || m.phase !== 'active') continue
|
|
3739
|
+
const last = (p.lastAskAt || {})[id] || 0
|
|
3740
|
+
if (last && (now() - last) < paperAskEvery()) continue
|
|
3741
|
+
const prompt = p.status === 'writing'
|
|
3742
|
+
? paperWritePrompt(m, p)
|
|
3743
|
+
: paperReviewPrompt(m, p, (p.reviewOf || {})[id])
|
|
3744
|
+
// `wakeMember`, not `wakeIfIdle`: the paper phase deliberately drives members with a
|
|
3745
|
+
// dedicated ask, and a research round must never be mistaken for a paper answer.
|
|
3746
|
+
const ok = await wakeMember(m, prompt, 'paper')
|
|
3747
|
+
if (ok) {
|
|
3748
|
+
asked += 1
|
|
3749
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3750
|
+
lastAskAt: Object.assign({}, cur.lastAskAt || {}, { [id]: now() }), updatedAt: now(),
|
|
3751
|
+
}) : cur))
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
// Nothing to advance yet. ALWAYS re-arm: the pass that got here may have skipped an ask
|
|
3755
|
+
// because its member is busy or still inside the ask window, and `schedulePass` returns
|
|
3756
|
+
// from the paper branch BEFORE its own heartbeat arming — without this a paper whose ask
|
|
3757
|
+
// was paced out would stall until some unrelated member turn ended.
|
|
3758
|
+
if (asked) { armHeartbeat(); return false }
|
|
3759
|
+
// Stall watchdog (same clock as meetings/verifications): a member whose turn never ends
|
|
3760
|
+
// must not wedge the paper forever. The stuck members are recorded as non-participants.
|
|
3761
|
+
const stale = now() - Number(p.updatedAt || p.createdAt || now())
|
|
3762
|
+
if (stale >= recoverStallMs()) {
|
|
3763
|
+
const stuck = missing.filter((id) => busy.has(id))
|
|
3764
|
+
const name = stuck.length ? stuck : missing
|
|
3765
|
+
const warn = '看门狗:' + name.join('、') + ' 在 ' + Math.round(stale / 1000) + 's 内没有交稿,已按“未参与”处理并记入附录。'
|
|
3766
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3767
|
+
warnings: (cur.warnings || []).concat([warn]),
|
|
3768
|
+
skipped: Array.from(new Set((cur.skipped || []).concat(name))), updatedAt: now(),
|
|
3769
|
+
}) : cur))
|
|
3770
|
+
await paperLog('看门狗', warn)
|
|
3771
|
+
return await paperAdvance(paper(), paperActiveParticipants(paper()))
|
|
3772
|
+
}
|
|
3773
|
+
armHeartbeat()
|
|
3774
|
+
return false
|
|
3775
|
+
}
|
|
3776
|
+
async function paperAdvance(p, active) {
|
|
3777
|
+
if (p.status === 'writing') {
|
|
3778
|
+
const reviewOf = {}
|
|
3779
|
+
for (let i = 0; i < active.length; i++) reviewOf[active[i]] = active[(i + 1) % active.length]
|
|
3780
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3781
|
+
status: 'reviewing', stage: 'reviewing', reviewOf, reviews: {}, lastAskAt: {}, updatedAt: now(),
|
|
3782
|
+
}) : cur))
|
|
3783
|
+
await paperLog('进入互审(第 ' + p.round + ' 轮)', active.map((a) => a + ' 审 ' + reviewOf[a]).join(';') || '(无参与成员)')
|
|
3784
|
+
return true
|
|
3785
|
+
}
|
|
3786
|
+
if (p.status === 'reviewing') {
|
|
3787
|
+
const reviews = p.reviews || {}
|
|
3788
|
+
const refusers = active.filter((id) => !(reviews[id] && reviews[id].deliverable === true))
|
|
3789
|
+
if (!refusers.length) {
|
|
3790
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, { status: 'awaiting-editor', stage: 'awaiting-editor', lastAskAt: {}, updatedAt: now() }) : cur))
|
|
3791
|
+
await paperLog('互审一致:可交付', '全体参与成员(' + active.join('、') + ')均表示可交付。定稿代表:' + p.editor)
|
|
3792
|
+
return true
|
|
3793
|
+
}
|
|
3794
|
+
const detail = refusers.map((id) => id + ':' + String((reviews[id] || {}).comments || '未说明')).join(';')
|
|
3795
|
+
const entry = { round: p.round, members: refusers, detail, at: now() }
|
|
3796
|
+
if (p.round < PAPER_MAX_ROUNDS) {
|
|
3797
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3798
|
+
status: 'writing', stage: 'writing', round: cur.round + 1, lastAskAt: {}, updatedAt: now(),
|
|
3799
|
+
disagreement: (cur.disagreement || []).concat([entry]),
|
|
3800
|
+
}) : cur))
|
|
3801
|
+
await paperLog('互审未达成一致 → 进入第 ' + (p.round + 1) + ' 轮', detail)
|
|
3802
|
+
return true
|
|
3803
|
+
}
|
|
3804
|
+
const warn = '互审在 ' + PAPER_MAX_ROUNDS + ' 轮上限仍未达成全体一致(' + refusers.join('、') + ');分歧已写入论文附录。'
|
|
3805
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3806
|
+
status: 'awaiting-editor', stage: 'awaiting-editor', lastAskAt: {}, updatedAt: now(),
|
|
3807
|
+
warnings: (cur.warnings || []).concat([warn]), disagreement: (cur.disagreement || []).concat([entry]), forcedAfterCap: true,
|
|
3808
|
+
}) : cur))
|
|
3809
|
+
await paperLog('达到轮次上限:转交定稿代表', warn)
|
|
3810
|
+
return true
|
|
3811
|
+
}
|
|
3812
|
+
return false
|
|
3813
|
+
}
|
|
3814
|
+
async function paperAwaitEditor(p) {
|
|
3815
|
+
if (p.editor !== 'academician') return false // office: waits for vibe_v5_finalize_paper
|
|
3816
|
+
const id = academicianId()
|
|
3817
|
+
if (!id) {
|
|
3818
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3819
|
+
editor: 'office', editorFallback: true, updatedAt: now(),
|
|
3820
|
+
warnings: (cur.warnings || []).concat(['定稿代表 academician 不在册:已回退为 office(需人工定稿)。']),
|
|
3821
|
+
}) : cur))
|
|
3822
|
+
await paperLog('定稿代表回退', 'academician 不在册,回退为 office:用 /v5 paper 或 vibe_v5_finalize_paper 定稿(仍需先与全所交流并开会)。')
|
|
3823
|
+
return false
|
|
3824
|
+
}
|
|
3825
|
+
const m = memberById(id)
|
|
3826
|
+
if (!m || m.phase !== 'active') return false
|
|
3827
|
+
if (busy.has(id)) { armHeartbeat(); return false }
|
|
3828
|
+
const last = (p.lastAskAt || {})[id] || 0
|
|
3829
|
+
if (last && (now() - last) < paperAskEvery()) { armHeartbeat(); return false }
|
|
3830
|
+
const ok = await wakeMember(m, paperFinalPrompt(m, p), 'paper')
|
|
3831
|
+
if (ok) await mutatePaper((cur) => (cur ? Object.assign({}, cur, { lastAskAt: Object.assign({}, cur.lastAskAt || {}, { [id]: now() }), updatedAt: now() }) : cur))
|
|
3832
|
+
armHeartbeat()
|
|
3833
|
+
return false
|
|
3834
|
+
}
|
|
3835
|
+
// The ONE driver. Called from `onMemberEnd` (after the reply is folded), from the manual
|
|
3836
|
+
// tools/command, from the resume path and from `schedulePass`; `paperLock` keeps it single.
|
|
3837
|
+
async function paperPass() {
|
|
3838
|
+
if (paperLock) return false
|
|
3839
|
+
paperLock = true
|
|
3840
|
+
try {
|
|
3841
|
+
for (let i = 0; i < 6; i++) {
|
|
3842
|
+
const p = paper()
|
|
3843
|
+
if (!p || p.status === 'finalized') return false
|
|
3844
|
+
if (p.status === 'awaiting-editor') { await paperAwaitEditor(p); return false }
|
|
3845
|
+
const advanced = await paperStep()
|
|
3846
|
+
if (!advanced) return false
|
|
3847
|
+
}
|
|
3848
|
+
return false
|
|
3849
|
+
} catch (e) {
|
|
3850
|
+
console.error('vibe-math-v5: paper pass: ' + String((e && e.stack) || e))
|
|
3851
|
+
return false
|
|
3852
|
+
} finally { paperLock = false }
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// ---- start / record / consult / finalize -------------------------------
|
|
3856
|
+
async function startPaper(trigger, opts) {
|
|
3857
|
+
const o = opts || {}
|
|
3858
|
+
const trg = String(trigger || 'manual')
|
|
3859
|
+
const existing = paper()
|
|
3860
|
+
if (existing && existing.status === 'finalized' && !o.force) {
|
|
3861
|
+
const refill = await refillPaperArtifacts(existing)
|
|
3862
|
+
return { ok: true, alreadyFinalized: true, id: existing.id, dir: 'Paper/' + existing.id + '/', refill, autoDisabled: params.finalPaper === false, note: paperAutoNote() }
|
|
3863
|
+
}
|
|
3864
|
+
if (existing && existing.status !== 'finalized' && !o.force) {
|
|
3865
|
+
return { ok: true, alreadyRunning: true, id: existing.id, status: existing.status, stage: existing.stage, round: existing.round, next: paperNextStep(existing), autoDisabled: params.finalPaper === false, note: paperAutoNote() }
|
|
3866
|
+
}
|
|
3867
|
+
const participants = paperParticipants()
|
|
3868
|
+
// A per-call editor override (`/v5 paper editor=office`) is ONE-SHOT: it is not written
|
|
3869
|
+
// back into the persisted params. An AUTOMATIC (run-complete) trigger always uses the
|
|
3870
|
+
// academician — the office is the root session and has no wake path (docs/final-paper.md §7).
|
|
3871
|
+
const configured = (o.editor === 'office' || o.editor === 'academician') ? o.editor : paperConfiguredEditor()
|
|
3872
|
+
const editor = trg === 'run-complete' ? 'academician' : configured
|
|
3873
|
+
const configWantsOffice = configured === 'office'
|
|
3874
|
+
const editorNote = (trg === 'run-complete' && configWantsOffice)
|
|
3875
|
+
? 'paperEditor=office 已配置,但自动(收口)触发必须由**可唤醒**的院士定稿(所办是根会话,没有唤醒路径);本次自动流程用 academician。要所办定稿请在收尾后用 /v5 paper 手动触发。'
|
|
3876
|
+
: (editor === 'office' ? '所办定稿:必须先与全所交流(>=1 条所办消息)并至少召开一次会议,结论写进定稿说明,然后调用 vibe_v5_finalize_paper。' : '')
|
|
3877
|
+
const lang = (o.lang === 'en' || o.lang === 'zh') ? o.lang : (params.paperLanguage === 'en' ? 'en' : 'zh')
|
|
3878
|
+
const format = ['both', 'md', 'tex'].indexOf(o.format) !== -1 ? o.format : (['both', 'md', 'tex'].indexOf(params.paperFormat) !== -1 ? params.paperFormat : 'both')
|
|
3879
|
+
const p = {
|
|
3880
|
+
id: paperIdFromState(), status: 'writing', stage: 'writing', round: 1,
|
|
3881
|
+
trigger: trg, createdAt: now(), updatedAt: now(),
|
|
3882
|
+
participants, parts: {}, reviews: {}, reviewOf: {}, final: null,
|
|
3883
|
+
editor, editorNote, completesRun: trg === 'run-complete',
|
|
3884
|
+
lang, format, pdf: params.paperCompilePdf !== false,
|
|
3885
|
+
consult: { messages: 0, meetings: 0 },
|
|
3886
|
+
lastAskAt: {}, warnings: editorNote ? [editorNote] : [], disagreement: [], skipped: [], artifacts: [],
|
|
3887
|
+
forced: !!o.force, forceReason: o.force ? String(o.reason || 'manual force') : '',
|
|
3888
|
+
}
|
|
3889
|
+
await patchInstitute({ paper: p })
|
|
3890
|
+
await paperLog('流程开始(' + (trg === 'run-complete' ? '收口自动触发' : '手动触发') + ')', [
|
|
3891
|
+
'- 目录:Paper/' + p.id + '/',
|
|
3892
|
+
'- 语言:' + p.lang + '|格式:' + p.format + '|编译 PDF:' + p.pdf,
|
|
3893
|
+
'- 参与成员:' + (participants.join('、') || '(无常驻成员)'),
|
|
3894
|
+
'- 定稿代表:' + editor + (editorNote ? '(' + editorNote + ')' : ''),
|
|
3895
|
+
].join('\n'))
|
|
3896
|
+
await paperPass()
|
|
3897
|
+
return {
|
|
3898
|
+
ok: true, started: true, id: p.id, dir: 'Paper/' + p.id + '/', status: 'writing',
|
|
3899
|
+
participants, editor, next: paperNextStep(paper()),
|
|
3900
|
+
autoDisabled: params.finalPaper === false, note: paperAutoNote(),
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
async function paperRecordPart(memberId, raw) {
|
|
3904
|
+
const p = paper()
|
|
3905
|
+
if (!p || p.status !== 'writing') return { ok: false, code: 'V5_PAPER_STATE', message: 'the paper flow is not in its writing stage (now: ' + (p ? p.status : 'none') + ')' }
|
|
3906
|
+
if ((p.participants || []).indexOf(memberId) === -1) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: memberId + ' is not a writing participant of this paper' }
|
|
3907
|
+
const part = {
|
|
3908
|
+
member: memberId, round: p.round,
|
|
3909
|
+
title: String(raw.title || '').slice(0, 300),
|
|
3910
|
+
solution: String(raw.solution || '').slice(0, 20000),
|
|
3911
|
+
methods: String(raw.methods || '').slice(0, 20000),
|
|
3912
|
+
rules: String(raw.rules || '').slice(0, 12000),
|
|
3913
|
+
limits: String(raw.limits || '').slice(0, 12000),
|
|
3914
|
+
evidence: Array.isArray(raw.evidence) ? raw.evidence.map(String).slice(0, 50) : [],
|
|
3915
|
+
at: now(),
|
|
3916
|
+
}
|
|
3917
|
+
if (!part.title && !part.solution && !part.methods && !part.rules && !part.limits) {
|
|
3918
|
+
await notice(memberId, 'paper_part 是空的:请至少给出 title 与你的贡献正文(solution/methods/rules/limits),只写库里已有证据支撑的内容。')
|
|
3919
|
+
return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'paper_part carries no content' }
|
|
3920
|
+
}
|
|
3921
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, { parts: Object.assign({}, cur.parts || {}, { [memberId]: part }), updatedAt: now() }) : cur))
|
|
3922
|
+
await paperLog('收到 ' + memberId + ' 的部分(第 ' + p.round + ' 轮)', '- 标题:' + (part.title || '(无)') + '\n- 声称证据:' + (part.evidence.length || 0) + ' 条')
|
|
3923
|
+
return { ok: true, recorded: memberId, round: p.round }
|
|
3924
|
+
}
|
|
3925
|
+
async function paperRecordReview(memberId, raw) {
|
|
3926
|
+
const p = paper()
|
|
3927
|
+
if (!p || p.status !== 'reviewing') return { ok: false, code: 'V5_PAPER_STATE', message: 'the paper flow is not in its cross-review stage (now: ' + (p ? p.status : 'none') + ')' }
|
|
3928
|
+
if ((p.participants || []).indexOf(memberId) === -1) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: memberId + ' is not a review participant of this paper' }
|
|
3929
|
+
if (raw.deliverable !== true && raw.deliverable !== false) {
|
|
3930
|
+
await notice(memberId, 'paper_review.deliverable 必须是 true 或 false(你敢不敢交付?未表态不算可交付)。')
|
|
3931
|
+
return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'paper_review.deliverable must be a boolean' }
|
|
3932
|
+
}
|
|
3933
|
+
const assigned = (p.reviewOf || {})[memberId]
|
|
3934
|
+
const rev = {
|
|
3935
|
+
member: memberId, of: String(raw.of || assigned || ''), round: p.round,
|
|
3936
|
+
deliverable: raw.deliverable === true,
|
|
3937
|
+
comments: String(raw.comments || '').slice(0, 8000), at: now(),
|
|
3938
|
+
}
|
|
3939
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, { reviews: Object.assign({}, cur.reviews || {}, { [memberId]: rev }), updatedAt: now() }) : cur))
|
|
3940
|
+
await paperLog('互审意见|' + memberId + ' → ' + rev.of, '- deliverable:' + rev.deliverable + '\n- 意见:' + (rev.comments || '(无)').slice(0, 2000))
|
|
3941
|
+
return { ok: true, recorded: memberId, of: rev.of, deliverable: rev.deliverable }
|
|
3942
|
+
}
|
|
3943
|
+
async function paperRecordFinal(memberId, raw) {
|
|
3944
|
+
const p = paper()
|
|
3945
|
+
if (!p || p.status !== 'awaiting-editor') return { ok: false, code: 'V5_PAPER_STATE', message: 'the paper is not awaiting its editor (now: ' + (p ? p.status : 'none') + ')' }
|
|
3946
|
+
if (p.editor !== 'academician' || memberId !== academicianId()) {
|
|
3947
|
+
return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'paperEditor is "' + p.editor + '": the academician does not finalise this paper' }
|
|
3948
|
+
}
|
|
3949
|
+
const decision = String(raw.decision || '')
|
|
3950
|
+
if (decision !== 'deliverable' && decision !== 'revise') {
|
|
3951
|
+
await notice(memberId, "paper_final.decision 必须是 'deliverable' 或 'revise'(收到 " + decision + ')。')
|
|
3952
|
+
return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'decision must be deliverable|revise' }
|
|
3953
|
+
}
|
|
3954
|
+
const fin = { by: memberId, decision, note: String(raw.note || '').slice(0, 4000), conclusion: String(raw.conclusion || '').slice(0, 20000), at: now() }
|
|
3955
|
+
if (decision === 'deliverable') {
|
|
3956
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, { final: fin, updatedAt: now() }) : cur))
|
|
3957
|
+
return await finalizePaper('academician-deliverable')
|
|
3958
|
+
}
|
|
3959
|
+
if (p.round < PAPER_MAX_ROUNDS) {
|
|
3960
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3961
|
+
final: fin, status: 'writing', stage: 'writing', round: cur.round + 1, lastAskAt: {}, updatedAt: now(),
|
|
3962
|
+
disagreement: (cur.disagreement || []).concat([{ round: cur.round, members: [memberId], detail: '定稿代表要求修订:' + fin.note, at: now() }]),
|
|
3963
|
+
}) : cur))
|
|
3964
|
+
await paperLog('定稿代表要求修订 → 第 ' + (p.round + 1) + ' 轮', fin.note || '(未说明)')
|
|
3965
|
+
return { ok: true, revised: true, round: p.round + 1 }
|
|
3966
|
+
}
|
|
3967
|
+
const warn = '定稿代表在第 ' + PAPER_MAX_ROUNDS + ' 轮上限仍要求修订;按规格记警告并定稿(分歧写入附录)。'
|
|
3968
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
3969
|
+
final: fin, warnings: (cur.warnings || []).concat([warn]), forcedAfterCap: true, updatedAt: now(),
|
|
3970
|
+
disagreement: (cur.disagreement || []).concat([{ round: cur.round, members: [memberId], detail: '定稿代表要求修订:' + fin.note, at: now() }]),
|
|
3971
|
+
}) : cur))
|
|
3972
|
+
await paperLog('达到轮次上限:定稿代表仍要求修订', warn)
|
|
3973
|
+
return await finalizePaper('academician-revise-at-cap')
|
|
3974
|
+
}
|
|
3975
|
+
// The office consultation gate (v2 §A6 / the user's requirement): the office may finalise
|
|
3976
|
+
// ONLY after it has actually talked to the institute (>=1 office message) AND convened at
|
|
3977
|
+
// least one meeting; both counts are recorded in the finalisation note and the meta.
|
|
3978
|
+
async function finalizePaperByOffice(o) {
|
|
3979
|
+
const args = o || {}
|
|
3980
|
+
const p = paper()
|
|
3981
|
+
if (!p) return { ok: false, code: 'V5_PAPER_STATE', message: 'no paper flow has been started (finish the run with finalPaper=true, or use /v5 paper)' }
|
|
3982
|
+
if (p.status === 'finalized' && !args.force) {
|
|
3983
|
+
const refill = await refillPaperArtifacts(p)
|
|
3984
|
+
return { ok: true, alreadyFinalized: true, id: p.id, refill, autoDisabled: params.finalPaper === false, note: paperAutoNote() }
|
|
3985
|
+
}
|
|
3986
|
+
if (p.status !== 'awaiting-editor') {
|
|
3987
|
+
return { ok: false, code: 'V5_PAPER_STATE', message: 'the paper flow is in stage "' + p.status + '"; the editor finalises only after every part is written and cross-reviewed' }
|
|
3988
|
+
}
|
|
3989
|
+
if (p.editor !== 'office') {
|
|
3990
|
+
return { ok: false, code: 'V5_NOT_OFFICE', message: 'paperEditor="academician": the academician finalises from its own round; the office does not' }
|
|
3991
|
+
}
|
|
3992
|
+
const decision = String(args.decision || '')
|
|
3993
|
+
if (decision !== 'deliverable' && decision !== 'revise') return { ok: false, code: 'V5_INVALID_ARGUMENT', message: "decision must be 'deliverable' or 'revise'" }
|
|
3994
|
+
const note = String(args.note || '').trim()
|
|
3995
|
+
if (decision === 'deliverable' && !note) {
|
|
3996
|
+
return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '定稿说明(note)是必填的:写明你如何与全所交流、商讨、优化、审查,以及结论' }
|
|
3997
|
+
}
|
|
3998
|
+
const consult = Object.assign({ messages: 0, meetings: 0 }, p.consult || {})
|
|
3999
|
+
if (!(Number(consult.messages) > 0) || !(Number(consult.meetings) > 0)) {
|
|
4000
|
+
return {
|
|
4001
|
+
ok: false, code: 'V5_PAPER_CONSULT_REQUIRED',
|
|
4002
|
+
message: 'paperEditor="office" 要求所办先与全所交流、商讨、优化、审查:至少 1 条所办消息(vibe_v5_message)+ 至少 1 次会议(vibe_v5_meeting),然后才能定稿。当前:messages=' +
|
|
4003
|
+
Number(consult.messages || 0) + ', meetings=' + Number(consult.meetings || 0),
|
|
4004
|
+
consult: { messages: Number(consult.messages || 0), meetings: Number(consult.meetings || 0) },
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
const fin = {
|
|
4008
|
+
by: 'office', decision, note: String(note).slice(0, 4000),
|
|
4009
|
+
conclusion: String(args.conclusion || '').slice(0, 20000),
|
|
4010
|
+
consult: { messages: Number(consult.messages || 0), meetings: Number(consult.meetings || 0) },
|
|
4011
|
+
at: now(),
|
|
4012
|
+
}
|
|
4013
|
+
if (decision === 'deliverable') {
|
|
4014
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, { final: fin, updatedAt: now() }) : cur))
|
|
4015
|
+
return await finalizePaper('office-deliverable')
|
|
4016
|
+
}
|
|
4017
|
+
if (p.round < PAPER_MAX_ROUNDS) {
|
|
4018
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
4019
|
+
final: fin, status: 'writing', stage: 'writing', round: cur.round + 1, lastAskAt: {}, updatedAt: now(),
|
|
4020
|
+
disagreement: (cur.disagreement || []).concat([{ round: cur.round, members: ['office'], detail: '所办要求修订:' + fin.note, at: now() }]),
|
|
4021
|
+
}) : cur))
|
|
4022
|
+
await paperLog('所办要求修订 → 第 ' + (p.round + 1) + ' 轮', fin.note)
|
|
4023
|
+
return { ok: true, revised: true, round: p.round + 1 }
|
|
4024
|
+
}
|
|
4025
|
+
const warn = '所办在第 ' + PAPER_MAX_ROUNDS + ' 轮上限仍要求修订;按规格记警告并定稿(分歧写入附录)。'
|
|
4026
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
4027
|
+
final: fin, warnings: (cur.warnings || []).concat([warn]), forcedAfterCap: true, updatedAt: now(),
|
|
4028
|
+
disagreement: (cur.disagreement || []).concat([{ round: cur.round, members: ['office'], detail: '所办要求修订:' + fin.note, at: now() }]),
|
|
4029
|
+
}) : cur))
|
|
4030
|
+
await paperLog('达到轮次上限:所办仍要求修订', warn)
|
|
4031
|
+
return await finalizePaper('office-revise-at-cap')
|
|
4032
|
+
}
|
|
4033
|
+
// Count the office's consultation evidence. Called from the ONE place each act really
|
|
4034
|
+
// happens (`say` for a message, `beginMeeting` for a meeting that actually starts), so the
|
|
4035
|
+
// gate cannot be satisfied by merely intending to consult.
|
|
4036
|
+
async function notePaperConsult(kind) {
|
|
4037
|
+
const p = paper()
|
|
4038
|
+
if (!p || p.status === 'finalized') return
|
|
4039
|
+
const key = kind === 'meeting' ? 'meetings' : 'messages'
|
|
4040
|
+
await mutatePaper((cur) => {
|
|
4041
|
+
if (!cur || cur.status === 'finalized') return cur
|
|
4042
|
+
const consult = Object.assign({ messages: 0, meetings: 0 }, cur.consult || {})
|
|
4043
|
+
consult[key] = Number(consult[key] || 0) + 1
|
|
4044
|
+
return Object.assign({}, cur, { consult, updatedAt: now() })
|
|
4045
|
+
})
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
// ---- artifacts ---------------------------------------------------------
|
|
4049
|
+
function paperEvidence() {
|
|
4050
|
+
const s = inst()
|
|
4051
|
+
const V = s.verdicts || {}
|
|
4052
|
+
const keys = Object.keys(V)
|
|
4053
|
+
const closed = (o) => keys.filter((k) => V[k] && V[k].closed && V[k].outcome === o)
|
|
4054
|
+
const tasks = (s.tasks || []).filter((t) => t && t.status !== 'deleted')
|
|
4055
|
+
const formal = formalRecords()
|
|
4056
|
+
return {
|
|
4057
|
+
s, V,
|
|
4058
|
+
trueV: closed('true'), falseV: closed('false'), undecidedV: closed('undecided'),
|
|
4059
|
+
openV: keys.filter((k) => V[k] && !V[k].closed),
|
|
4060
|
+
tasks, done: tasks.filter((t) => t.status === 'completed'), openT: tasks.filter((t) => t.status !== 'completed'),
|
|
4061
|
+
formal, formalKeys: Object.keys(formal),
|
|
4062
|
+
members: s.members || [],
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
function verdictCardRel(kind, id) { return 'Verified/' + (kind === 'subproblem' ? '问题' : kind === 'method' ? '方法' : '命题') + '/' + id + '.md' }
|
|
4066
|
+
function cardDirOf(kind) { return kind === 'method' ? 'Methods' : kind === 'subproblem' ? 'Subproblems' : 'Propos' }
|
|
4067
|
+
function paperTitle(p) {
|
|
4068
|
+
const st = String((inst().problem || {}).statement || '').trim()
|
|
4069
|
+
const head = st ? st.split(/\n/)[0].slice(0, 100) : ''
|
|
4070
|
+
return head || ('研究所成果论文|' + p.id)
|
|
4071
|
+
}
|
|
4072
|
+
function paperSources(p, active, ev) {
|
|
4073
|
+
const out = []
|
|
4074
|
+
out.push('Problems/conclusion.md(结题记录)')
|
|
4075
|
+
if ((inst().problem || {}).id) out.push('Problems/' + inst().problem.id + '.md(原问题卡)')
|
|
4076
|
+
out.push('State/' + instituteName + '.v5state.json(权威状态)')
|
|
4077
|
+
for (const k of ev.trueV.concat(ev.falseV)) out.push(verdictCardRel((ev.V[k] || {}).kind, k) + '(Verified 卡片)')
|
|
4078
|
+
for (const k of ev.trueV.concat(ev.falseV)) out.push('Shared/Debates/' + k + '.md(辩论录)')
|
|
4079
|
+
for (const k of ev.formalKeys) {
|
|
4080
|
+
const r = ev.formal[k] || {}
|
|
4081
|
+
if (r.file) out.push(r.file + '(Lean 工作文件)')
|
|
4082
|
+
if (r.proof) out.push(r.proof + '(归档证明)')
|
|
4083
|
+
}
|
|
4084
|
+
if (ev.formalKeys.length) out.push('Formal/Index.md', 'Formal/TODO.md')
|
|
4085
|
+
for (const m of ev.members) {
|
|
4086
|
+
out.push('Members/' + m.id + '/Progress/progress.md(' + m.id + ' 的研究日志)')
|
|
4087
|
+
out.push('Members/' + m.id + '/{Propos,Methods,Subproblems}/(' + m.id + ' 的卡片)')
|
|
4088
|
+
}
|
|
4089
|
+
out.push('Institutes.md(编制镜像)', 'Shared/TaskBoard.md(任务板镜像)')
|
|
4090
|
+
out.push('Paper/' + p.id + '/paper.log.md(本论文流程往来)')
|
|
4091
|
+
for (const id of active) {
|
|
4092
|
+
const part = (p.parts || {})[id] || {}
|
|
4093
|
+
for (const e of (part.evidence || [])) out.push(String(e) + '(' + id + ' 声称的证据)')
|
|
4094
|
+
}
|
|
4095
|
+
return Array.from(new Set(out))
|
|
4096
|
+
}
|
|
4097
|
+
function paperSections(p, active, skipped, ev, extra) {
|
|
4098
|
+
const o = extra || {}
|
|
4099
|
+
const fin = o.final || p.final || null
|
|
4100
|
+
const prob = ev.s.problem || {}
|
|
4101
|
+
const S = []
|
|
4102
|
+
// 1
|
|
4103
|
+
const b1 = []
|
|
4104
|
+
b1.push({ p: '标题:' + paperTitle(p) })
|
|
4105
|
+
b1.push({ ul: [
|
|
4106
|
+
'作者:' + ((p.participants || []).map((id) => id + '(' + ((memberById(id) || {}).kind === 'academician' ? '院士' : '常驻研究员') + ')').join('、') || '(无在册作者)'),
|
|
4107
|
+
'研究所:' + instituteName + '|项目:' + project + '|运行:' + (ev.s.runId || runId || '(无)'),
|
|
4108
|
+
'日期:' + fmtTime(p.finalizedAt || now()).slice(0, 10) + '|语言:' + p.lang + '|定稿代表:' + p.editor,
|
|
4109
|
+
] })
|
|
4110
|
+
const mainConclusions = ev.trueV.length
|
|
4111
|
+
? ('已由表决确立的结论:' + ev.trueV.join('、'))
|
|
4112
|
+
: '尚无经表决确立为真的结论(见第 4、8 节)。'
|
|
4113
|
+
b1.push({ p: '摘要:' + (prob.statement ? String(prob.statement).replace(/\s+/g, ' ').slice(0, 400) : '(未设定原问题)') + '。' + mainConclusions })
|
|
4114
|
+
if (fin && fin.note) b1.push({ p: '定稿说明:' + fin.note + (fin.consult ? '(定稿前与全所交流:消息 ' + fin.consult.messages + ' 条、会议 ' + fin.consult.meetings + ' 次)' : '') })
|
|
4115
|
+
if (fin && fin.conclusion) b1.push({ p: '定稿结论:' + fin.conclusion })
|
|
4116
|
+
S.push({ title: PAPER_SECTIONS[0], blocks: b1 })
|
|
4117
|
+
// 2
|
|
4118
|
+
S.push({ title: PAPER_SECTIONS[1], blocks: [
|
|
4119
|
+
{ p: prob.statement ? String(prob.statement) : '(本所未记录原问题的完整陈述——请勿在论文中补写。)' },
|
|
4120
|
+
{ p: '问题 ID:' + (prob.id || '(无)') + '|参与成员:' + (p.participants || []).join('、') },
|
|
4121
|
+
] })
|
|
4122
|
+
// 3
|
|
4123
|
+
const b3 = []
|
|
4124
|
+
const solvers = active.filter((id) => String(((p.parts || {})[id] || {}).solution || '').trim())
|
|
4125
|
+
if (solvers.length) {
|
|
4126
|
+
for (const id of solvers) {
|
|
4127
|
+
const part = (p.parts || {})[id] || {}
|
|
4128
|
+
b3.push({ h: id + '|' + String(part.title || '贡献') })
|
|
4129
|
+
b3.push({ p: String(part.solution) })
|
|
4130
|
+
}
|
|
4131
|
+
} else {
|
|
4132
|
+
b3.push({ p: '(参与成员未提交可引用的完整解法叙述;请以第 4 节的表决结论与证据索引为准。)' })
|
|
4133
|
+
}
|
|
4134
|
+
if (o.conclusionText) b3.push({ h: '结题记录(Problems/conclusion.md)' }, { p: String(o.conclusionText).slice(0, 4000) })
|
|
4135
|
+
S.push({ title: PAPER_SECTIONS[2], blocks: b3 })
|
|
4136
|
+
// 4
|
|
4137
|
+
const b4 = []
|
|
4138
|
+
if (ev.trueV.length) {
|
|
4139
|
+
b4.push({ p: '以下对象经 m 票布尔一致判定为**真**(证据:Verified 卡片 + 辩论录):' })
|
|
4140
|
+
b4.push({ ul: ev.trueV.map((k) => {
|
|
4141
|
+
const v = ev.V[k] || {}
|
|
4142
|
+
return '**' + k + '**(判为真;平均概率 ' + Number(v.mean || 0).toFixed(2) + ';m=' + (v.m || '?') + ';表决者 ' + ((v.voters || []).join('、') || '?') +
|
|
4143
|
+
';证据:' + verdictCardRel(v.kind, k) + ';辩论:Shared/Debates/' + k + '.md;提出者:' + (v.proposer || '(office)') + ')'
|
|
4144
|
+
}) })
|
|
4145
|
+
} else b4.push({ p: '(没有经表决判定为真的命题。)' })
|
|
4146
|
+
if (ev.falseV.length) {
|
|
4147
|
+
b4.push({ p: '**已被否证(显式标注,不得当作结论使用)**:' })
|
|
4148
|
+
b4.push({ ul: ev.falseV.map((k) => {
|
|
4149
|
+
const v = ev.V[k] || {}
|
|
4150
|
+
return k + '(判为假;平均概率 ' + Number(v.mean || 0).toFixed(2) + ';m=' + (v.m || '?') + ';证据:' + verdictCardRel(v.kind, k) + ')'
|
|
4151
|
+
}) })
|
|
4152
|
+
}
|
|
4153
|
+
S.push({ title: PAPER_SECTIONS[3], blocks: b4 })
|
|
4154
|
+
// 5
|
|
4155
|
+
const b5 = []
|
|
4156
|
+
const subV = ev.trueV.filter((k) => (ev.V[k] || {}).kind === 'subproblem')
|
|
4157
|
+
if (subV.length) b5.push({ ul: subV.map((k) => '子问题 ' + k + '(已判定为真;证据:' + verdictCardRel('subproblem', k) + ')') })
|
|
4158
|
+
if (ev.done.length) b5.push({ ul: ev.done.map((t) => '任务 ' + t.id + ':' + String(t.subject || '') + '(owner=' + (t.ownerId || '?') + ';镜像:Shared/TaskBoard.md)') })
|
|
4159
|
+
if (!subV.length && !ev.done.length) b5.push({ p: '(没有已完成的子问题或任务被记录。)' })
|
|
4160
|
+
S.push({ title: PAPER_SECTIONS[4], blocks: b5 })
|
|
4161
|
+
// 6
|
|
4162
|
+
const b6 = []
|
|
4163
|
+
const methodParts = active.filter((id) => String(((p.parts || {})[id] || {}).methods || '').trim())
|
|
4164
|
+
for (const id of methodParts) b6.push({ h: id + '|' + String(((p.parts || {})[id] || {}).title || '') }, { p: String(((p.parts || {})[id] || {}).methods) })
|
|
4165
|
+
if (!methodParts.length) b6.push({ p: '(参与成员未提交方法/理论/思想/经验/数学理解的叙述。)' })
|
|
4166
|
+
S.push({ title: PAPER_SECTIONS[5], blocks: b6 })
|
|
4167
|
+
// 7
|
|
4168
|
+
const b7 = []
|
|
4169
|
+
const ruleParts = active.filter((id) => String(((p.parts || {})[id] || {}).rules || '').trim())
|
|
4170
|
+
for (const id of ruleParts) b7.push({ h: id + '|' + String(((p.parts || {})[id] || {}).title || '') }, { p: String(((p.parts || {})[id] || {}).rules) })
|
|
4171
|
+
if (!ruleParts.length) b7.push({ p: '(参与成员未归纳出可复用规律。)' })
|
|
4172
|
+
S.push({ title: PAPER_SECTIONS[6], blocks: b7 })
|
|
4173
|
+
// 8
|
|
4174
|
+
const b8 = []
|
|
4175
|
+
const limitParts = active.filter((id) => String(((p.parts || {})[id] || {}).limits || '').trim())
|
|
4176
|
+
for (const id of limitParts) b8.push({ h: id }, { p: String(((p.parts || {})[id] || {}).limits) })
|
|
4177
|
+
const unresolved = []
|
|
4178
|
+
for (const k of ev.undecidedV) unresolved.push('未定论:' + k + '(平均概率 ' + Number((ev.V[k] || {}).mean || 0).toFixed(2) + ',m=' + ((ev.V[k] || {}).m || '?') + ')')
|
|
4179
|
+
for (const k of ev.openV) unresolved.push('尚未裁决:' + k)
|
|
4180
|
+
for (const t of ev.openT) unresolved.push('未完成任务:' + t.id + '|' + String(t.subject || '') + '(' + t.status + ')')
|
|
4181
|
+
for (const m of ev.members.filter((x) => x.phase === 'failed' || x.phase === 'dismissed')) unresolved.push('成员 ' + m.id + ':' + m.phase + (m.error ? '(' + String(m.error).slice(0, 120) + ')' : ''))
|
|
4182
|
+
for (const id of skipped) unresolved.push('未交稿/未参与:' + id)
|
|
4183
|
+
if (unresolved.length) b8.push({ p: '**未决与未参与(显式标注)**:' }, { ul: unresolved })
|
|
4184
|
+
if ((p.warnings || []).length) b8.push({ p: '流程警告:' }, { ul: (p.warnings || []).map(String) })
|
|
4185
|
+
if (!limitParts.length && !unresolved.length && !(p.warnings || []).length) b8.push({ p: '(无特别记录的局限。)' })
|
|
4186
|
+
S.push({ title: PAPER_SECTIONS[7], blocks: b8 })
|
|
4187
|
+
// 9
|
|
4188
|
+
const b9 = [{ p: '以下路径均相对于研究所根目录(与 State/<institute>.v5state.json 同级)。' }]
|
|
4189
|
+
b9.push({ ul: paperSources(p, active, ev) })
|
|
4190
|
+
if ((p.disagreement || []).length) {
|
|
4191
|
+
b9.push({ h: '分歧记录(未达成全体一致 / 定稿代表要求修订)' })
|
|
4192
|
+
b9.push({ ul: (p.disagreement || []).map((d) => '第 ' + d.round + ' 轮|' + (d.members || []).join('、') + ':' + String(d.detail || '')) })
|
|
4193
|
+
}
|
|
4194
|
+
if (skipped.length) b9.push({ p: '按“未参与”处理的成员:' + skipped.join('、') })
|
|
4195
|
+
S.push({ title: PAPER_SECTIONS[8], blocks: b9 })
|
|
4196
|
+
return S
|
|
4197
|
+
}
|
|
4198
|
+
function renderPaperMd(p, S) {
|
|
4199
|
+
const L = ['# ' + paperTitle(p), '']
|
|
4200
|
+
S.forEach((sec, i) => {
|
|
4201
|
+
L.push('## ' + (i + 1) + '. ' + sec.title)
|
|
4202
|
+
L.push('')
|
|
4203
|
+
for (const b of sec.blocks) {
|
|
4204
|
+
if (b.h) { L.push('### ' + b.h); L.push('') }
|
|
4205
|
+
else if (b.p !== undefined) { L.push(String(b.p)); L.push('') }
|
|
4206
|
+
else if (b.ul) { for (const it of b.ul) L.push('- ' + String(it)); L.push('') }
|
|
4207
|
+
}
|
|
4208
|
+
})
|
|
4209
|
+
return L.join('\n')
|
|
4210
|
+
}
|
|
4211
|
+
const TEX_ESCAPES = { '\\': '\\textbackslash{}', '&': '\\&', '%': '\\%', '$': '\\$', '#': '\\#', '_': '\\_', '{': '\\{', '}': '\\}', '~': '\\textasciitilde{}', '^': '\\textasciicircum{}' }
|
|
4212
|
+
function texEscape(s) { return String(s == null ? '' : s).replace(/[\\&%$#_{}~^]/g, (c) => TEX_ESCAPES[c]) }
|
|
4213
|
+
function renderPaperTex(p, S) {
|
|
4214
|
+
const zh = p.lang !== 'en'
|
|
4215
|
+
const L = [
|
|
4216
|
+
'\\documentclass[11pt]{' + (zh ? 'ctexart' : 'article') + '}',
|
|
4217
|
+
'\\usepackage{amsmath,amssymb,amsthm}',
|
|
4218
|
+
'\\usepackage[margin=2.5cm]{geometry}',
|
|
4219
|
+
'\\usepackage[hidelinks]{hyperref}',
|
|
4220
|
+
'\\usepackage{longtable,booktabs}',
|
|
4221
|
+
'\\title{' + texEscape(paperTitle(p)) + '}',
|
|
4222
|
+
'\\author{' + texEscape(((p.participants || []).join('、')) || (zh ? '研究所' : 'Institute')) + '}',
|
|
4223
|
+
'\\date{' + texEscape(fmtTime(p.finalizedAt || now()).slice(0, 10)) + '}',
|
|
4224
|
+
'\\begin{document}',
|
|
4225
|
+
'\\maketitle',
|
|
4226
|
+
]
|
|
4227
|
+
S.forEach((sec, i) => {
|
|
4228
|
+
L.push('\\section{' + texEscape((i + 1) + '. ' + sec.title) + '}')
|
|
4229
|
+
for (const b of sec.blocks) {
|
|
4230
|
+
if (b.h) L.push('\\subsection{' + texEscape(b.h) + '}')
|
|
4231
|
+
else if (b.p !== undefined) L.push(texEscape(b.p) + '\n')
|
|
4232
|
+
else if (b.ul) { L.push('\\begin{itemize}'); for (const it of b.ul) L.push('\\item ' + texEscape(it)); L.push('\\end{itemize}') }
|
|
4233
|
+
}
|
|
4234
|
+
})
|
|
4235
|
+
L.push('\\end{document}')
|
|
4236
|
+
return L.join('\n') + '\n'
|
|
4237
|
+
}
|
|
4238
|
+
// The repair pass keeps only the packages the text itself needs (docs/final-paper.md §8):
|
|
4239
|
+
// every optional or unknown package is dropped before the engine swap retry.
|
|
4240
|
+
function texStripOptionalPackages(tex) {
|
|
4241
|
+
return String(tex).split('\n').filter((line) => {
|
|
4242
|
+
if (/^\s*\\(?:input|include)\b/.test(line)) return false
|
|
4243
|
+
const m = /^\s*\\usepackage(?:\[[^\]]*\])?\{([^}]+)\}/.exec(line)
|
|
4244
|
+
if (!m) return true
|
|
4245
|
+
return m[1].split(',').map((x) => x.trim()).some((pk) => PAPER_REPAIR_CORE.indexOf(pk) !== -1)
|
|
4246
|
+
}).join('\n')
|
|
4247
|
+
}
|
|
4248
|
+
function texMinimal(tex, lang) {
|
|
4249
|
+
const body = String(tex).replace(/[\s\S]*?\\begin\{document\}/, '').replace(/\\end\{document\}[\s\S]*$/, '')
|
|
4250
|
+
return '\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}\n\\begin{document}\n' + body.trim() + '\n\\end{document}\n'
|
|
4251
|
+
}
|
|
4252
|
+
function paperEngineCandidates(lang) {
|
|
4253
|
+
const order = PAPER_ENGINE_ORDER[lang === 'en' ? 'en' : 'zh'].slice()
|
|
4254
|
+
const forced = String(params.paperLatexCommand || '').trim()
|
|
4255
|
+
if (!forced) return order
|
|
4256
|
+
return [forced].concat(order.filter((n) => n !== forced))
|
|
4257
|
+
}
|
|
4258
|
+
async function latexEngines(lang) {
|
|
4259
|
+
const sub = subprocessOf()
|
|
4260
|
+
if (!sub || typeof sub.resolveExecutable !== 'function') {
|
|
4261
|
+
return { engines: [], reason: '宿主没有 subprocess 服务:无法检测或调用 LaTeX' }
|
|
4262
|
+
}
|
|
4263
|
+
const engines = []
|
|
4264
|
+
for (const name of paperEngineCandidates(lang)) {
|
|
4265
|
+
try { const exe = await sub.resolveExecutable(name); if (exe) engines.push({ name: String(name), exe: String(exe) }) } catch (e) { /* not installed */ }
|
|
4266
|
+
}
|
|
4267
|
+
return { engines, reason: engines.length ? '' : ('未检测到 LaTeX 引擎(' + paperEngineCandidates(lang).join('/') + ')') }
|
|
4268
|
+
}
|
|
4269
|
+
// ONE engine invocation. NEVER throws: every failure becomes a readable result (the Lean
|
|
4270
|
+
// seam's discipline) and a timeout really terminates the process.
|
|
4271
|
+
async function runPaperProcess(argv, cwd) {
|
|
4272
|
+
const sub = subprocessOf()
|
|
4273
|
+
const started = now()
|
|
4274
|
+
if (!sub || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, ms: 0, message: 'no subprocess service' }
|
|
4275
|
+
let handle
|
|
4276
|
+
try {
|
|
4277
|
+
handle = sub.spawn({ argv, cwd, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: 120000 })
|
|
4278
|
+
} catch (e) { return { ok: false, exitCode: null, ms: now() - started, message: String((e && e.message) || e) } }
|
|
4279
|
+
let outcome = null
|
|
4280
|
+
let timer = null
|
|
4281
|
+
try {
|
|
4282
|
+
const ran = Promise.resolve(handle.done).then((v) => ({ settled: true, value: v }), (e) => ({ settled: false, error: e }))
|
|
4283
|
+
const r = await Promise.race([
|
|
4284
|
+
ran,
|
|
4285
|
+
new Promise((resolve) => {
|
|
4286
|
+
timer = ctx.timeout(() => {
|
|
4287
|
+
try { if (typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* the race result is the report */ }
|
|
4288
|
+
resolve({ settled: true, value: { exitCode: null, signal: 'SIGTERM' } })
|
|
4289
|
+
}, 120000)
|
|
4290
|
+
}),
|
|
4291
|
+
])
|
|
4292
|
+
if (!r.settled) return { ok: false, exitCode: null, ms: now() - started, message: String((r.error && r.error.message) || r.error) }
|
|
4293
|
+
outcome = r.value
|
|
4294
|
+
} finally { if (timer) { try { timer() } catch (e) { /* already settled */ } } }
|
|
4295
|
+
const exitCode = outcome ? outcome.exitCode : null
|
|
4296
|
+
return { ok: exitCode === 0, exitCode, ms: now() - started, message: '' }
|
|
4297
|
+
}
|
|
4298
|
+
// Compile attempts are CAPPED (docs/final-paper.md §8): full → nonstopmode rerun → engine swap with
|
|
4299
|
+
// the optional packages stripped → one minimal-template retry → report and degrade.
|
|
4300
|
+
async function compilePaperTex(id, tex, lang) {
|
|
4301
|
+
if (await fileExistsAbs(paperAbs(id, 'paper.pdf'))) {
|
|
4302
|
+
return { status: 'kept-existing', engine: '', reason: 'Paper/' + id + '/paper.pdf 已存在:不覆盖(docs/final-paper.md §5)', attempts: [] }
|
|
4303
|
+
}
|
|
4304
|
+
const det = await latexEngines(lang)
|
|
4305
|
+
if (!det.engines.length) return { status: 'not-detected', engine: '', reason: det.reason, attempts: [] }
|
|
4306
|
+
const e0 = det.engines[0], e1 = det.engines[1] || e0
|
|
4307
|
+
const plan = [
|
|
4308
|
+
{ label: 'full', engine: e0, text: tex, nonstopOnly: false },
|
|
4309
|
+
{ label: 'nonstopmode-rerun', engine: e0, text: tex, nonstopOnly: true },
|
|
4310
|
+
{ label: 'engine-swap+stripped', engine: e1, text: texStripOptionalPackages(tex), nonstopOnly: true },
|
|
4311
|
+
{ label: 'minimal-template', engine: e0, text: texMinimal(tex, lang), nonstopOnly: true },
|
|
4312
|
+
]
|
|
4313
|
+
const attempts = []
|
|
4314
|
+
for (const a of plan) {
|
|
4315
|
+
const r = await runLatexAttempt(id, a)
|
|
4316
|
+
attempts.push(r)
|
|
4317
|
+
if (r.ok) return { status: 'compiled', engine: a.engine.name, attempts }
|
|
4318
|
+
}
|
|
4319
|
+
return { status: 'failed', engine: '', reason: '编译在 ' + attempts.length + ' 次尝试后仍失败(已保留 paper.tex 与 paper.md)', attempts }
|
|
4320
|
+
}
|
|
4321
|
+
async function runLatexAttempt(id, a) {
|
|
4322
|
+
const dirRel = paperDirRel(id)
|
|
4323
|
+
const dirAbs = instRoot() + '/' + dirRel
|
|
4324
|
+
if (!await writeTextRel(dirRel + '/paper.tex', a.text)) {
|
|
4325
|
+
return { label: a.label, engine: a.engine.name, ok: false, exitCode: null, ms: 0, message: 'tex 写入失败' }
|
|
4326
|
+
}
|
|
4327
|
+
const started = now()
|
|
4328
|
+
const args = a.engine.name === 'tectonic'
|
|
4329
|
+
? ['-X', 'paper.tex']
|
|
4330
|
+
: (a.nonstopOnly ? ['-interaction=nonstopmode', 'paper.tex'] : ['-interaction=nonstopmode', '-halt-on-error', 'paper.tex'])
|
|
4331
|
+
const argv = [a.engine.exe].concat(args)
|
|
4332
|
+
// TWO passes (docs/final-paper.md §8): the second resolves references/TOC. A failed first pass is not
|
|
4333
|
+
// rerun — the retry PLAN is what varies the command, not a blind repeat.
|
|
4334
|
+
const first = await runPaperProcess(argv, dirAbs)
|
|
4335
|
+
if (!first.ok) return { label: a.label, engine: a.engine.name, ok: false, exitCode: first.exitCode, ms: now() - started, message: first.message || 'first pass failed' }
|
|
4336
|
+
const second = await runPaperProcess(argv, dirAbs)
|
|
4337
|
+
const pdf = await fileExistsAbs(paperAbs(id, 'paper.pdf'))
|
|
4338
|
+
return { label: a.label, engine: a.engine.name, ok: second.ok && pdf, exitCode: second.exitCode, ms: now() - started, message: pdf ? (second.message || '') : '编译器没有产出 paper.pdf' }
|
|
4339
|
+
}
|
|
4340
|
+
function paperMeta(p, extra) {
|
|
4341
|
+
const o = extra || {}
|
|
4342
|
+
const active = paperActiveParticipants(p)
|
|
4343
|
+
const skipped = (p.participants || []).filter((x) => active.indexOf(x) === -1)
|
|
4344
|
+
return {
|
|
4345
|
+
id: p.id,
|
|
4346
|
+
dir: 'Paper/' + p.id + '/',
|
|
4347
|
+
institute: instituteName, project, runId: inst().runId || runId || '',
|
|
4348
|
+
problemId: (inst().problem || {}).id || '',
|
|
4349
|
+
trigger: p.trigger,
|
|
4350
|
+
finalizedAt: o.finalizedAt || (p.finalizedAt || now()),
|
|
4351
|
+
language: p.lang, format: p.format, compilePdf: p.pdf === true,
|
|
4352
|
+
editor: p.editor, editorNote: p.editorNote || '', editorFallback: !!p.editorFallback,
|
|
4353
|
+
finalisation: p.final || null,
|
|
4354
|
+
consultation: Object.assign({ messages: 0, meetings: 0 }, p.consult || {}),
|
|
4355
|
+
participants: p.participants || [], skipped,
|
|
4356
|
+
rounds: p.round, maxRounds: PAPER_MAX_ROUNDS,
|
|
4357
|
+
parts: Object.keys(p.parts || {}).map((id) => ({ member: id, round: (p.parts[id] || {}).round, title: (p.parts[id] || {}).title || '', evidence: ((p.parts[id] || {}).evidence || []).length })),
|
|
4358
|
+
reviews: Object.keys(p.reviews || {}).map((id) => ({ member: id, of: (p.reviews[id] || {}).of, round: (p.reviews[id] || {}).round, deliverable: (p.reviews[id] || {}).deliverable === true })),
|
|
4359
|
+
disagreement: p.disagreement || [], warnings: o.warnings || p.warnings || [],
|
|
4360
|
+
forcedAfterCap: !!p.forcedAfterCap,
|
|
4361
|
+
sections: PAPER_SECTIONS.length,
|
|
4362
|
+
compile: o.compile || { status: p.compile || 'unknown', engine: p.engine || '', attempts: [] },
|
|
4363
|
+
params: {
|
|
4364
|
+
finalPaper: params.finalPaper, paperFormat: params.paperFormat, paperLanguage: params.paperLanguage,
|
|
4365
|
+
paperCompilePdf: params.paperCompilePdf, paperEditor: params.paperEditor, paperLatexCommand: params.paperLatexCommand,
|
|
4366
|
+
},
|
|
4367
|
+
files: o.files || [],
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
async function refillPaperArtifacts(p) {
|
|
4371
|
+
const id = p.id
|
|
4372
|
+
const active = paperActiveParticipants(p)
|
|
4373
|
+
const skipped = (p.participants || []).filter((x) => active.indexOf(x) === -1)
|
|
4374
|
+
const conclusionText = await readTextRel('Problems/conclusion.md')
|
|
4375
|
+
const S = paperSections(p, active, skipped, paperEvidence(), { final: p.final, conclusionText })
|
|
4376
|
+
const filled = []
|
|
4377
|
+
if (p.format !== 'tex' && (await readTextAbs(paperAbs(id, 'paper.md'))) === undefined) {
|
|
4378
|
+
if (await writeTextRel(PAPER_FILE(id, 'paper.md'), renderPaperMd(p, S))) filled.push('paper.md')
|
|
4379
|
+
}
|
|
4380
|
+
let tex = await readTextAbs(paperAbs(id, 'paper.tex'))
|
|
4381
|
+
if (p.format !== 'md' && tex === undefined) {
|
|
4382
|
+
tex = renderPaperTex(p, S)
|
|
4383
|
+
if (await writeTextRel(PAPER_FILE(id, 'paper.tex'), tex)) filled.push('paper.tex')
|
|
4384
|
+
}
|
|
4385
|
+
if (p.format !== 'md' && p.pdf === true && !await fileExistsAbs(paperAbs(id, 'paper.pdf'))) {
|
|
4386
|
+
const c = await compilePaperTex(id, tex || renderPaperTex(p, S), p.lang)
|
|
4387
|
+
if (c.status === 'compiled') filled.push('paper.pdf')
|
|
4388
|
+
// Same rule as finalizePaper: the delivered tex is always the canonical generated one.
|
|
4389
|
+
if (tex !== undefined && tex !== null) await writeTextRel(PAPER_FILE(id, 'paper.tex'), tex)
|
|
4390
|
+
}
|
|
4391
|
+
if ((await readTextAbs(paperAbs(id, 'paper.meta.json'))) === undefined) {
|
|
4392
|
+
if (await writeTextRel(PAPER_FILE(id, 'paper.meta.json'), JSON.stringify(paperMeta(p, { refilled: true }), null, 2) + '\n')) filled.push('paper.meta.json')
|
|
4393
|
+
}
|
|
4394
|
+
if ((await readTextAbs(paperAbs(id, 'paper.log.md'))) === undefined) {
|
|
4395
|
+
await paperLog('重复触发:补写缺失产物', '- 本次补写:' + (filled.join('、') || '(无缺失)'))
|
|
4396
|
+
filled.push('paper.log.md')
|
|
4397
|
+
} else if (filled.length) {
|
|
4398
|
+
await paperLog('重复触发:补写缺失产物', '- 本次补写:' + filled.join('、'))
|
|
4399
|
+
}
|
|
4400
|
+
return { filled }
|
|
4401
|
+
}
|
|
4402
|
+
async function finalizePaper(reason) {
|
|
4403
|
+
const p = paper()
|
|
4404
|
+
if (!p) return { ok: false, code: 'V5_PAPER_STATE', message: 'no paper flow' }
|
|
4405
|
+
const id = p.id
|
|
4406
|
+
const active = paperActiveParticipants(p)
|
|
4407
|
+
const skipped = (p.participants || []).filter((x) => active.indexOf(x) === -1)
|
|
4408
|
+
const ev = paperEvidence()
|
|
4409
|
+
const conclusionText = await readTextRel('Problems/conclusion.md')
|
|
4410
|
+
const finalizedAt = now()
|
|
4411
|
+
const S = paperSections(p, active, skipped, ev, { final: p.final, conclusionText })
|
|
4412
|
+
const files = []
|
|
4413
|
+
if (p.format !== 'tex') {
|
|
4414
|
+
if (await writeTextRel(PAPER_FILE(id, 'paper.md'), renderPaperMd(p, S))) files.push('paper.md')
|
|
4415
|
+
}
|
|
4416
|
+
let tex = ''
|
|
4417
|
+
if (p.format !== 'md') {
|
|
4418
|
+
tex = renderPaperTex(p, S)
|
|
4419
|
+
if (await writeTextRel(PAPER_FILE(id, 'paper.tex'), tex)) files.push('paper.tex')
|
|
4420
|
+
}
|
|
4421
|
+
// `paperFormat=md` with `paperCompilePdf=true` must NOT warn about a missing tex: no tex
|
|
4422
|
+
// was produced, so compilation is skipped silently (docs/final-paper.md §8).
|
|
4423
|
+
let compile
|
|
4424
|
+
if (p.format === 'md') compile = { status: 'skipped', engine: '', reason: 'paperFormat=md:未产出 tex,跳过编译', attempts: [] }
|
|
4425
|
+
else if (p.pdf !== true) compile = { status: 'skipped', engine: '', reason: 'paperCompilePdf=false', attempts: [] }
|
|
4426
|
+
else compile = await compilePaperTex(id, tex, p.lang)
|
|
4427
|
+
// The repair attempts overwrite `paper.tex` with stripped/minimal variants to get past a
|
|
4428
|
+
// broken engine. Whatever the compile outcome, the DELIVERED tex must be the real,
|
|
4429
|
+
// fully-generated one (the attempts are recorded in the meta either way).
|
|
4430
|
+
if (p.format !== 'md') await writeTextRel(PAPER_FILE(id, 'paper.tex'), tex)
|
|
4431
|
+
if (await fileExistsAbs(paperAbs(id, 'paper.pdf'))) files.push('paper.pdf')
|
|
4432
|
+
const warnings = (p.warnings || []).slice()
|
|
4433
|
+
if (compile.status === 'failed') warnings.push('LaTeX 编译失败(已保留 paper.tex 与 paper.md,不阻塞定稿):' + (compile.attempts || []).map((a) => a.engine + '/' + a.label + ' exit=' + a.exitCode + (a.message ? '(' + a.message + ')' : '')).join(';'))
|
|
4434
|
+
if (compile.status === 'not-detected') warnings.push('未检测到 LaTeX 引擎(' + (compile.reason || '') + '):只交付 paper.tex 与 paper.md。')
|
|
4435
|
+
const meta = paperMeta(Object.assign({}, p, { finalizedAt, compile: compile.status, engine: compile.engine || '' }), {
|
|
4436
|
+
finalizedAt, warnings, compile: { status: compile.status, engine: compile.engine || '', reason: compile.reason || '', attempts: compile.attempts || [] }, files,
|
|
4437
|
+
})
|
|
4438
|
+
if (await writeTextRel(PAPER_FILE(id, 'paper.meta.json'), JSON.stringify(meta, null, 2) + '\n')) files.push('paper.meta.json')
|
|
4439
|
+
await paperLog('定稿(' + reason + ')', [
|
|
4440
|
+
'- 目录:Paper/' + id + '/',
|
|
4441
|
+
'- 产物:' + files.join('、'),
|
|
4442
|
+
'- 编译:' + compile.status + (compile.engine ? '(' + compile.engine + ')' : '') + '|尝试 ' + (compile.attempts || []).length + ' 次',
|
|
4443
|
+
'- 定稿代表:' + p.editor + '|轮次:' + p.round + '/' + PAPER_MAX_ROUNDS,
|
|
4444
|
+
'- 所办交流:消息 ' + Number((p.consult || {}).messages || 0) + ' 条、会议 ' + Number((p.consult || {}).meetings || 0) + ' 次',
|
|
4445
|
+
(p.final && p.final.note ? '- 定稿说明:' + p.final.note : ''),
|
|
4446
|
+
(warnings.length ? '- 警告:\n - ' + warnings.join('\n - ') : ''),
|
|
4447
|
+
].filter(Boolean).join('\n'))
|
|
4448
|
+
await mutatePaper((cur) => (cur ? Object.assign({}, cur, {
|
|
4449
|
+
status: 'finalized', stage: 'finalized', finalizedAt, compile: compile.status,
|
|
4450
|
+
engine: compile.engine || '', warnings, artifacts: files, updatedAt: now(),
|
|
4451
|
+
}) : cur))
|
|
4452
|
+
await saveChatLine('【论文】最终论文已定稿:Paper/' + id + '/(' + (files.join('、') || '(无产物)') + ';编译 ' + compile.status + ')。')
|
|
4453
|
+
notifyActivity()
|
|
4454
|
+
// The run is only now complete (docs/final-paper.md §3): the paper phase ran BEFORE the flags.
|
|
4455
|
+
if (p.completesRun && !autoDone) await finishRun('全体有表决权者一致认为原问题已解决')
|
|
4456
|
+
return {
|
|
4457
|
+
ok: true, finalized: true, id, dir: 'Paper/' + id + '/', files,
|
|
4458
|
+
compile: compile.status, engine: compile.engine || '', attempts: (compile.attempts || []).length,
|
|
4459
|
+
warnings, agreement: !(p.disagreement || []).length, rounds: p.round,
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
|
|
3472
4463
|
// ---- hire / fire -------------------------------------------------------
|
|
3473
4464
|
function employedTemps() { return activeMembers().filter((m) => m.kind === 'temp') }
|
|
3474
4465
|
// ANY academician or permanently-employed researcher may hire its own temp
|
|
@@ -3673,6 +4664,16 @@ export function apply(ctx) {
|
|
|
3673
4664
|
await beginMeeting({ agenda: p.agenda, kind: p.kind, target: p.target, by: p.by })
|
|
3674
4665
|
return
|
|
3675
4666
|
}
|
|
4667
|
+
// The final-paper phase (docs/final-paper.md §3) owns the room once it is active: a paper ask is its
|
|
4668
|
+
// own kind of turn, and a research round must not be mistaken for a paper answer. While
|
|
4669
|
+
// the flow waits for the OFFICE the institute keeps working normally — the required
|
|
4670
|
+
// consultation needs office messages and a real meeting to be deliverable.
|
|
4671
|
+
if (paperActive()) {
|
|
4672
|
+
await paperPass()
|
|
4673
|
+
const cur = paper()
|
|
4674
|
+
const waitingForOffice = !!cur && cur.status === 'awaiting-editor' && cur.editor === 'office'
|
|
4675
|
+
if (!waitingForOffice) return
|
|
4676
|
+
}
|
|
3676
4677
|
const budget = Math.max(1, Math.floor(Number(params.maxParallel) || 3))
|
|
3677
4678
|
const idleMs = posMs(params.activityTimeoutMs, 120000)
|
|
3678
4679
|
let filled = 0
|
|
@@ -3723,7 +4724,7 @@ export function apply(ctx) {
|
|
|
3723
4724
|
const stallMs = posMs(params.stallAutoMeetingMs, 360000)
|
|
3724
4725
|
if (!meeting && !pendingMeeting && !hasVerifyInFlight() && phase === 'active' &&
|
|
3725
4726
|
busy.size === 0 && (now() - lastProgressAt) >= stallMs) {
|
|
3726
|
-
await startMeeting('office', { agenda: '本所较长时间没有新进展。请你们自行讨论:现在最该推进的是什么?谁来做?是否需要发起验证?', kind: 'sync' })
|
|
4727
|
+
await startMeeting('office', { agenda: '本所较长时间没有新进展。请你们自行讨论:现在最该推进的是什么?谁来做?是否需要发起验证?', kind: 'sync', auto: true })
|
|
3727
4728
|
return
|
|
3728
4729
|
}
|
|
3729
4730
|
// (e) heartbeat: push the longest-idle member to make progress rather than just
|
|
@@ -3897,6 +4898,12 @@ export function apply(ctx) {
|
|
|
3897
4898
|
}
|
|
3898
4899
|
// (10) solve votes / personal judgement
|
|
3899
4900
|
if (p.vote_solved !== undefined) await recordSolveVote(member.id, p.vote_solved === true)
|
|
4901
|
+
// (10b) final-paper channels: the member's own part, a cross-review, or the editor's
|
|
4902
|
+
// decision. Each is only honoured in the stage that asked for it (the record functions
|
|
4903
|
+
// refuse otherwise), so a stray/stale field can never advance the paper.
|
|
4904
|
+
if (p.paper_part && typeof p.paper_part === 'object') await paperRecordPart(member.id, p.paper_part)
|
|
4905
|
+
if (p.paper_review && typeof p.paper_review === 'object') await paperRecordReview(member.id, p.paper_review)
|
|
4906
|
+
if (p.paper_final && typeof p.paper_final === 'object') await paperRecordFinal(member.id, p.paper_final)
|
|
3900
4907
|
// (11) meeting input collection (keyed on the LIVE member set, so a member who
|
|
3901
4908
|
// joined mid-meeting still has to speak and a dismissed one stops blocking it)
|
|
3902
4909
|
if (kind === 'meeting' && meeting) {
|
|
@@ -3952,6 +4959,9 @@ export function apply(ctx) {
|
|
|
3952
4959
|
try { await maybeRealCompact(childId, member) } catch (e) { /* compaction is best-effort */ }
|
|
3953
4960
|
wakeKind.delete(member.id)
|
|
3954
4961
|
if (member.activeMeetingId) delete member.activeMeetingId
|
|
4962
|
+
// The final-paper phase is driven by member turns, exactly like a meeting: the reply was
|
|
4963
|
+
// just folded, so advance the paper BEFORE asking the (possibly inert) scheduler.
|
|
4964
|
+
try { await paperPass() } catch (e) { console.error('vibe-math-v5: paper after end: ' + String((e && e.message) || e)) }
|
|
3955
4965
|
await scheduleNext()
|
|
3956
4966
|
}
|
|
3957
4967
|
// ---- toolFilter names the host may not register -------------------------
|
|
@@ -4014,11 +5024,15 @@ export function apply(ctx) {
|
|
|
4014
5024
|
const ints = ['researcherCount', 'quorumCap', 'verdictMaxRounds', 'maxTempPerMember', 'maxTempTotal',
|
|
4015
5025
|
'compactThreshold', 'compactAfterRounds', 'maxParallel', 'activityTimeoutMs', 'stallAutoMeetingMs',
|
|
4016
5026
|
'chatDigestMs', 'chatDigestMax', 'meetingKeepEvery', 'leanTimeoutMs']
|
|
4017
|
-
const bools = ['academician', 'academicianLeads', 'memberMayRejectAssign']
|
|
4018
|
-
const strs = ['quorumMode', 'provider', 'model', 'staffPersona', 'formalVerify', 'leanCommand'
|
|
5027
|
+
const bools = ['academician', 'academicianLeads', 'memberMayRejectAssign', 'finalPaper', 'paperCompilePdf']
|
|
5028
|
+
const strs = ['quorumMode', 'provider', 'model', 'staffPersona', 'formalVerify', 'leanCommand',
|
|
5029
|
+
'paperFormat', 'paperLanguage', 'paperEditor', 'paperLatexCommand']
|
|
4019
5030
|
const arrs = ['toolAllow', 'toolDeny', 'tempToolAllow', 'tempToolDeny', 'leanArgs']
|
|
4020
5031
|
for (const k of ints) if (input[k] !== undefined) { const n = Math.floor(Number(input[k])); if (Number.isFinite(n)) out[k] = n }
|
|
4021
|
-
|
|
5032
|
+
// Explicit boolean coercion (docs/final-paper.md §2): the old `=== true || === 'true'` turned a
|
|
5033
|
+
// legitimate `1` / `'yes'` into FALSE and left junk values truthy-looking. An
|
|
5034
|
+
// unrecognised spelling is REJECTED (the default wins) rather than guessed.
|
|
5035
|
+
for (const k of bools) if (input[k] !== undefined) out[k] = coerceBool(input[k], false)
|
|
4022
5036
|
for (const k of strs) if (input[k] !== undefined) out[k] = String(input[k])
|
|
4023
5037
|
for (const k of arrs) {
|
|
4024
5038
|
if (input[k] === undefined) continue
|
|
@@ -4032,6 +5046,12 @@ export function apply(ctx) {
|
|
|
4032
5046
|
out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'
|
|
4033
5047
|
}
|
|
4034
5048
|
if (out.leanCommand !== undefined && !String(out.leanCommand).trim()) out.leanCommand = 'lean'
|
|
5049
|
+
// ── final-paper enums (closed sets; an unknown value degrades to the documented
|
|
5050
|
+
// default instead of silently becoming an unreachable fourth mode) ────────────────
|
|
5051
|
+
if (out.paperFormat !== undefined) out.paperFormat = coercePaperEnum('paperFormat', out.paperFormat, ['both', 'md', 'tex'], 'both')
|
|
5052
|
+
if (out.paperLanguage !== undefined) out.paperLanguage = coercePaperEnum('paperLanguage', out.paperLanguage, ['zh', 'en'], 'zh')
|
|
5053
|
+
if (out.paperEditor !== undefined) out.paperEditor = coercePaperEnum('paperEditor', out.paperEditor, ['office', 'academician'], 'academician')
|
|
5054
|
+
if (out.paperLatexCommand !== undefined) out.paperLatexCommand = String(out.paperLatexCommand).trim()
|
|
4035
5055
|
// Guard every duration against a negative/NaN value: such a value would make a
|
|
4036
5056
|
// watchdog fire instantly and abandon all consensus (v4 §30-T41).
|
|
4037
5057
|
for (const k of ['activityTimeoutMs', 'stallAutoMeetingMs', 'chatDigestMs', 'leanTimeoutMs']) {
|
|
@@ -4076,6 +5096,10 @@ export function apply(ctx) {
|
|
|
4076
5096
|
chatDigestMax: params.chatDigestMax, meetingKeepEvery: params.meetingKeepEvery,
|
|
4077
5097
|
formalVerify: params.formalVerify, leanCommand: params.leanCommand,
|
|
4078
5098
|
leanArgs: params.leanArgs, leanTimeoutMs: params.leanTimeoutMs,
|
|
5099
|
+
// ── final paper ──────────────────────────────────────────────────────
|
|
5100
|
+
finalPaper: params.finalPaper, paperFormat: params.paperFormat,
|
|
5101
|
+
paperLanguage: params.paperLanguage, paperCompilePdf: params.paperCompilePdf,
|
|
5102
|
+
paperEditor: params.paperEditor, paperLatexCommand: params.paperLatexCommand,
|
|
4079
5103
|
provider: params.provider, model: params.model,
|
|
4080
5104
|
toolAllow: params.toolAllow, toolDeny: params.toolDeny,
|
|
4081
5105
|
}
|
|
@@ -4318,6 +5342,9 @@ export function apply(ctx) {
|
|
|
4318
5342
|
await saveChatLine('【恢复】研究所继续推进(重建成员 ' + respawned + ' 名)。')
|
|
4319
5343
|
notifyActivity()
|
|
4320
5344
|
await scheduleNext()
|
|
5345
|
+
// An interrupted FINAL PAPER resumes its own stage (the paper phase runs before the
|
|
5346
|
+
// completion flags, so a crash there leaves a live, resumable institute).
|
|
5347
|
+
if (paperActive()) { try { await paperPass() } catch (e) { console.error('vibe-math-v5: paper after resume: ' + String((e && e.message) || e)) } }
|
|
4321
5348
|
return { ok: true, resumed: true, members: members.map((m) => m.id), respawned, running: true }
|
|
4322
5349
|
}
|
|
4323
5350
|
function setPause() {
|
|
@@ -4394,6 +5421,10 @@ export function apply(ctx) {
|
|
|
4394
5421
|
blocked: Object.keys(formalRecords()).filter((k) => (formalRecords()[k] || {}).status === 'blocked'),
|
|
4395
5422
|
todo: formalTodo(),
|
|
4396
5423
|
},
|
|
5424
|
+
// The final-paper flow: stage, who wrote/reviewed what, the office-consultation
|
|
5425
|
+
// counters and the compile result. Without this an operator cannot tell whether the
|
|
5426
|
+
// run is waiting for the office, for a member, or for nobody.
|
|
5427
|
+
paper: paperSummary(),
|
|
4397
5428
|
lastProgressAt, params: visibleParams(),
|
|
4398
5429
|
}
|
|
4399
5430
|
}
|
|
@@ -4460,6 +5491,21 @@ export function apply(ctx) {
|
|
|
4460
5491
|
L.push('- 可复用库:VibeMath/Formal/{Lib,Proved}/(跨项目)|本所形式化:Formal/|归档证明:Verified/Lean/')
|
|
4461
5492
|
}
|
|
4462
5493
|
L.push('')
|
|
5494
|
+
L.push('## 最终论文')
|
|
5495
|
+
{
|
|
5496
|
+
const pp = paper()
|
|
5497
|
+
if (!pp) L.push('- 尚未开始(' + (params.finalPaper === false ? '自动已关闭(finalPaper=false),可用 /v5 paper 手动生成' : '收口时会自动开始;也可用 /v5 paper 手动开始') + ')')
|
|
5498
|
+
else {
|
|
5499
|
+
L.push('- 目录:Paper/' + pp.id + '/|状态:' + pp.status + '|阶段:' + pp.stage + '|轮次:' + pp.round + '/' + PAPER_MAX_ROUNDS)
|
|
5500
|
+
L.push('- 定稿代表:' + pp.editor + (pp.editorFallback ? '(原定代表不在册,已回退)' : '') + '|语言:' + pp.lang + '|格式:' + pp.format)
|
|
5501
|
+
L.push('- 交稿:' + (Object.keys(pp.parts || {}).join('、') || '(无)') + '|互审:' + (Object.keys(pp.reviews || {}).map((k) => k + '→' + ((pp.reviews[k] || {}).of || '?') + (pp.reviews[k] && pp.reviews[k].deliverable === true ? '(可交付)' : '(需修改)')).join('、') || '(无)'))
|
|
5502
|
+
L.push('- 所办交流:消息 ' + Number((pp.consult || {}).messages || 0) + ' 条|会议 ' + Number((pp.consult || {}).meetings || 0) + ' 次')
|
|
5503
|
+
if (pp.compile) L.push('- 编译:' + pp.compile + (pp.engine ? '(' + pp.engine + ')' : ''))
|
|
5504
|
+
if ((pp.warnings || []).length) L.push('- 警告:' + pp.warnings.join(';'))
|
|
5505
|
+
L.push('- 下一步:' + paperNextStep(pp))
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
L.push('')
|
|
4463
5509
|
L.push('## 文件位置')
|
|
4464
5510
|
L.push('- 根目录:' + instRoot())
|
|
4465
5511
|
L.push('- 已确立:Verified/|成员库:Members/<id>/|群聊:Shared/Chat/|会议:Shared/Meetings/|辩论:Shared/Debates/')
|
|
@@ -4526,6 +5572,8 @@ export function apply(ctx) {
|
|
|
4526
5572
|
leanRunToolApi: async (relPath, timeoutMs) => await leanRunFile(relPath, timeoutMs),
|
|
4527
5573
|
// consensus / meetings
|
|
4528
5574
|
maybeQueueVerify, castVerdict, currentVerify, hasVerifyInFlight, startMeeting, quorumM, voterCount,
|
|
5575
|
+
// final paper (docs/final-paper.md; the phase runs BEFORE finishRun)
|
|
5576
|
+
startPaper, paperStatus: paperSummary, finalizePaperByOffice,
|
|
4529
5577
|
// authorization helpers (used by tool handlers)
|
|
4530
5578
|
memberIdOfAgent, isOffice, isAcademician, isProvablyOffice, officeCaller, memberById, activeMembers,
|
|
4531
5579
|
}
|
|
@@ -4599,7 +5647,7 @@ export function apply(ctx) {
|
|
|
4599
5647
|
registerTool('vibe_v5_stop', 'Stop the institute: interrupt every member, clear coordination state, and release their child sessions.', objParams({}), (s, a, x) => withOffice(s, x, 'stop the institute', () => s.initStop()))
|
|
4600
5648
|
registerTool('vibe_v5_status', 'Machine-readable institute status (members, tasks, quorum, meetings, verification, mail).', objParams({}), (s) => s.status())
|
|
4601
5649
|
registerTool('vibe_v5_report', 'Human-readable institute report (staffing, tasks, consensus, meetings, file locations).', objParams({}), (s) => s.report())
|
|
4602
|
-
registerTool('vibe_v5_set', 'Tune institute parameters (persisted in State/<institute>.v5state.json). provider/model override staff LLM routes (empty = inherit the office route). toolAllow/toolDeny restrict PERMANENT staff tools; tempToolAllow/tempToolDeny restrict temp workers. quorumCap sets m = min(quorumCap, voters); an m-vote passes only when at least m Boolean votes (exactly 1 or exactly 0) exist AND no voter returns an opposing Boolean, so with the default roster it degenerates to unanimity among the current voters; only current voters count (a dismissed member\'s earlier ballot is dropped). quorumMode "m-unanimous" (v5) or "all-unanimous" (v4 legacy). formalVerify: "off" (default, no extra requirement) | "encourage" (agents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record).', objParams({
|
|
5650
|
+
registerTool('vibe_v5_set', 'Tune institute parameters (persisted in State/<institute>.v5state.json). provider/model override staff LLM routes (empty = inherit the office route). toolAllow/toolDeny restrict PERMANENT staff tools; tempToolAllow/tempToolDeny restrict temp workers. quorumCap sets m = min(quorumCap, voters); an m-vote passes only when at least m Boolean votes (exactly 1 or exactly 0) exist AND no voter returns an opposing Boolean, so with the default roster it degenerates to unanimity among the current voters; only current voters count (a dismissed member\'s earlier ballot is dropped). quorumMode "m-unanimous" (v5) or "all-unanimous" (v4 legacy). formalVerify: "off" (default, no extra requirement) | "encourage" (agents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record). FINAL PAPER: finalPaper (default true) writes the final paper when the run concludes — the paper phase runs BEFORE the run is marked complete, the permanent staff write their own part, cross-review each other, and the editor named by paperEditor finalises; paperFormat "both"|"md"|"tex"; paperLanguage "zh"|"en"; paperCompilePdf compiles a PDF when a LaTeX engine is detected; paperEditor "academician" (default, the only editor an unattended run can reach) | "office" (manual /v5 paper only — the office must first consult the whole institute: >=1 office message AND >=1 meeting, recorded in the finalisation note); paperLatexCommand forces one engine command instead of auto-detection (empty = auto: xelatex -> latexmk -> pdflatex -> lualatex -> tectonic, English prefers pdflatex). Unknown spellings of these enums fall back to the documented default.', objParams({
|
|
4603
5651
|
academician: B, academicianLeads: B, memberMayRejectAssign: B, researcherCount: I,
|
|
4604
5652
|
quorumCap: I, quorumMode: S, verdictMaxRounds: I,
|
|
4605
5653
|
maxTempPerMember: I, maxTempTotal: I,
|
|
@@ -4607,6 +5655,9 @@ export function apply(ctx) {
|
|
|
4607
5655
|
activityTimeoutMs: I, stallAutoMeetingMs: I, chatDigestMs: I, chatDigestMax: I, meetingKeepEvery: I,
|
|
4608
5656
|
formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] },
|
|
4609
5657
|
leanCommand: S, leanArgs: SA, leanTimeoutMs: I,
|
|
5658
|
+
finalPaper: B, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] },
|
|
5659
|
+
paperLanguage: { type: 'string', enum: ['zh', 'en'] },
|
|
5660
|
+
paperCompilePdf: B, paperEditor: { type: 'string', enum: ['office', 'academician'] }, paperLatexCommand: S,
|
|
4610
5661
|
provider: S, model: S, staffPersona: S, toolAllow: SA, toolDeny: SA, tempToolAllow: SA, tempToolDeny: SA,
|
|
4611
5662
|
}), (s, a, x) => withOffice(s, x, 'tune institute parameters', () => s.setParams(a)))
|
|
4612
5663
|
registerTool('vibe_v5_message', 'Relay a message from the office/human into the institute (to a member id, to "all", or to "voters").', objParams({ to: S, content: S }, ['to', 'content']), (s, a, x) => {
|
|
@@ -4651,6 +5702,16 @@ export function apply(ctx) {
|
|
|
4651
5702
|
? s.removeResearcher(a.id)
|
|
4652
5703
|
: { ok: false, code: 'V5_NOT_OFFICE', message: 'only the office (the session root) may dismiss a permanent researcher' }
|
|
4653
5704
|
)))
|
|
5705
|
+
// ── final paper (docs/final-paper.md) ─────────────────────────────────────
|
|
5706
|
+
registerTool('vibe_v5_paper', 'Final paper: start (or re-run) the team-authored paper for this run. The permanent staff write their own part, cross-review another member\'s part, and the editor named by paperEditor finalises. lang/format override paperLanguage/paperFormat for THIS paper; editor overrides paperEditor for THIS paper only (office = the manual path, which parks the flow until the office consults the institute and calls vibe_v5_finalize_paper); force rewrites an already-finalised paper (idempotent otherwise: it only fills artifacts that are missing).', objParams({ lang: S, format: S, editor: { type: 'string', enum: ['office', 'academician'] }, force: B, reason: S }), (s, a, x) => withOffice(s, x, 'write the final paper', () => {
|
|
5707
|
+
const lang = a.lang === undefined ? undefined : String(a.lang)
|
|
5708
|
+
const format = a.format === undefined ? undefined : String(a.format)
|
|
5709
|
+
if (lang !== undefined && lang !== 'zh' && lang !== 'en') return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'lang must be zh|en' }
|
|
5710
|
+
if (format !== undefined && ['both', 'md', 'tex'].indexOf(format) === -1) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'format must be both|md|tex' }
|
|
5711
|
+
if (a.editor !== undefined && ['office', 'academician'].indexOf(String(a.editor)) === -1) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'editor must be office|academician' }
|
|
5712
|
+
return s.startPaper(a.reason === 'run-complete' ? 'run-complete' : 'manual', { lang, format, editor: a.editor, force: a.force === true, reason: a.reason })
|
|
5713
|
+
}))
|
|
5714
|
+
registerTool('vibe_v5_finalize_paper', 'Office: finalise the final paper after consulting the whole institute. Required when paperEditor="office": at least one office message (vibe_v5_message) AND at least one meeting convened by the office must happen first (paper.meta.json records them, and the note must state the conclusion). decision="revise" asks for another writing round (bounded).', objParams({ decision: { type: 'string', enum: ['deliverable', 'revise'] }, note: S, conclusion: S, force: B }, ['decision']), (s, a, x) => withOffice(s, x, 'finalise the final paper', () => s.finalizePaperByOffice(a)))
|
|
4654
5715
|
|
|
4655
5716
|
// ── member-facing controls ────────────────────────────────────────────────
|
|
4656
5717
|
registerTool('vibe_v5_say', '(member) Speak in the group chat (omit "to"), send a private message ("to":"r-2"), or address only the voters ("to":"voters").', objParams({ text: S, to: S }, ['text']), (s, a, x) => {
|
|
@@ -4748,7 +5809,7 @@ export function apply(ctx) {
|
|
|
4748
5809
|
// ── /v5 slash command ────────────────────────────────────────────────────
|
|
4749
5810
|
ctx.effect(() => commands.register({
|
|
4750
5811
|
name: 'v5', description: 'control the Vibe Math V5 research institute',
|
|
4751
|
-
input: { hint: '[configure|start|resume|pause|stop|status|report|members|message|meeting|hire|fire|add|remove|set]' },
|
|
5812
|
+
input: { hint: '[configure|start|resume|pause|stop|status|report|members|message|meeting|hire|fire|add|remove|set|paper [lang=en] [format=tex] [editor=office] [force]]' },
|
|
4752
5813
|
handler: async function (inv) {
|
|
4753
5814
|
const s = getSession(inv && inv.agent)
|
|
4754
5815
|
if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no session' }) }
|
|
@@ -4784,7 +5845,28 @@ export function apply(ctx) {
|
|
|
4784
5845
|
else if (cmd === 'fire') r = await s.fire('office', { id: rest[0] || '', reason: rest.slice(1).join(' ') })
|
|
4785
5846
|
else if (cmd === 'add') r = await s.addResearcher('office', rest.join(' '))
|
|
4786
5847
|
else if (cmd === 'remove') r = await s.removeResearcher(rest[0] || '')
|
|
4787
|
-
else if (cmd === '
|
|
5848
|
+
else if (cmd === 'paper') {
|
|
5849
|
+
// `/v5 paper [lang=en] [format=tex] [force] [editor=office|academician]`
|
|
5850
|
+
const o = {}
|
|
5851
|
+
let force = false
|
|
5852
|
+
let bad = ''
|
|
5853
|
+
for (const tok of rest) {
|
|
5854
|
+
if (tok === 'force') { force = true; continue }
|
|
5855
|
+
const eq = tok.indexOf('=')
|
|
5856
|
+
if (eq <= 0) { bad = 'unknown paper argument: ' + tok; break }
|
|
5857
|
+
const k = tok.slice(0, eq), v = tok.slice(eq + 1)
|
|
5858
|
+
if (k === 'lang' || k === 'format' || k === 'editor') o[k] = v
|
|
5859
|
+
else { bad = 'unknown paper option: ' + k + ' (use lang=zh|en format=both|md|tex editor=office|academician force)'; break }
|
|
5860
|
+
}
|
|
5861
|
+
if (!bad && o.lang !== undefined && o.lang !== 'zh' && o.lang !== 'en') bad = 'lang must be zh|en'
|
|
5862
|
+
if (!bad && o.format !== undefined && ['both', 'md', 'tex'].indexOf(o.format) === -1) bad = 'format must be both|md|tex'
|
|
5863
|
+
if (!bad && o.editor !== undefined && ['office', 'academician'].indexOf(o.editor) === -1) bad = 'editor must be office|academician'
|
|
5864
|
+
if (bad) return { kind: 'error', text: JSON.stringify({ ok: false, code: 'V5_INVALID_ARGUMENT', message: bad }, null, 2) }
|
|
5865
|
+
// `editor=office` is the manual path that uses the office as the finalising
|
|
5866
|
+
// representative (docs/final-paper.md §7 — never reachable from an automatic run). One-shot: it
|
|
5867
|
+
// does not rewrite the persisted `paperEditor`.
|
|
5868
|
+
r = await s.startPaper('manual', { lang: o.lang, format: o.format, editor: o.editor, force })
|
|
5869
|
+
} else if (cmd === 'set') {
|
|
4788
5870
|
const upd = {}
|
|
4789
5871
|
for (const tok of rest) {
|
|
4790
5872
|
const eq = tok.indexOf('=')
|
|
@@ -4794,7 +5876,7 @@ export function apply(ctx) {
|
|
|
4794
5876
|
upd[k] = Number.isFinite(n) && v !== '' ? n : (v === 'true' ? true : v === 'false' ? false : v)
|
|
4795
5877
|
}
|
|
4796
5878
|
r = await s.setParams(upd)
|
|
4797
|
-
} else r = { ok: false, usage: 'configure|start|resume|pause|stop|status|report|members|message|meeting|hire|fire|add|remove|set' }
|
|
5879
|
+
} else r = { ok: false, usage: 'configure|start|resume|pause|stop|status|report|members|message|meeting|hire|fire|add|remove|set|paper [lang=zh|en] [format=both|md|tex] [editor=office|academician] [force]' }
|
|
4798
5880
|
// A business failure (the dispatch result's own ok:false) is a FAILED command: the host's
|
|
4799
5881
|
// CommandResult union distinguishes success from error, and returning 'success' made a rejected
|
|
4800
5882
|
// invocation look identical to a successful one (same fix as v2/v3).
|
|
@@ -4889,6 +5971,32 @@ function slugify(s) {
|
|
|
4889
5971
|
return t || ''
|
|
4890
5972
|
}
|
|
4891
5973
|
|
|
5974
|
+
// Explicit parameter coercion (docs/final-paper.md §2). A boolean parameter spelled `'false'`, `0`, `'no'`
|
|
5975
|
+
// or `'off'` must be false — the old `=== true || === 'true'` silently treated `1`/`'yes'` as
|
|
5976
|
+
// false. An unrecognised spelling is REJECTED (the caller's default wins), never guessed.
|
|
5977
|
+
function coerceBool(v, def) {
|
|
5978
|
+
if (v === undefined) return def
|
|
5979
|
+
if (v === true || v === 1 || v === '1') return true
|
|
5980
|
+
if (v === false || v === 0 || v === '0') return false
|
|
5981
|
+
const t = String(v).trim().toLowerCase()
|
|
5982
|
+
if (['true', 'yes', 'on', 'y', 't'].indexOf(t) !== -1) return true
|
|
5983
|
+
if (['false', 'no', 'off', 'n', 'f', ''].indexOf(t) !== -1) return false
|
|
5984
|
+
return def
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
// Enum coercion with legacy aliases (.v5 / older flat spellings) and a documented fallback.
|
|
5988
|
+
const ENUM_ALIASES = {
|
|
5989
|
+
paperFormat: { markdown: 'md', latex: 'tex', texonly: 'tex', all: 'both' },
|
|
5990
|
+
paperLanguage: { cn: 'zh', chinese: 'zh', 'zh-cn': 'zh', english: 'en', 'en-us': 'en' },
|
|
5991
|
+
paperEditor: { root: 'office', host: 'office', acad: 'academician', dean: 'academician' },
|
|
5992
|
+
}
|
|
5993
|
+
function coercePaperEnum(key, v, allowed, def) {
|
|
5994
|
+
const t = String(v == null ? '' : v).trim().toLowerCase()
|
|
5995
|
+
if (allowed.indexOf(t) !== -1) return t
|
|
5996
|
+
const alias = (ENUM_ALIASES[key] || {})[t]
|
|
5997
|
+
return alias !== undefined && allowed.indexOf(alias) !== -1 ? alias : def
|
|
5998
|
+
}
|
|
5999
|
+
|
|
4892
6000
|
function tryJson(s) { try { return JSON.parse(s) } catch (e) { return undefined } }
|
|
4893
6001
|
|
|
4894
6002
|
function parseReply(text) {
|