dsh-vibe-math 2.7.1 → 2.7.2
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 +681 -690
- package/README.md +8 -17
- package/cordis.patch.yml +6 -6
- package/docs/AUDIT-CHECKLIST.md +518 -518
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +588 -588
- package/docs/math-computation.md +187 -163
- package/docs/release-notes/README.en.md +48 -0
- package/docs/release-notes/README.md +47 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
- package/docs/release-notes/TEMPLATE.md +120 -0
- package/docs/test-timing.md +154 -154
- package/package.json +208 -203
- package/prompt-corpus-persona/persona-corpus.json +8 -8
- package/prompt-corpus-persona/persona-corpus.md +12 -12
- package/prompt-corpus-v2/formal-verify-v2.json +121 -109
- package/prompt-corpus-v2/formal-verify-v2.md +223 -107
- package/prompt-corpus-v3/formal-verify-v3.json +100 -76
- package/prompt-corpus-v3/formal-verify-v3.md +268 -70
- package/prompt-corpus-v4/formal-verify-v4.json +10 -10
- package/prompt-corpus-v4/formal-verify-v4.md +20 -20
- package/prompt-corpus-v5/prompt-corpus-v5.json +81 -81
- package/prompt-corpus-v5/prompt-corpus-v5.md +138 -128
- package/tests/audit-math-computation-contract.mjs +43 -1
- package/tests/audit-math-computation-sensitivity.mjs +2 -2
- package/tests/audit-v5-sensitivity.mjs +3 -1
- package/tests/e2e-v5-round2.test.mjs +114 -5
- package/tests/formal-verify-v2.test.mjs +38 -0
- package/tests/formal-verify-v3.test.mjs +33 -0
- package/tests/formal-verify-v4.test.mjs +87 -0
- package/tests/math-computation-shared.test.mjs +126 -1
- package/vibe-math-v2/agent.cordis.yml +2 -2
- package/vibe-math-v2/math-computation.js +122 -11
- package/vibe-math-v2/vibe-math-v2.js +75 -11
- package/vibe-math-v3/agent.cordis.yml +2 -2
- package/vibe-math-v3/math-computation.js +122 -11
- package/vibe-math-v3/vibe-math-v3.js +60 -13
- package/vibe-math-v4/agent.cordis.yml +4 -4
- package/vibe-math-v4/math-computation.js +122 -11
- package/vibe-math-v4/vibe-math-v4.js +55 -15
- package/vibe-math-v5/agent.cordis.yml +4 -4
- package/vibe-math-v5/math-computation.js +122 -11
- package/vibe-math-v5/vibe-math-v5.js +137 -28
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1660 -1655
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -72,11 +72,12 @@ export const MATH_PERSONA_TOOL_LINE = '- math_computation {op: probe|run|receipt
|
|
|
72
72
|
// P2a: the archive -> edit -> re-run workflow. Injected into every preset's rule block (and appended
|
|
73
73
|
// to both persona blocks) so an agent cannot miss that (a) the script is archived, (b) it may edit
|
|
74
74
|
// it, and (c) re-running is what produces evidence for the edited code.
|
|
75
|
-
export const MATH_ARCHIVE_WORKFLOW_LINE = '- 归档→编辑→重跑:mode:\'code\' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext
|
|
75
|
+
export const MATH_ARCHIVE_WORKFLOW_LINE = '- 归档→编辑→重跑:mode:\'code\' 的脚本原件在回执的 scriptPath(Computation/<id>/script.<ext>,**相对项目根**);'
|
|
76
|
+
+ '**成员的文件工具是按会话 cwd 解析的**,所以打开它要用**绝对路径** `receipt.scriptAbs`,或把 `receipt.cwd` 与 `receipt.scriptPath` 拼起来(回执两个字段都有);'
|
|
76
77
|
+ '编辑后用 mode:\'file\' 指向它重跑,会写出一份**新回执/新 attempt**(含新的 scriptHash)。**旧回执对修改后的代码无效**——'
|
|
77
78
|
+ '报告里必须引用与当前代码哈希一致的那份回执;工具会在 scriptChanged / scriptChangedDuringRun 为 true 时显式告警。'
|
|
78
79
|
|
|
79
|
-
export const MATH_ARCHIVE_WORKFLOW_LINE_EN = '- Archive -> edit -> re-run: for mode:\'code\' the script original is at the receipt\'s scriptPath (Computation/<id>/script.<ext
|
|
80
|
+
export const MATH_ARCHIVE_WORKFLOW_LINE_EN = '- Archive -> edit -> re-run: for mode:\'code\' the script original is at the receipt\'s scriptPath (Computation/<id>/script.<ext>, **relative to the project root**); member file tools resolve paths against the SESSION CWD, so open it via the ABSOLUTE `receipt.scriptAbs`, or join `receipt.cwd` with `receipt.scriptPath` (both are in the receipt); after editing, re-run it with mode:\'file\' to write a NEW receipt/attempt with a NEW scriptHash. **An old receipt is NOT evidence for edited code** - cite the receipt whose scriptHash matches the current code; the tool warns explicitly via scriptChanged / scriptChangedDuringRun.'
|
|
80
81
|
|
|
81
82
|
// Round-6 (A): honesty about SUBSTITUTIONS. An alternative that weakens exactness or conclusion
|
|
82
83
|
// strength must be declared, and the conclusion must never read as if the requested (exact) result
|
|
@@ -288,14 +289,39 @@ function fail(code, engine, message, extra) {
|
|
|
288
289
|
// round-7 (live-session fix): the probe diagnostic must survive into the response, otherwise
|
|
289
290
|
// "存在但不可用" is opaque (a real session showed exactly that).
|
|
290
291
|
if (extra && extra.probe) out.probe = jsonSafeDiag(extra.probe)
|
|
292
|
+
// round-9 (F7): the version-constraint policy travels with failures too (no silent drop).
|
|
293
|
+
if (extra && extra.versionPolicy) out.versionPolicy = extra.versionPolicy
|
|
294
|
+
if (extra && extra.constraintsNotEnforced) out.constraintsNotEnforced = extra.constraintsNotEnforced
|
|
291
295
|
return out
|
|
292
296
|
}
|
|
293
297
|
|
|
294
|
-
|
|
298
|
+
// round-9 (real-engine finding): the suggested install command may not be runnable on this machine
|
|
299
|
+
// (e.g. `winget` is absent), and handing a user a command that cannot run is misleading. So the
|
|
300
|
+
// command is emitted ONLY when its package manager is actually resolvable; otherwise we return the
|
|
301
|
+
// official vendor/download pointer plus an explicit note. Async because resolution goes through the host.
|
|
302
|
+
async function userInstallNext(H, engine) {
|
|
295
303
|
const d = MATH_ENGINES[engine] || {}
|
|
296
304
|
const per = d.userInstall || {}
|
|
297
305
|
const platform = process && process.platform === 'win32' ? 'windows' : (process && process.platform === 'darwin' ? 'macos' : 'linux')
|
|
298
|
-
|
|
306
|
+
const raw = per[platform] || ''
|
|
307
|
+
const mgr = (function () { const m = /^\s*(winget|brew|apt-get|apt|choco|scoop|dnf|pacman|zypper)\b/.exec(String(raw)); return m ? m[1] : null })()
|
|
308
|
+
let available = true
|
|
309
|
+
if (mgr) {
|
|
310
|
+
try { available = !!(await H.resolveExecutable(mgr)) } catch (e) { available = false }
|
|
311
|
+
}
|
|
312
|
+
return next('user-install', {
|
|
313
|
+
engine: engine,
|
|
314
|
+
perOs: per,
|
|
315
|
+
command: (mgr && !available) ? '' : raw,
|
|
316
|
+
suggestedCommand: raw,
|
|
317
|
+
platform: platform,
|
|
318
|
+
packageManager: mgr,
|
|
319
|
+
packageManagerAvailable: mgr ? available : null,
|
|
320
|
+
vendorUrl: d.vendor || null,
|
|
321
|
+
note: (mgr && !available)
|
|
322
|
+
? ('本机没有检测到包管理器 ' + mgr + ':建议命令无法运行,请按上面的官方地址手动安装(或先自行安装一个包管理器)。')
|
|
323
|
+
: null,
|
|
324
|
+
})
|
|
299
325
|
}
|
|
300
326
|
|
|
301
327
|
// ── host adaptation ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -353,6 +379,42 @@ export async function probeMathEngines(host, opts) {
|
|
|
353
379
|
return result
|
|
354
380
|
}
|
|
355
381
|
|
|
382
|
+
// round-9 (real-engine finding): an engine can be INSTALLED yet invisible, because discovery only
|
|
383
|
+
// looked at the server process's PATH (R 4.6.1 landed in C:\Program Files\R\R-4.6.1\bin and was NOT
|
|
384
|
+
// added to PATH). Add the known per-OS INSTALL locations, globbed by version, still AFTER PATH and
|
|
385
|
+
// after the bundled runtime; existence is always proven by LISTING, never assumed.
|
|
386
|
+
async function knownInstallCandidates(H, engineName) {
|
|
387
|
+
if (typeof H.listDirAbs !== 'function') return []
|
|
388
|
+
const cands = mathEngineCandidates(engineName)
|
|
389
|
+
if (!cands) return []
|
|
390
|
+
const env = (typeof process !== 'undefined' && process.env) || {}
|
|
391
|
+
const win = !!(typeof process !== 'undefined' && process.platform === 'win32')
|
|
392
|
+
const exts = win ? ['', '.exe', '.cmd'] : ['']
|
|
393
|
+
const out = []
|
|
394
|
+
const listDirs = async (dir) => { try { return ((await H.listDirAbs(dir)) || []).filter((e) => e && e.type === 'directory').map((e) => e.name) } catch (e) { return [] } }
|
|
395
|
+
const listFiles = async (dir) => { try { return ((await H.listDirAbs(dir)) || []).filter((e) => e && e.type === 'file').map((e) => e.name) } catch (e) { return [] } }
|
|
396
|
+
const match = async (dir) => {
|
|
397
|
+
const files = await listFiles(dir)
|
|
398
|
+
for (const c of cands) for (const x of exts) if (files.indexOf(c + x) !== -1) out.push(dir + '/' + c + x)
|
|
399
|
+
}
|
|
400
|
+
if (engineName === 'r') {
|
|
401
|
+
const roots = win
|
|
402
|
+
? [String(env.ProgramFiles || 'C:/Program Files') + '/R', String(env['ProgramFiles(x86)'] || 'C:/Program Files (x86)') + '/R', env.LOCALAPPDATA ? String(env.LOCALAPPDATA) + '/Programs/R' : null]
|
|
403
|
+
: ['/usr/lib/R', '/Library/Frameworks/R.framework/Resources', '/usr/local/lib/R', '/opt/R', '/usr/lib64/R']
|
|
404
|
+
for (const root of roots) {
|
|
405
|
+
if (!root) continue
|
|
406
|
+
for (const d of await listDirs(root)) await match(root + '/' + d + '/bin') // windows versioned dirs
|
|
407
|
+
await match(root + '/bin') // unix / framework layout
|
|
408
|
+
}
|
|
409
|
+
} else if (engineName === 'python' && win) {
|
|
410
|
+
const local = env.LOCALAPPDATA ? String(env.LOCALAPPDATA) + '/Programs/Python' : null
|
|
411
|
+
for (const d of (local ? await listDirs(local) : [])) await match(local + '/' + d)
|
|
412
|
+
const pf = String(env.ProgramFiles || 'C:/Program Files')
|
|
413
|
+
for (const d of await listDirs(pf)) if (/^Python[0-9]/i.test(d)) await match(pf + '/' + d)
|
|
414
|
+
}
|
|
415
|
+
return Array.from(new Set(out))
|
|
416
|
+
}
|
|
417
|
+
|
|
356
418
|
async function resolveCandidate(H, d) {
|
|
357
419
|
const cands = mathEngineCandidates(d.name)
|
|
358
420
|
if (!cands) return null
|
|
@@ -362,11 +424,15 @@ async function resolveCandidate(H, d) {
|
|
|
362
424
|
if (typeof p === 'string' && p) return p
|
|
363
425
|
} catch (e) { /* not this one */ }
|
|
364
426
|
}
|
|
365
|
-
// round-7 (fix 2): DSH ships its own runtimes; PATH always wins, so this is
|
|
427
|
+
// round-7 (fix 2): DSH ships its own runtimes; PATH always wins, so this is a LAST resort. The
|
|
366
428
|
// tree name is globbed (never hard-coded) and executable names come from the descriptor candidates.
|
|
367
429
|
for (const p of await dshRuntimeCandidates(H, d.name)) {
|
|
368
430
|
if (typeof p === 'string' && p) return p
|
|
369
431
|
}
|
|
432
|
+
// round-9: then the known per-OS install locations (an engine installed without touching PATH).
|
|
433
|
+
for (const p of await knownInstallCandidates(H, d.name)) {
|
|
434
|
+
if (typeof p === 'string' && p) return p
|
|
435
|
+
}
|
|
370
436
|
return null
|
|
371
437
|
}
|
|
372
438
|
|
|
@@ -513,7 +579,7 @@ async function resolveEngine(H, requested, params, args) {
|
|
|
513
579
|
if (params.mathMode !== 'typed+shell') return fail('MATH_REFUSED', name, 'cli 被策略禁用(mathMode=' + params.mathMode + ')', { next: next('reason', { reason: 'policy' }) })
|
|
514
580
|
let exe = null
|
|
515
581
|
try { exe = await H.resolveExecutable(args.cli.command) } catch (e) { exe = null }
|
|
516
|
-
if (!exe) return fail('MATH_ENGINE_NOT_FOUND', name, 'cli 命令无法解析:' + args.cli.command, { next: userInstallNext('cli') })
|
|
582
|
+
if (!exe) return fail('MATH_ENGINE_NOT_FOUND', name, 'cli 命令无法解析:' + args.cli.command, { next: await userInstallNext(H, 'cli') })
|
|
517
583
|
// round-7 (finding 5): report the REAL version when the command's family is recognisable (the
|
|
518
584
|
// descriptor for that family carries the version probe); unknown commands stay 'unknown'.
|
|
519
585
|
let cliVersion = 'unknown'
|
|
@@ -548,7 +614,7 @@ async function resolveEngine(H, requested, params, args) {
|
|
|
548
614
|
return { ok: true, name: name, desc: d, exe: hit, version: v.version }
|
|
549
615
|
}
|
|
550
616
|
const first = requested === 'auto' ? (params.mathEngines[0] || 'python') : requested
|
|
551
|
-
return fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(试过:' + names.join(', ') + ')', { next: userInstallNext(first) })
|
|
617
|
+
return fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(试过:' + names.join(', ') + ')', { next: await userInstallNext(H, first) })
|
|
552
618
|
}
|
|
553
619
|
|
|
554
620
|
// ── package probe ───────────────────────────────────────────────────────────────────────────────
|
|
@@ -810,7 +876,7 @@ async function opProbe(H, args, params) {
|
|
|
810
876
|
}
|
|
811
877
|
if (!probe.available && !(chosen && chosen.ok)) {
|
|
812
878
|
const first = requested || params.mathEngines[0] || 'python'
|
|
813
|
-
const out = fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(请求:' + first + ')', { next: userInstallNext(first), engines: [], available: false, packages: packages })
|
|
879
|
+
const out = fail('MATH_ENGINE_NOT_FOUND', first, '本机没有可用的计算引擎(请求:' + first + ')', { next: await userInstallNext(H, first), engines: [], available: false, packages: packages })
|
|
814
880
|
out.op = 'probe'
|
|
815
881
|
return out
|
|
816
882
|
}
|
|
@@ -821,10 +887,22 @@ async function opProbe(H, args, params) {
|
|
|
821
887
|
engines.push({ name: chosen.name, path: chosen.exe, version: chosen.version, license: (MATH_ENGINES[chosen.name] || {}).license })
|
|
822
888
|
}
|
|
823
889
|
const engineInfo = (chosen && chosen.ok) ? { name: chosen.name, path: chosen.exe, version: chosen.version } : null
|
|
890
|
+
// round-9 (F3): the availability line must never be silently partial - name the CONFIGURED engines
|
|
891
|
+
// that were not found (and why), instead of just listing the ones that happened to resolve.
|
|
892
|
+
const foundNames = engines.map((e) => e.name)
|
|
893
|
+
// Per-engine absence explanation. The key is `why` (NOT the reserved machine-readable vocabulary
|
|
894
|
+
// key), so the parity guard's exhaustive `next.reason` set stays exactly the refusal vocabulary.
|
|
895
|
+
const absent = params.mathEngines.filter((e) => foundNames.indexOf(e) === -1).map((e) => {
|
|
896
|
+
if (MATH_ENGINES[e] && MATH_ENGINES[e].resolveFrom === 'cli.command') return { engine: e, why: 'needs-a-caller-supplied-command' }
|
|
897
|
+
return { engine: e, why: 'not-found-on-this-machine' }
|
|
898
|
+
})
|
|
824
899
|
return {
|
|
825
900
|
ok: true, op: 'probe', engine: engineInfo ? engineInfo.name : null, engineInfo: engineInfo,
|
|
826
901
|
engines: engines, available: true, packages: packages,
|
|
827
|
-
|
|
902
|
+
configured: params.mathEngines.slice(), absent: absent,
|
|
903
|
+
message: '可用引擎:' + engines.map((e) => e.name + ' ' + e.version).join('、')
|
|
904
|
+
+ (engineInfo ? '(请求 ' + engineInfo.name + ':' + engineInfo.version + ')' : '')
|
|
905
|
+
+ (absent.length ? ';已配置但本机未发现:' + absent.map((a) => a.engine).join('、') : ';已配置的引擎都在'),
|
|
828
906
|
}
|
|
829
907
|
}
|
|
830
908
|
|
|
@@ -851,6 +929,9 @@ async function opRun(H, args, params) {
|
|
|
851
929
|
const engineLabel = det.name === 'cli' ? ('cli:' + cliCommand) : det.name
|
|
852
930
|
|
|
853
931
|
const want = (args.packages && args.packages.length) ? args.packages : params.mathPackages
|
|
932
|
+
// round-9 (F7): version constraints are NOT enforced - we only check existence by base name.
|
|
933
|
+
const pinnedSpecs = want.filter((sp) => parsePackageSpec(sp).pinned)
|
|
934
|
+
const versionPolicy = 'version-constraints-are-existence-only: 只按 base name 检查是否存在,约束本身从不校验(版本求解交给包管理器)' + (pinnedSpecs.length ? ';本次未校验的约束:' + pinnedSpecs.join(', ') : ';本次没有版本约束')
|
|
854
935
|
// round-7 (finding 3): for cli the precheck runs against the FAMILY of `cli.command` (same
|
|
855
936
|
// discovery-shaped logic); an unrecognisable command SKIPS the precheck instead of reporting a
|
|
856
937
|
// false "missing" that would block the escape hatch.
|
|
@@ -865,6 +946,7 @@ async function opRun(H, args, params) {
|
|
|
865
946
|
missing: missing,
|
|
866
947
|
next: next('agent-install', { engine: det.name, packages: missing, specs: want.slice(), dryRun: true }),
|
|
867
948
|
packages: pk,
|
|
949
|
+
versionPolicy: versionPolicy, constraintsNotEnforced: pinnedSpecs.slice(),
|
|
868
950
|
})
|
|
869
951
|
}
|
|
870
952
|
|
|
@@ -872,10 +954,17 @@ async function opRun(H, args, params) {
|
|
|
872
954
|
let scriptText = ''
|
|
873
955
|
let fileRel = null
|
|
874
956
|
let sourceHashBefore = null
|
|
957
|
+
// round-9 (N1): `mode:'file'` is keyed by the SOURCE PATH, so re-running an ORIGINAL source lands on
|
|
958
|
+
// the same archive id (attempt>=2, scriptChanged). Pointing it at an ARCHIVED script instead targets
|
|
959
|
+
// a different path and therefore a NEW archive id by design - the prior attempt is never touched, but
|
|
960
|
+
// there is no previous receipt FOR THAT ID, so scriptChanged/previousReceipt are necessarily empty.
|
|
961
|
+
// That is not a bug, but it must be SAID rather than looking like broken change detection.
|
|
962
|
+
let archivedScriptSource = false
|
|
875
963
|
if (mode === 'code') scriptText = args.code
|
|
876
964
|
else if (mode === 'file') {
|
|
877
965
|
fileRel = projectRel('', args.file)
|
|
878
966
|
if (fileRel === null) return fail('MATH_REFUSED', det.name, 'file 必须位于项目内:' + args.file, { next: next('reason', { reason: 'path-outside-project' }) })
|
|
967
|
+
archivedScriptSource = /^Computation\//.test(String(fileRel))
|
|
879
968
|
const txt = await H.readText(fileRel)
|
|
880
969
|
if (txt === undefined) return fail('MATH_REFUSED', det.name, '找不到文件:' + args.file, { next: next('reason', { reason: 'file-not-found' }) })
|
|
881
970
|
scriptText = txt
|
|
@@ -939,6 +1028,7 @@ async function opRun(H, args, params) {
|
|
|
939
1028
|
}
|
|
940
1029
|
|
|
941
1030
|
const warnings = []
|
|
1031
|
+
if (archivedScriptSource) warnings.push(warning('ARCHIVED_SCRIPT_RERUN', 'mode:\'file\' 指向的是**归档脚本**(Computation/…):按设计归档 id 以**源路径**为键,所以这是一次**新归档**(新目录、attempt 1),没有该 id 的历史回执 ⇒ scriptChanged/previousReceipt 为空是**预期**,且**旧 attempt 绝不会被覆盖**。要做"编辑→重跑→attempt≥2 + scriptChanged"的核对,请把 mode:\'file\' 指向**原始源文件**并编辑它,而不是指向归档副本。'))
|
|
942
1032
|
if (!probeDet && want.length) warnings.push(warning('PACKAGE_PRECHECK_SKIPPED', 'engine=cli:无法从命令名判断引擎族,已跳过包预检(不阻塞执行,也不会误报缺包);如需预检请直接用 python/r/octave/julia 或改用可识别的解释器路径。'))
|
|
943
1033
|
if (pk && pk.probeFailed && want.length) warnings.push(warning('PACKAGE_PRECHECK_UNKNOWN', '包预检未能执行(宿主 subprocess 在这几次尝试里返回空/超时,会话早期常见):本次不判定缺包、不阻塞执行;如需确定性结论请重试或直接用对应引擎。probe=' + JSON.stringify(pk.argv) + ' attempts=' + JSON.stringify(pk.attempts)))
|
|
944
1034
|
if (fullOut.length > MATH_CAPS.stdout) warnings.push(warning('OUTPUT_TRUNCATED', 'stdout 超过 64KB,完整版见 ' + dir + '/stdout.txt'))
|
|
@@ -953,7 +1043,15 @@ async function opRun(H, args, params) {
|
|
|
953
1043
|
engine: { name: engineLabel, path: det.exe, version: det.version, source: det.name === 'cli' ? 'cli' : 'path' },
|
|
954
1044
|
script: { path: scriptRel, sha256: scriptHash, bytes: Buffer.byteLength(scriptText, 'utf8') },
|
|
955
1045
|
scriptPath: scriptRel, scriptHash: scriptHash,
|
|
1046
|
+
// round-8 (P0/D3): member file tools are session-cwd relative, so the ABSOLUTE locations must be
|
|
1047
|
+
// in the receipt: `scriptAbs` is directly openable, `cwd` is the project root to join with.
|
|
1048
|
+
scriptAbs: scriptAbs, cwd: root,
|
|
956
1049
|
sourceFile: fileRel || null,
|
|
1050
|
+
fileIsArchivedScript: archivedScriptSource,
|
|
1051
|
+
// round-9 (N2): a TIMEOUT must still carry the evidence it produced - the capped partial output
|
|
1052
|
+
// lives in the receipt as well as in stdout.txt/stderr.txt on disk.
|
|
1053
|
+
partialStdout: String(fullOut).slice(-2000),
|
|
1054
|
+
partialStderr: String(fullErr).slice(-2000),
|
|
957
1055
|
sourceHashBefore: sourceHashBefore, sourceHashAfter: sourceHashAfter,
|
|
958
1056
|
scriptChanged: scriptChanged, scriptChangedDuringRun: scriptChangedDuringRun,
|
|
959
1057
|
previousReceipt: prevReceipt ? { runId: prevReceipt.runId, attempt: prevReceipt.attempt || 1, scriptHash: prevReceipt.scriptHash || (prevReceipt.script && prevReceipt.script.sha256) || null } : null,
|
|
@@ -979,12 +1077,20 @@ async function opRun(H, args, params) {
|
|
|
979
1077
|
|
|
980
1078
|
const shell = {
|
|
981
1079
|
ok: true, op: 'run', engine: engineLabel, mode: mode,
|
|
1080
|
+
runId: runId, attempt: attempt,
|
|
1081
|
+
versionPolicy: versionPolicy, constraintsNotEnforced: pinnedSpecs.slice(),
|
|
982
1082
|
engineInfo: receipt.engine,
|
|
983
1083
|
argv: assembled.argv.slice(),
|
|
984
1084
|
scriptPath: scriptRel, scriptHash: scriptHash,
|
|
1085
|
+
scriptAbs: scriptAbs, cwd: root,
|
|
985
1086
|
attempt: attempt, attemptDir: dir, baseRunDir: baseDir,
|
|
986
1087
|
scriptChanged: scriptChanged, scriptChangedDuringRun: scriptChangedDuringRun,
|
|
987
1088
|
sourceFile: fileRel || null,
|
|
1089
|
+
fileIsArchivedScript: archivedScriptSource,
|
|
1090
|
+
// round-9 (N2): a TIMEOUT must still carry the evidence it produced - the capped partial output
|
|
1091
|
+
// lives in the receipt as well as in stdout.txt/stderr.txt on disk.
|
|
1092
|
+
partialStdout: String(fullOut).slice(-2000),
|
|
1093
|
+
partialStderr: String(fullErr).slice(-2000),
|
|
988
1094
|
previousReceipt: receipt.previousReceipt,
|
|
989
1095
|
cliScriptAppended: !!(receipt.cli && receipt.cli.scriptAppended),
|
|
990
1096
|
packages: pk.found,
|
|
@@ -994,8 +1100,13 @@ async function opRun(H, args, params) {
|
|
|
994
1100
|
warnings: warnings,
|
|
995
1101
|
}
|
|
996
1102
|
if (receipt.timedOut) {
|
|
997
|
-
|
|
998
|
-
|
|
1103
|
+
// round-9 (F5): failure EVIDENCE must be uniform - a timeout carries the SAME fields a non-zero
|
|
1104
|
+
// exit does (exit + stderr), plus timedOut:true.
|
|
1105
|
+
const out = fail('MATH_TIMEOUT', engineLabel, '执行超时(' + timeoutMs + 'ms)已被终止', { argv: assembled.argv.slice(), receipt: receiptRef })
|
|
1106
|
+
out.exit = null
|
|
1107
|
+
out.stdout = String(fullOut).slice(0, MATH_CAPS.stdout)
|
|
1108
|
+
out.stderr = String(fullErr).slice(0, MATH_CAPS.stderr)
|
|
1109
|
+
return Object.assign(out, archiveFields(), { ms: receipt.ms, timedOut: true })
|
|
999
1110
|
}
|
|
1000
1111
|
if (r.exit !== 0) {
|
|
1001
1112
|
const blob = fullErr + '\n' + fullOut
|
|
@@ -769,6 +769,14 @@ export function apply(ctx) {
|
|
|
769
769
|
const vibeRoot = () => (workspaceRoot() + '/VibeMath').replace(/\\/g, '/')
|
|
770
770
|
const projectRoot = () => vibeRoot() + '/Projects/' + project
|
|
771
771
|
const instRoot = () => projectRoot() + '/Institutes/' + instituteName
|
|
772
|
+
// DEFECT A (3rd self-test): member-facing text used INSTITUTE-relative paths
|
|
773
|
+
// (`Members/<id>/…`), but a member's OWN file tools resolve a relative path against the
|
|
774
|
+
// SESSION CWD — so a member following that text verbatim created `<cwd>/Members/…`, a stray
|
|
775
|
+
// tree outside `VibeMath/Projects/…` (the self-test's `Members/acad/Propos/p-*.md`). Every
|
|
776
|
+
// member-facing path now uses the CWD-relative form (their real baseline); the framework's own
|
|
777
|
+
// writes keep going through `instRoot()`. `instRel()` is the one place that composes it.
|
|
778
|
+
const instRootRel = () => 'VibeMath/Projects/' + project + '/Institutes/' + instituteName
|
|
779
|
+
const instRel = (rel) => instRootRel() + '/' + String(rel == null ? '' : rel).replace(/^\/+/, '')
|
|
772
780
|
|
|
773
781
|
function getPolicy() {
|
|
774
782
|
const sp = sandboxPolicyOf()
|
|
@@ -924,9 +932,95 @@ export function apply(ctx) {
|
|
|
924
932
|
const memberById = (id) => inst().members.find((m) => m.id === id)
|
|
925
933
|
const voters = () => activeMembers().filter((m) => m.kind === 'academician' || m.kind === 'researcher')
|
|
926
934
|
const voterCount = () => voters().length
|
|
927
|
-
function
|
|
935
|
+
function quorumMFrom(vc) {
|
|
928
936
|
const cap = Math.max(1, Math.floor(Number(params.quorumCap) || 3))
|
|
929
|
-
return Math.max(1, Math.min(cap,
|
|
937
|
+
return Math.max(1, Math.min(cap, vc))
|
|
938
|
+
}
|
|
939
|
+
function quorumM() { return quorumMFrom(voterCount()) }
|
|
940
|
+
// DEFECT B (3rd self-test): the office's digest showed `m=2` while `status`/`acad` showed
|
|
941
|
+
// `m=3` in the SAME window — two views had computed the voter set at different moments
|
|
942
|
+
// (roster-snapshot timing). Every quorum-bearing view now reads ONE live snapshot and carries
|
|
943
|
+
// its `rosterVersion`, so all views at one point in time are provably identical and a stale
|
|
944
|
+
// reader can be DETECTED instead of silently disagreeing.
|
|
945
|
+
function rosterVersion() {
|
|
946
|
+
const ids = voters().map((m) => String(m.id) + ':' + String(m.phase)).sort()
|
|
947
|
+
return 'r' + String((inst().members || []).length) + ':' + ids.join(',')
|
|
948
|
+
}
|
|
949
|
+
function rosterSnapshot() {
|
|
950
|
+
const vs = voters()
|
|
951
|
+
const vc = vs.length
|
|
952
|
+
const started = vc > 0
|
|
953
|
+
return {
|
|
954
|
+
version: rosterVersion(),
|
|
955
|
+
m: started ? quorumMFrom(vc) : 0,
|
|
956
|
+
voters: vs.map((m) => m.id),
|
|
957
|
+
voterCount: vc,
|
|
958
|
+
started: started,
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// DEFECT 2 (v5 self-test §18): before `start` there are no voters, yet the reported quorum
|
|
962
|
+
// was `m=1` with `voters=0` — it read like "a quorum of 1 is already there". The REPORTED
|
|
963
|
+
// quorum is therefore coherent and explicit: `started:false`, `m:0`, `voterCount:0` and the
|
|
964
|
+
// next step. `quorumM()` itself stays >=1 so the consensus arithmetic is unchanged, and
|
|
965
|
+
// `judgeVerdict` refuses to conclude at all when there are no voters.
|
|
966
|
+
function quorumView() {
|
|
967
|
+
const snap = rosterSnapshot()
|
|
968
|
+
const started = snap.started
|
|
969
|
+
const view = {
|
|
970
|
+
m: snap.m,
|
|
971
|
+
mode: params.quorumMode,
|
|
972
|
+
voters: snap.voters,
|
|
973
|
+
voterCount: snap.voterCount,
|
|
974
|
+
started: started,
|
|
975
|
+
phase: started ? 'voting' : 'not-started',
|
|
976
|
+
rosterVersion: snap.version,
|
|
977
|
+
}
|
|
978
|
+
if (!started) {
|
|
979
|
+
view.next = {
|
|
980
|
+
kind: 'start', tool: 'vibe_v5_start',
|
|
981
|
+
hint: '研究所尚未启动:有表决权者 0 人,因此 m 记为 0(不是"已满足 1 票门槛")。先 vibe_v5_start(或 /v5 start)建立名册,m 才有意义。',
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return view
|
|
985
|
+
}
|
|
986
|
+
// DEFECT 1 (v5 self-test §18): a member-scoped tool called before the institute exists — or
|
|
987
|
+
// by nobody identifiable — answered a BARE `V5_MEMBER_NOT_FOUND`, so an operator (or the
|
|
988
|
+
// model) could not tell WHY it failed or WHAT TO DO FIRST. The error CODE is unchanged (all
|
|
989
|
+
// existing gates keep matching on it); the answer now names the state and the next step, in
|
|
990
|
+
// the same `next`-hint shape the `math_computation` tool uses.
|
|
991
|
+
function memberDiagnosis(what, memberId) {
|
|
992
|
+
const total = (inst().members || []).length
|
|
993
|
+
const active = activeMembers().length
|
|
994
|
+
const who = String(memberId || '')
|
|
995
|
+
if (active === 0 && !running) {
|
|
996
|
+
return {
|
|
997
|
+
ok: false, code: 'V5_MEMBER_NOT_FOUND',
|
|
998
|
+
message: what + '失败:研究所尚未启动(在册 ' + total + ' 名、有表决权者 0 人),没有可归属的成员身份。',
|
|
999
|
+
next: {
|
|
1000
|
+
kind: 'start', tool: 'vibe_v5_start',
|
|
1001
|
+
hint: '先启动研究所(vibe_v5_start 或 /v5 start)让框架派出常驻成员,再由**成员子代理**调用该工具;所办(会话根)本身不是成员。',
|
|
1002
|
+
},
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (active === 0) {
|
|
1006
|
+
return {
|
|
1007
|
+
ok: false, code: 'V5_MEMBER_NOT_FOUND',
|
|
1008
|
+
message: what + '失败:研究所当前没有在册(active)成员。',
|
|
1009
|
+
next: { kind: 'staff', tool: 'vibe_v5_add_researcher', hint: '先用 vibe_v5_add_researcher / vibe_v5_hire 补充成员,再调用该工具。' },
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (!who) {
|
|
1013
|
+
return {
|
|
1014
|
+
ok: false, code: 'V5_MEMBER_NOT_FOUND',
|
|
1015
|
+
message: what + '失败:无法识别调用者(这是成员工具,所办/会话根没有成员身份)。',
|
|
1016
|
+
next: { kind: 'member-call', tool: 'vibe_v5_members', hint: '请由成员子代理调用;所办可用 vibe_v5_message 转达,或直接用所办工具(vibe_v5_report / vibe_v5_status)。' },
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
ok: false, code: 'V5_MEMBER_NOT_FOUND',
|
|
1021
|
+
message: what + '失败:名册中没有成员 "' + who + '"(可能已被解雇或尚未就位)。',
|
|
1022
|
+
next: { kind: 'roster', tool: 'vibe_v5_members', hint: '先用 vibe_v5_members 查看当前名册与阶段(active / provisioning / dismissed)。' },
|
|
1023
|
+
}
|
|
930
1024
|
}
|
|
931
1025
|
function byChild(childId) { return inst().members.find((m) => m.childId === childId) }
|
|
932
1026
|
// The live academician's id, or '' when the office founded the institute with
|
|
@@ -1149,7 +1243,7 @@ export function apply(ctx) {
|
|
|
1149
1243
|
L.push(' 2. 只有 Verified/ 目录下的结论(以及成果卡中标注"已验证·真/假"的条目)绝对可信。')
|
|
1150
1244
|
L.push(' 其余一切——他人的推测、你自己的未验结论、Progress/、Methods/ 里的未验证断言——')
|
|
1151
1245
|
L.push(' 都只是经验性参考,引用时必须注明"未验证"。')
|
|
1152
|
-
L.push(' 3. 任何人可以读任何人的成果库;你只能写自己的库(Members
|
|
1246
|
+
L.push(' 3. 任何人可以读任何人的成果库;你只能写自己的库(' + instRel('Members/<你>/') + ')。')
|
|
1153
1247
|
L.push(' 4. 你写下的有价值内容由你自己判断是否入库,但入库必须写明三项:')
|
|
1154
1248
|
L.push(' 价值程度、动机用途计划、你自己对"该对象为真"的概率估计。')
|
|
1155
1249
|
L.push(' 5. 你随时可以在群聊里说话;要单独找人可以私信。需要集体决策就提议开会。')
|
|
@@ -1164,8 +1258,10 @@ export function apply(ctx) {
|
|
|
1164
1258
|
L.push('')
|
|
1165
1259
|
// ── 三、libraries ───────────────────────────────────────────────────
|
|
1166
1260
|
L.push('【三、你的资料库、progress 与卡片格式】')
|
|
1167
|
-
L.push('
|
|
1261
|
+
L.push(' 你的资料库根目录(**相对会话工作目录**):' + instRel('Members/' + member.id + '/'))
|
|
1168
1262
|
L.push(' (以下路径都相对该目录。你**只写这里**,但可以读任何人的对应目录。)')
|
|
1263
|
+
L.push(' ⚠ 路径基准:你自己的文件工具的**相对路径以会话工作目录为基准**,所以直接读写文件时必须用上面这条完整路径;')
|
|
1264
|
+
L.push(' 若只想记录成果,直接用 vibe_v5_record_progress / vibe_v5_record_proposition / vibe_v5_record_method / vibe_v5_record_subproblem,框架会写到正确位置。')
|
|
1169
1265
|
L.push('')
|
|
1170
1266
|
L.push(LIB_SPEC)
|
|
1171
1267
|
L.push('')
|
|
@@ -1303,7 +1399,7 @@ export function apply(ctx) {
|
|
|
1303
1399
|
const ms = activeMembers()
|
|
1304
1400
|
const b = []
|
|
1305
1401
|
b.push('[状态] 你是 ' + member.id + '(' + kindLabel(member.kind) + ')|轮次 ' + (rounds.get(member.id) || 0) +
|
|
1306
|
-
'|法定票数 m=' + quorumM() + '|有表决权者 ' + voterCount() + ' 人')
|
|
1402
|
+
'|法定票数 m=' + (voterCount() > 0 ? quorumM() : '未启动') + '|有表决权者 ' + voterCount() + ' 人')
|
|
1307
1403
|
b.push('[在册] ' + (ms.length ? ms.map((x) => x.id).join('、') : '(无)'))
|
|
1308
1404
|
// Members that are on the books but NOT on the floor. Silently omitting them made a
|
|
1309
1405
|
// failed provision invisible to the whole institute.
|
|
@@ -1770,7 +1866,7 @@ export function apply(ctx) {
|
|
|
1770
1866
|
const soft = (contextPct.get(member.id) || 0) >= Number(params.compactThreshold) ||
|
|
1771
1867
|
(roundsSinceCompact.get(member.id) || 0) >= Number(params.compactAfterRounds)
|
|
1772
1868
|
if (soft) {
|
|
1773
|
-
prompt =
|
|
1869
|
+
prompt = coreRules() + '\n[CONTEXT COMPACT — 你的对话已接近上限。不要重新推导历史。\n' +
|
|
1774
1870
|
'请把当前工作状态浓缩成一段自述(已有发现、当前方向、已记录的关键成果、下一步具体动作、未决问题),' +
|
|
1775
1871
|
'然后照常以 JSON 回答本轮。请在回复里填 "contextPct": 15 与 "compacted": true。]\n\n' + prompt
|
|
1776
1872
|
// Reset the counter WITH the injection so the directive cannot repeat on the
|
|
@@ -1779,7 +1875,7 @@ export function apply(ctx) {
|
|
|
1779
1875
|
}
|
|
1780
1876
|
}
|
|
1781
1877
|
if (needReanchor.has(member.id)) {
|
|
1782
|
-
prompt =
|
|
1878
|
+
prompt = coreRules() + '\n' + prompt
|
|
1783
1879
|
needReanchor.delete(member.id)
|
|
1784
1880
|
}
|
|
1785
1881
|
try {
|
|
@@ -1861,7 +1957,7 @@ export function apply(ctx) {
|
|
|
1861
1957
|
L.push(' "prioritize": {"order":[{"task_id":"t-1","priority":2}],"why":"…"} ← 设定全所优先级,')
|
|
1862
1958
|
L.push(' "nudge": {"to":"r-2","why":"为何督办","next_step":"建议的具体下一步"},')
|
|
1863
1959
|
}
|
|
1864
|
-
L.push(' "task_create": {"subject":"…","description":"…","blocked_by":["t-1"],"write_scopes":["Members/r-1/Propos"]},')
|
|
1960
|
+
L.push(' "task_create": {"subject":"…","description":"…","blocked_by":["t-1"],"write_scopes":["' + instRel('Members/r-1/Propos') + '"]},')
|
|
1865
1961
|
L.push(' "task_claim": "t-3",')
|
|
1866
1962
|
L.push(' "task_done": "t-3",')
|
|
1867
1963
|
L.push(' "task_update": {"task_id":"t-3","expected_revision":2,"action":"complete|release|reopen|edit|set_dependencies|delete"},')
|
|
@@ -3328,9 +3424,12 @@ export function apply(ctx) {
|
|
|
3328
3424
|
// A SHORT core-rules recap, injected ONLY (a) right after a REAL compaction, or
|
|
3329
3425
|
// (b) in the same wake as a soft-compact directive — never on every round. The
|
|
3330
3426
|
// charter itself lives in `persona` and needs no reinforcement otherwise.
|
|
3331
|
-
|
|
3427
|
+
// A function, not a frozen string: the member-facing library path must reflect the LIVE
|
|
3428
|
+
// project/institute (DEFECT A) — a path captured at session creation could point at the
|
|
3429
|
+
// default project after a `configure`.
|
|
3430
|
+
const coreRules = () => '[核心规则] 只有 Verified/(及标记"已验证·真/假"的卡片)算已确立;' +
|
|
3332
3431
|
'任何对象要进 Verified/,必须至少有 m 名有表决权者投出布尔值(恰好 1 或恰好 0)**且没有任何一张反向票**,否则留库附平均概率;' +
|
|
3333
|
-
'你只写自己的库(Members
|
|
3432
|
+
'你只写自己的库(' + instRel('Members/<你>/') + '——相对**会话工作目录**),可只读任何人的库;组织与分派由院士负责,但判断属于你自己;' +
|
|
3334
3433
|
'退出时只输出一个 JSON 对象。'
|
|
3335
3434
|
|
|
3336
3435
|
// ONE place accounts for context usage on EVERY reply (normal, meeting, verify,
|
|
@@ -3377,7 +3476,7 @@ export function apply(ctx) {
|
|
|
3377
3476
|
return n
|
|
3378
3477
|
}
|
|
3379
3478
|
async function publishProgress(memberId, text) {
|
|
3380
|
-
if (!memberId || !memberById(memberId)) return
|
|
3479
|
+
if (!memberId || !memberById(memberId)) return memberDiagnosis('记录研究进度(vibe_v5_record_progress)', memberId)
|
|
3381
3480
|
if (!String(text || '').trim()) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'empty progress' }
|
|
3382
3481
|
const rel = 'Members/' + memberId + '/Progress/progress.md'
|
|
3383
3482
|
const prev = (await readTextRel(rel)) || ''
|
|
@@ -3390,7 +3489,7 @@ export function apply(ctx) {
|
|
|
3390
3489
|
// hard requirements. Missing fields are refused rather than silently defaulted,
|
|
3391
3490
|
// so the libraries keep their meaning.
|
|
3392
3491
|
async function recordCard(memberId, kind, o) {
|
|
3393
|
-
if (!memberId || !memberById(memberId)) return
|
|
3492
|
+
if (!memberId || !memberById(memberId)) return memberDiagnosis('记录成果卡片(vibe_v5_record_proposition/method/subproblem)', memberId)
|
|
3394
3493
|
const args = o || {}
|
|
3395
3494
|
const missing = []
|
|
3396
3495
|
if (args.value === undefined || args.value === null) missing.push('value(价值程度)')
|
|
@@ -3553,7 +3652,7 @@ export function apply(ctx) {
|
|
|
3553
3652
|
// An unknown caller must be refused HERE as well: `owner = task.ownerId === memberId` is true
|
|
3554
3653
|
// for an UNOWNED task (ownerId '') when memberId is '', so without this guard an
|
|
3555
3654
|
// unidentifiable caller would count as the owner of every unclaimed task (audit L6 follow-up).
|
|
3556
|
-
if (!memberId) return
|
|
3655
|
+
if (!memberId) return memberDiagnosis('修改任务(vibe_v5_task_update)', memberId)
|
|
3557
3656
|
const args = o || {}
|
|
3558
3657
|
const id = String(args.task_id || args.taskId || '')
|
|
3559
3658
|
const task = inst().tasks.find((x) => x.id === id)
|
|
@@ -3650,7 +3749,7 @@ export function apply(ctx) {
|
|
|
3650
3749
|
// it can never make any statement true, and the assignee may object with reasons
|
|
3651
3750
|
// (the objection is broadcast, not silently swallowed).
|
|
3652
3751
|
async function taskAssign(memberId, o) {
|
|
3653
|
-
if (!memberId) return
|
|
3752
|
+
if (!memberId) return memberDiagnosis('分派任务(vibe_v5_assign)', memberId)
|
|
3654
3753
|
if (!isOffice(memberId) && !(isAcademician(memberId) && params.academicianLeads)) {
|
|
3655
3754
|
return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks' }
|
|
3656
3755
|
}
|
|
@@ -3694,7 +3793,7 @@ export function apply(ctx) {
|
|
|
3694
3793
|
return { ok: true, task: taskView(withMeta) }
|
|
3695
3794
|
}
|
|
3696
3795
|
async function taskPrioritize(memberId, o) {
|
|
3697
|
-
if (!memberId) return
|
|
3796
|
+
if (!memberId) return memberDiagnosis('设置任务优先级(vibe_v5_task_prioritize)', memberId)
|
|
3698
3797
|
if (!isOffice(memberId) && !(isAcademician(memberId) && params.academicianLeads)) {
|
|
3699
3798
|
return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can set priorities' }
|
|
3700
3799
|
}
|
|
@@ -3746,7 +3845,7 @@ export function apply(ctx) {
|
|
|
3746
3845
|
const s = inst()
|
|
3747
3846
|
const lines = ['# 研究所编制表(人读镜像)|' + instituteName + '|' + fmtTime(), '',
|
|
3748
3847
|
'> 权威状态在 State/<研究所>.v5state.json 里;本文件只是快照,勿手改。', '']
|
|
3749
|
-
lines.push('- 求真门槛:m = ' + quorumM() + '(模式 ' + params.quorumMode + ')|有表决权者 ' + voterCount() + ' 人')
|
|
3848
|
+
lines.push('- 求真门槛:' + (voterCount() > 0 ? 'm = ' + quorumM() : '未启动(有表决权者 0 人,m 未定义)') + '(模式 ' + params.quorumMode + ')|有表决权者 ' + voterCount() + ' 人')
|
|
3750
3849
|
lines.push('- 阶段:' + phase + '|运行中:' + running + '|已结题:' + autoDone)
|
|
3751
3850
|
lines.push('')
|
|
3752
3851
|
lines.push('| 代号 | 职位 | 状态 | 雇主 | 方向/用途 | 轮次 | 上下文% |')
|
|
@@ -3873,6 +3972,9 @@ export function apply(ctx) {
|
|
|
3873
3972
|
}
|
|
3874
3973
|
const mean = all.length ? all.reduce((a, x) => a + x, 0) / all.length : 0.5
|
|
3875
3974
|
const base = { m, P, bTrue, bFalse, abstain, mean, votedCount: all.length, voters: E }
|
|
3975
|
+
// DEFECT 2 hardening: with NO voters there is nothing to conclude — every `>= m` test
|
|
3976
|
+
// below would be trivially satisfiable if m were ever 0. Consensus needs voters.
|
|
3977
|
+
if (P === 0) return Object.assign(base, { outcome: 'undecided', reason: 'no voters: the institute has no voting members yet' })
|
|
3876
3978
|
if (params.quorumMode === 'all-unanimous') {
|
|
3877
3979
|
const allVoted = E.length > 0 && E.every((id) => votes[id])
|
|
3878
3980
|
if (!allVoted) return Object.assign(base, { outcome: 'undecided', reason: 'not every voter has voted' })
|
|
@@ -4219,7 +4321,7 @@ export function apply(ctx) {
|
|
|
4219
4321
|
// Record one vote and, when every voter has answered, settle the round.
|
|
4220
4322
|
async function castVerdict(memberId, target, verdict, reason) {
|
|
4221
4323
|
const member = memberById(memberId)
|
|
4222
|
-
if (!member) return
|
|
4324
|
+
if (!member) return memberDiagnosis('投票(vibe_v5_verdict)', memberId)
|
|
4223
4325
|
if (member.kind === 'temp') {
|
|
4224
4326
|
// Temp workers have no vote — but their judgement still matters, so it is
|
|
4225
4327
|
// relayed to the group instead of being silently dropped.
|
|
@@ -4578,7 +4680,7 @@ export function apply(ctx) {
|
|
|
4578
4680
|
L.push('【最终论文·撰写 —— ' + kindLabel(member.kind) + ' ' + member.id + '】')
|
|
4579
4681
|
L.push('本所对原问题的一致结论已经达成,现在撰写**最终论文**(第 ' + p.round + '/' + PAPER_MAX_ROUNDS + ' 轮)。')
|
|
4580
4682
|
L.push('请你**只写你自己库里已有证据支撑**的内容:')
|
|
4581
|
-
L.push('- 直接引用你的卡片(Members/' + member.id + '/Propos|Methods|Subproblems
|
|
4683
|
+
L.push('- 直接引用你的卡片(' + instRel('Members/' + member.id + '/Propos|Methods|Subproblems/') + ')、' + instRel('Members/' + member.id + '/Progress/progress.md') + '、你参与的表决记录;')
|
|
4582
4684
|
L.push('- **不得编造**:没有证据的推测不要写成结论;未决 / 被否证的条目必须显式标注“未定论 / 已被否证”;')
|
|
4583
4685
|
L.push('- 在 evidence 里写清证据路径,附录会逐条索引。')
|
|
4584
4686
|
if (p.round > 1) {
|
|
@@ -4598,7 +4700,7 @@ export function apply(ctx) {
|
|
|
4598
4700
|
L.push(' "methods": "你创造/发现的方法、理论、思想、有价值经验、数学理解",')
|
|
4599
4701
|
L.push(' "rules": "你从这些工作中归纳出的可复用规律",')
|
|
4600
4702
|
L.push(' "limits": "局限、未决、被否证之处(必须诚实、显式)",')
|
|
4601
|
-
L.push(' "evidence": ["Members/' + member.id + '/Propos/p-x.md"] } }')
|
|
4703
|
+
L.push(' "evidence": ["' + instRel('Members/' + member.id + '/Propos/p-x.md') + '"] } }')
|
|
4602
4704
|
return L.join('\n')
|
|
4603
4705
|
}
|
|
4604
4706
|
function paperReviewPrompt(member, p, ofId) {
|
|
@@ -5024,8 +5126,10 @@ export function apply(ctx) {
|
|
|
5024
5126
|
}
|
|
5025
5127
|
if (ev.formalKeys.length) out.push('Formal/Index.md', 'Formal/TODO.md')
|
|
5026
5128
|
for (const m of ev.members) {
|
|
5027
|
-
|
|
5028
|
-
|
|
5129
|
+
// Member-facing evidence paths are CWD-relative (DEFECT A): a member copying one of these
|
|
5130
|
+
// into its own file tool must not create a stray `<cwd>/Members/…` tree.
|
|
5131
|
+
out.push(instRel('Members/' + m.id + '/Progress/progress.md') + '(' + m.id + ' 的研究日志)')
|
|
5132
|
+
out.push(instRel('Members/' + m.id + '/{Propos,Methods,Subproblems}/') + '(' + m.id + ' 的卡片)')
|
|
5029
5133
|
}
|
|
5030
5134
|
out.push('Institutes.md(编制镜像)', 'Shared/TaskBoard.md(任务板镜像)')
|
|
5031
5135
|
out.push('Paper/' + p.id + '/paper.log.md(本论文流程往来)')
|
|
@@ -5508,7 +5612,7 @@ export function apply(ctx) {
|
|
|
5508
5612
|
return { ok: true, dismissed: id, reclaimedTasks: reclaimed, reason }
|
|
5509
5613
|
}
|
|
5510
5614
|
async function nudge(callerId, o) {
|
|
5511
|
-
if (!callerId) return
|
|
5615
|
+
if (!callerId) return memberDiagnosis('督办(vibe_v5_nudge)', callerId)
|
|
5512
5616
|
if (!isOffice(callerId) && !(isAcademician(callerId) && params.academicianLeads)) {
|
|
5513
5617
|
return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can nudge members' }
|
|
5514
5618
|
}
|
|
@@ -6103,7 +6207,7 @@ export function apply(ctx) {
|
|
|
6103
6207
|
memberMayRejectAssign: params.memberMayRejectAssign,
|
|
6104
6208
|
researcherCount: params.researcherCount,
|
|
6105
6209
|
quorumCap: params.quorumCap, quorumMode: params.quorumMode,
|
|
6106
|
-
m:
|
|
6210
|
+
m: quorumView().m, voterCount: voterCount(), started: voterCount() > 0,
|
|
6107
6211
|
verdictMaxRounds: params.verdictMaxRounds,
|
|
6108
6212
|
maxTempPerMember: params.maxTempPerMember, maxTempTotal: params.maxTempTotal,
|
|
6109
6213
|
compactThreshold: params.compactThreshold, compactAfterRounds: params.compactAfterRounds,
|
|
@@ -6419,7 +6523,7 @@ export function apply(ctx) {
|
|
|
6419
6523
|
// failure modes that silently drop state were invisible in the operator's view.
|
|
6420
6524
|
diagnostics: s.diagnostics || [],
|
|
6421
6525
|
debug: Object.assign({ scheduling, reschedule }, dbg),
|
|
6422
|
-
quorum:
|
|
6526
|
+
quorum: quorumView(),
|
|
6423
6527
|
members: s.members.map((m) => ({
|
|
6424
6528
|
id: m.id, kind: m.kind, phase: m.phase, direction: m.direction, hiredBy: m.hiredBy,
|
|
6425
6529
|
rounds: rounds.get(m.id) || 0, busy: busy.has(m.id), contextPct: contextPct.get(m.id) || 0,
|
|
@@ -6456,12 +6560,15 @@ export function apply(ctx) {
|
|
|
6456
6560
|
function report() {
|
|
6457
6561
|
syncParamsFromState()
|
|
6458
6562
|
const s = inst()
|
|
6563
|
+
// ONE snapshot for the whole report: the text line and the returned `quorum` object can
|
|
6564
|
+
// never disagree (DEFECT B), and `rosterVersion` lets a reader detect a stale copy.
|
|
6565
|
+
const qv = quorumView()
|
|
6459
6566
|
const L = []
|
|
6460
6567
|
L.push('# 「' + instituteName + '」研究所汇报')
|
|
6461
6568
|
L.push('')
|
|
6462
6569
|
L.push('- 项目:' + project + '|阶段:' + phase + '|运行中:' + running + '|已结题:' + autoDone)
|
|
6463
6570
|
L.push('- 研究对象:' + (s.problem.statement ? s.problem.statement.slice(0, 200) : '(未设定)'))
|
|
6464
|
-
L.push('- 求真门槛:m = ' +
|
|
6571
|
+
L.push('- 求真门槛:' + (qv.started ? 'm = ' + qv.m : '未启动(有表决权者 0 人,m 未定义)') + '(模式 ' + params.quorumMode + ')|有表决权者 ' + qv.voterCount + ' 人|rosterVersion ' + qv.rosterVersion)
|
|
6465
6572
|
L.push('')
|
|
6466
6573
|
L.push('## 编制')
|
|
6467
6574
|
if (!s.members.length) L.push('(暂无成员)')
|
|
@@ -6533,8 +6640,8 @@ export function apply(ctx) {
|
|
|
6533
6640
|
L.push('')
|
|
6534
6641
|
L.push('## 文件位置')
|
|
6535
6642
|
L.push('- 根目录:' + instRoot())
|
|
6536
|
-
L.push('-
|
|
6537
|
-
return { ok: true, report: L.join('\n') }
|
|
6643
|
+
L.push('- 工作目录相对路径(你的文件工具的基准):' + instRootRel() + '/|已确立:Verified/|成员库:Members/<id>/|群聊:Shared/Chat/|会议:Shared/Meetings/|辩论:Shared/Debates/')
|
|
6644
|
+
return { ok: true, report: L.join('\n'), quorum: qv }
|
|
6538
6645
|
}
|
|
6539
6646
|
// Adding/removing a PERMANENT researcher is a change to the institute's public
|
|
6540
6647
|
// structure, so members may only propose it; the office decides and executes.
|
|
@@ -6606,6 +6713,8 @@ export function apply(ctx) {
|
|
|
6606
6713
|
startPaper, paperStatus: paperSummary, finalizePaperByOffice,
|
|
6607
6714
|
// authorization helpers (used by tool handlers)
|
|
6608
6715
|
memberIdOfAgent, isOffice, isAcademician, isProvablyOffice, officeCaller, memberById, activeMembers,
|
|
6716
|
+
// diagnosis helpers (defects 1/2 of the architecture self-test)
|
|
6717
|
+
memberDiagnosis, quorumM, quorumView, voterCount,
|
|
6609
6718
|
}
|
|
6610
6719
|
}
|
|
6611
6720
|
|
|
@@ -6800,7 +6909,7 @@ export function apply(ctx) {
|
|
|
6800
6909
|
// ── member-facing controls ────────────────────────────────────────────────
|
|
6801
6910
|
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) => {
|
|
6802
6911
|
const from = s.memberIdOfAgent(x)
|
|
6803
|
-
if (!from) return
|
|
6912
|
+
if (!from) return s.memberDiagnosis('发言(vibe_v5_say)', from)
|
|
6804
6913
|
const to = a.to || 'all'
|
|
6805
6914
|
return s.say(from, { to, text: a.text, kind: to === 'voters' ? 'voters' : (a.to ? 'dm' : 'chat') })
|
|
6806
6915
|
})
|