dsh-vibe-math 2.7.0 → 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.
Files changed (55) hide show
  1. package/README.en.md +681 -685
  2. package/README.md +9 -13
  3. package/cordis.patch.yml +12 -6
  4. package/docs/AUDIT-CHECKLIST.md +518 -518
  5. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  6. package/docs/final-paper.md +172 -172
  7. package/docs/formal-verification.md +588 -588
  8. package/docs/math-computation.md +187 -121
  9. package/docs/release-notes/README.en.md +48 -0
  10. package/docs/release-notes/README.md +47 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.1.en.md +86 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
  13. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  15. package/docs/release-notes/TEMPLATE.md +120 -0
  16. package/docs/test-timing.md +154 -154
  17. package/package.json +208 -201
  18. package/prompt-corpus-persona/persona-corpus.json +8 -8
  19. package/prompt-corpus-persona/persona-corpus.md +24 -12
  20. package/prompt-corpus-v2/formal-verify-v2.json +176 -98
  21. package/prompt-corpus-v2/formal-verify-v2.md +1161 -96
  22. package/prompt-corpus-v3/formal-verify-v3.json +201 -63
  23. package/prompt-corpus-v3/formal-verify-v3.md +1401 -57
  24. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  25. package/prompt-corpus-v4/formal-verify-v4.md +40 -20
  26. package/prompt-corpus-v5/prompt-corpus-v5.json +81 -81
  27. package/prompt-corpus-v5/prompt-corpus-v5.md +199 -128
  28. package/tests/audit-math-computation-contract.mjs +56 -1
  29. package/tests/audit-math-computation-parity.mjs +30 -1
  30. package/tests/audit-math-computation-sensitivity.mjs +2 -2
  31. package/tests/audit-v5-sensitivity.mjs +3 -1
  32. package/tests/e2e-v5-round2.test.mjs +116 -0
  33. package/tests/formal-verify-v2.test.mjs +108 -2
  34. package/tests/formal-verify-v3.test.mjs +117 -2
  35. package/tests/formal-verify-v4.test.mjs +87 -0
  36. package/tests/helpers/math-computation-fake-seam.mjs +30 -2
  37. package/tests/math-computation-shared.test.mjs +430 -3
  38. package/vibe-math-v2/agent.cordis.yml +4 -2
  39. package/vibe-math-v2/math-computation.js +509 -55
  40. package/vibe-math-v2/math-engines.js +47 -6
  41. package/vibe-math-v2/vibe-math-v2.js +92 -12
  42. package/vibe-math-v3/agent.cordis.yml +4 -2
  43. package/vibe-math-v3/math-computation.js +509 -55
  44. package/vibe-math-v3/math-engines.js +47 -6
  45. package/vibe-math-v3/vibe-math-v3.js +77 -14
  46. package/vibe-math-v4/agent.cordis.yml +8 -4
  47. package/vibe-math-v4/math-computation.js +509 -55
  48. package/vibe-math-v4/math-engines.js +47 -6
  49. package/vibe-math-v4/vibe-math-v4.js +70 -15
  50. package/vibe-math-v5/agent.cordis.yml +8 -4
  51. package/vibe-math-v5/math-computation.js +509 -55
  52. package/vibe-math-v5/math-engines.js +47 -6
  53. package/vibe-math-v5/vibe-math-v5.js +152 -31
  54. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1660 -1655
  55. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -22,14 +22,47 @@ export const MATH_ENGINES = {
22
22
  candidates: ['python3', 'python', 'py'], winPrefix: ['-3'],
23
23
  versionArgv: ['--version'], versionRe: '(\\d+\\.\\d+\\.\\d+)',
24
24
  scriptArgv: ['<script>'], evalArgv: ['-c', '<expr>'], stdinArgv: ['-'], ext: '.py',
25
- probeCode: "import importlib.util,sys;print('|'.join('%s:%s'%(p,'ok' if importlib.util.find_spec(p) else 'missing') for p in sys.argv[1:]))",
26
- packageProbe: { argv: ['-c', '<probeCode>', '<pkgs>'], parse: 'pairs' },
25
+ // round-7 (live): NO shell metacharacters (`|`, `%`) in any probe argv - a real host rejected
26
+ // them with a null spawn. One line per package: "name ok|missing".
27
+ probeCode: "import importlib.util,sys\nfor p in sys.argv[1:]: print(p, 'ok' if importlib.util.find_spec(p) is not None else 'missing')",
28
+ // round-7 (finding 6, found on the real bundled python): the probe code iterates `sys.argv[1:]`,
29
+ // so the names must be SEPARATE argv items - the old comma-joined `<pkgs>` made it look up one
30
+ // package literally named "numpy,pandas" and reported everything missing.
31
+ packageProbe: { argv: ['-c', '<probeCode>', '<pkgs...>'], parse: 'pairs' },
27
32
  license: 'free', argErrorHints: ARG_ERROR_HINTS,
28
33
  install: {
29
34
  manager: 'pip',
30
35
  userArgv: ['<exe>', '-m', 'pip', 'install', '--user', '<pkg>'],
31
36
  systemArgv: ['<exe>', '-m', 'pip', 'install', '<pkg>'],
32
37
  uninstallArgv: ['<exe>', '-m', 'pip', 'uninstall', '-y', '<pkg>'],
38
+ // Round-6 (B): DISPATCH by the detected interpreter environment. `pip` is the documented
39
+ // fallback; the module picks conda/mamba/uv only when the interpreter's OWN environment says so
40
+ // (detect, don't guess) and the plan states which manager it assumed.
41
+ managers: {
42
+ conda: {
43
+ detect: 'conda-markers',
44
+ userArgv: ['conda', 'install', '-y', '-c', 'conda-forge', '<pkg>'],
45
+ uninstallArgv: ['conda', 'remove', '-y', '<pkg>'],
46
+ systemUnsupportedReason: 'conda 环境是用户级的;系统级安装不在 conda 的概念里(本工具只出用户级计划)',
47
+ },
48
+ mamba: {
49
+ detect: 'conda-markers',
50
+ userArgv: ['mamba', 'install', '-y', '-c', 'conda-forge', '<pkg>'],
51
+ uninstallArgv: ['mamba', 'remove', '-y', '<pkg>'],
52
+ systemUnsupportedReason: 'mamba 环境是用户级的;系统级安装不在 mamba 的概念里(本工具只出用户级计划)',
53
+ },
54
+ uv: {
55
+ detect: 'uv-next-to-interpreter',
56
+ userArgv: ['uv', 'pip', 'install', '<pkg>'],
57
+ uninstallArgv: ['uv', 'pip', 'uninstall', '<pkg>'],
58
+ systemUnsupportedReason: 'uv 装进当前虚拟环境(用户级);系统级需要发行版包管理器,本工具只出用户级计划',
59
+ },
60
+ pip: {
61
+ detect: 'fallback',
62
+ systemArgv: ['<exe>', '-m', 'pip', 'install', '<pkg>'],
63
+ uninstallArgv: ['<exe>', '-m', 'pip', 'uninstall', '-y', '<pkg>'],
64
+ },
65
+ },
33
66
  },
34
67
  userInstall: { windows: 'winget install --id Python.Python.3.12', macos: 'brew install python', linux: 'apt install python3' },
35
68
  },
@@ -38,13 +71,16 @@ export const MATH_ENGINES = {
38
71
  candidates: ['Rscript', 'R'],
39
72
  versionArgv: ['--version'], versionRe: '(\\d+\\.\\d+\\.\\d+)',
40
73
  scriptArgv: ['<script>'], evalArgv: ['-e', '<expr>'], stdinArgv: ['--no-save', '--slave'], ext: '.R',
41
- probeCode: "cat(paste(sapply(strsplit('__PKGS__',',')[[1]],function(p) paste0(p,':',requireNamespace(p,quietly=TRUE))),collapse='|'))",
74
+ // round-7 (live): line-per-package, no `|` in the argv.
75
+ probeCode: "for (p in strsplit('__PKGS__',',')[[1]]) cat(p, if (requireNamespace(p, quietly=TRUE)) 'ok' else 'missing', '\\n')",
42
76
  packageProbe: { argv: ['-e', '<probeCode>'], parse: 'pairs' },
43
77
  license: 'free', argErrorHints: ARG_ERROR_HINTS,
44
78
  install: {
45
79
  manager: 'r',
46
80
  userArgv: ['<exe>', '-e', "install.packages('__PKG__',lib=Sys.getenv('R_LIBS_USER'),repos='https://cloud.r-project.org')"],
47
81
  uninstallArgv: ['<exe>', '-e', "remove.packages('__PKG__',lib=Sys.getenv('R_LIBS_USER'))"],
82
+ // Round-6 (C): user-only BY DESIGN, and the refusal now says why per engine.
83
+ systemUnsupportedReason: 'R 的用户库是 R_LIBS_USER;系统级安装要写发行版的包目录(需 root 或发行版包管理器),本工具只出用户级计划',
48
84
  },
49
85
  userInstall: { windows: 'winget install --id RProject.R', macos: 'brew install r', linux: 'apt install r-base' },
50
86
  },
@@ -54,13 +90,15 @@ export const MATH_ENGINES = {
54
90
  versionArgv: ['--version'], versionRe: '(\\d+\\.\\d+\\.\\d+)',
55
91
  scriptArgv: ['--no-gui', '--quiet', '<script>'], evalArgv: ['--no-gui', '--quiet', '--eval', '<expr>'],
56
92
  stdinArgv: ['--no-gui', '--quiet'], ext: '.m',
57
- probeCode: "s=pkg('list'); n={'__QPKGS__'}; for i=1:numel(n); printf('%s:%s|',n{i},merge(any(cellfun(@(x) strcmp(x.name,n{i}),s)),'ok','missing')); end",
93
+ // round-7 (live): line-per-package via disp, no `|` and no `%` in the argv.
94
+ probeCode: "s=pkg('list'); n={'__QPKGS__'}; for i=1:numel(n); if any(cellfun(@(x) strcmp(x.name,n{i}),s)); disp([n{i} ' ok']); else; disp([n{i} ' missing']); end; end",
58
95
  packageProbe: { argv: ['--no-gui', '--quiet', '--eval', '<probeCode>'], parse: 'pairs' },
59
96
  license: 'free', argErrorHints: ARG_ERROR_HINTS,
60
97
  install: {
61
98
  manager: 'octave-pkg',
62
99
  userArgv: ['<exe>', '--no-gui', '--quiet', '--eval', "pkg install -forge __PKG__"],
63
100
  uninstallArgv: ['<exe>', '--no-gui', '--quiet', '--eval', 'pkg uninstall __PKG__'],
101
+ systemUnsupportedReason: 'Octave 的 pkg install 装进用户包目录;系统级要写 Octave 的 share/packages(需 root),本工具只出用户级计划',
64
102
  },
65
103
  userInstall: { windows: 'winget install --id GNU.Octave', macos: 'brew install octave', linux: 'apt install octave' },
66
104
  },
@@ -76,6 +114,7 @@ export const MATH_ENGINES = {
76
114
  manager: 'julia-pkg',
77
115
  userArgv: ['<exe>', '-e', 'using Pkg; Pkg.add("__PKG__")'],
78
116
  uninstallArgv: ['<exe>', '-e', 'using Pkg; Pkg.rm("__PKG__")'],
117
+ systemUnsupportedReason: 'Julia 的 Pkg.add 装进当前活动环境 / 用户 depot(JULIA_DEPOT_PATH);系统级不是 Julia 的概念,本工具只出用户级计划',
79
118
  },
80
119
  userInstall: { windows: 'winget install --id Julialang.Julia', macos: 'brew install julia', linux: 'apt install julia' },
81
120
  },
@@ -84,7 +123,8 @@ export const MATH_ENGINES = {
84
123
  candidates: ['matlab'],
85
124
  versionArgv: ['-batch', 'disp(version)'], versionRe: '(\\d+\\.\\d+)',
86
125
  scriptArgv: ['-batch', "run('<script>')"], evalArgv: ['-batch', '<expr>'], stdinArgv: null, ext: '.m',
87
- probeCode: "t={'__QPKGS__'}; for i=1:numel(t); printf('%s:%d|',t{i},license('test',t{i})); end",
126
+ // round-7 (live): no `|`/`%` in any probe argv (a real host rejected such argv with a null spawn).
127
+ probeCode: "t={'__QPKGS__'}; for i=1:numel(t); if license('test',t{i}); disp([t{i} ' ok']); else; disp([t{i} ' missing']); end; end",
88
128
  packageProbe: { argv: ['-batch', '<probeCode>'], parse: 'pairs' },
89
129
  license: 'commercial', argErrorHints: ARG_ERROR_HINTS,
90
130
  licenseProbe: { argv: ['-batch', "disp(license('test','MATLAB'))"], okWhen: 'trim-1' },
@@ -96,7 +136,8 @@ export const MATH_ENGINES = {
96
136
  candidates: ['maple'],
97
137
  versionArgv: ['--version'], versionRe: '(\\d+\\.\\d+)',
98
138
  scriptArgv: ['-q', '<script>'], evalArgv: ['-q', '-c', '<expr>'], stdinArgv: null, ext: '.mpl',
99
- probeCode: 'for p in ["__PKGS__"] do try with(p) catch: printf("%s:missing|", p); next end try; printf("%s:ok|", p) end do',
139
+ // round-7 (live): `print` instead of printf so the argv carries no `|`/`%`.
140
+ probeCode: 'for p in ["__PKGS__"] do try with(p) catch: print(p, " missing"); next end try; print(p, " ok") end do',
100
141
  packageProbe: { argv: ['-q', '-c', '<probeCode>'], parse: 'pairs' },
101
142
  license: 'commercial', argErrorHints: ARG_ERROR_HINTS,
102
143
  licenseProbe: { argv: ['-q', '-c', 'printf("1")'], okWhen: 'exit-0' },
@@ -67,6 +67,8 @@ import {
67
67
  MATH_RULE_LINES_EN,
68
68
  MATH_ARCHIVE_WORKFLOW_LINE,
69
69
  MATH_ARCHIVE_WORKFLOW_LINE_EN,
70
+ MATH_SUBSTITUTION_RULE_LINE,
71
+ MATH_SUBSTITUTION_RULE_LINE_EN,
70
72
  normalizeMathParams,
71
73
  probeMathEngines,
72
74
  registerMathComputation,
@@ -767,6 +769,14 @@ export function apply(ctx) {
767
769
  const vibeRoot = () => (workspaceRoot() + '/VibeMath').replace(/\\/g, '/')
768
770
  const projectRoot = () => vibeRoot() + '/Projects/' + project
769
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(/^\/+/, '')
770
780
 
771
781
  function getPolicy() {
772
782
  const sp = sandboxPolicyOf()
@@ -922,9 +932,95 @@ export function apply(ctx) {
922
932
  const memberById = (id) => inst().members.find((m) => m.id === id)
923
933
  const voters = () => activeMembers().filter((m) => m.kind === 'academician' || m.kind === 'researcher')
924
934
  const voterCount = () => voters().length
925
- function quorumM() {
935
+ function quorumMFrom(vc) {
926
936
  const cap = Math.max(1, Math.floor(Number(params.quorumCap) || 3))
927
- return Math.max(1, Math.min(cap, voterCount()))
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
+ }
928
1024
  }
929
1025
  function byChild(childId) { return inst().members.find((m) => m.childId === childId) }
930
1026
  // The live academician's id, or '' when the office founded the institute with
@@ -1147,7 +1243,7 @@ export function apply(ctx) {
1147
1243
  L.push(' 2. 只有 Verified/ 目录下的结论(以及成果卡中标注"已验证·真/假"的条目)绝对可信。')
1148
1244
  L.push(' 其余一切——他人的推测、你自己的未验结论、Progress/、Methods/ 里的未验证断言——')
1149
1245
  L.push(' 都只是经验性参考,引用时必须注明"未验证"。')
1150
- L.push(' 3. 任何人可以读任何人的成果库;你只能写自己的库(Members/<你>/)。')
1246
+ L.push(' 3. 任何人可以读任何人的成果库;你只能写自己的库(' + instRel('Members/<你>/') + ')。')
1151
1247
  L.push(' 4. 你写下的有价值内容由你自己判断是否入库,但入库必须写明三项:')
1152
1248
  L.push(' 价值程度、动机用途计划、你自己对"该对象为真"的概率估计。')
1153
1249
  L.push(' 5. 你随时可以在群聊里说话;要单独找人可以私信。需要集体决策就提议开会。')
@@ -1162,8 +1258,10 @@ export function apply(ctx) {
1162
1258
  L.push('')
1163
1259
  // ── 三、libraries ───────────────────────────────────────────────────
1164
1260
  L.push('【三、你的资料库、progress 与卡片格式】')
1165
- L.push(' 你的资料库根目录:Members/' + member.id + '/')
1261
+ L.push(' 你的资料库根目录(**相对会话工作目录**):' + instRel('Members/' + member.id + '/'))
1166
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,框架会写到正确位置。')
1167
1265
  L.push('')
1168
1266
  L.push(LIB_SPEC)
1169
1267
  L.push('')
@@ -1301,7 +1399,7 @@ export function apply(ctx) {
1301
1399
  const ms = activeMembers()
1302
1400
  const b = []
1303
1401
  b.push('[状态] 你是 ' + member.id + '(' + kindLabel(member.kind) + ')|轮次 ' + (rounds.get(member.id) || 0) +
1304
- '|法定票数 m=' + quorumM() + '|有表决权者 ' + voterCount() + ' 人')
1402
+ '|法定票数 m=' + (voterCount() > 0 ? quorumM() : '未启动') + '|有表决权者 ' + voterCount() + ' 人')
1305
1403
  b.push('[在册] ' + (ms.length ? ms.map((x) => x.id).join('、') : '(无)'))
1306
1404
  // Members that are on the books but NOT on the floor. Silently omitting them made a
1307
1405
  // failed provision invisible to the whole institute.
@@ -1768,7 +1866,7 @@ export function apply(ctx) {
1768
1866
  const soft = (contextPct.get(member.id) || 0) >= Number(params.compactThreshold) ||
1769
1867
  (roundsSinceCompact.get(member.id) || 0) >= Number(params.compactAfterRounds)
1770
1868
  if (soft) {
1771
- prompt = CORE_RULES + '\n[CONTEXT COMPACT — 你的对话已接近上限。不要重新推导历史。\n' +
1869
+ prompt = coreRules() + '\n[CONTEXT COMPACT — 你的对话已接近上限。不要重新推导历史。\n' +
1772
1870
  '请把当前工作状态浓缩成一段自述(已有发现、当前方向、已记录的关键成果、下一步具体动作、未决问题),' +
1773
1871
  '然后照常以 JSON 回答本轮。请在回复里填 "contextPct": 15 与 "compacted": true。]\n\n' + prompt
1774
1872
  // Reset the counter WITH the injection so the directive cannot repeat on the
@@ -1777,7 +1875,7 @@ export function apply(ctx) {
1777
1875
  }
1778
1876
  }
1779
1877
  if (needReanchor.has(member.id)) {
1780
- prompt = CORE_RULES + '\n' + prompt
1878
+ prompt = coreRules() + '\n' + prompt
1781
1879
  needReanchor.delete(member.id)
1782
1880
  }
1783
1881
  try {
@@ -1859,7 +1957,7 @@ export function apply(ctx) {
1859
1957
  L.push(' "prioritize": {"order":[{"task_id":"t-1","priority":2}],"why":"…"} ← 设定全所优先级,')
1860
1958
  L.push(' "nudge": {"to":"r-2","why":"为何督办","next_step":"建议的具体下一步"},')
1861
1959
  }
1862
- 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') + '"]},')
1863
1961
  L.push(' "task_claim": "t-3",')
1864
1962
  L.push(' "task_done": "t-3",')
1865
1963
  L.push(' "task_update": {"task_id":"t-3","expected_revision":2,"action":"complete|release|reopen|edit|set_dependencies|delete"},')
@@ -2902,6 +3000,11 @@ export function apply(ctx) {
2902
3000
  // reports MATH_NO_SUBPROCESS before engine detection), and let it check the retention cap.
2903
3001
  hasSubprocess: () => { const sub = subprocessOf(); return !!(sub && typeof sub.spawn === 'function') },
2904
3002
  listDir: (rel) => mathListDirRel(rel),
3003
+ // round-7 (fix 2): DSH's own bundled runtimes (`<home>/.dsh/dsh-runtimes/*/dependencies/<engine>/`)
3004
+ // as a LAST resort after PATH. Generic roots only; the module globs the tree name and accepts
3005
+ // only the descriptor's own candidate names.
3006
+ runtimeRoots: () => { const env = process.env.DSH_HOME; if (env) return [String(env)]; const home = String(process.env.HOME || process.env.USERPROFILE || ''); return home ? [home.replace(/[\\/]+$/, '') + '/.dsh'] : [] },
3007
+ listDirAbs: async (abs) => { try { const t = await fs.resolve(String(abs)); const st = await fs.stat(t); if (!st) return []; return (await fs.listDir(t)) || [] } catch (e) { return [] } },
2905
3008
  log: (kind, msg) => mathLog(kind, msg),
2906
3009
  }
2907
3010
  // The DYNAMIC per-round availability line: computed from a (cached) probe when the prompt is
@@ -2923,6 +3026,10 @@ export function apply(ctx) {
2923
3026
  // `scriptChanged` actionable for a member).
2924
3027
  if (mathLineZh.indexOf(MATH_ARCHIVE_WORKFLOW_LINE) === -1) console.error('vibe-math-v5: the math availability line lost the archive workflow rule')
2925
3028
  if (mathLineEn.indexOf(MATH_ARCHIVE_WORKFLOW_LINE_EN) === -1) console.error('vibe-math-v5: the EN math availability line lost the archive workflow rule')
3029
+ // Round-6 (A): an undeclared SUBSTITUTION must never read like the exact result — the
3030
+ // honesty rule has to reach the prompt in both languages.
3031
+ if (mathLineZh.indexOf(MATH_SUBSTITUTION_RULE_LINE) === -1) console.error('vibe-math-v5: the math availability line lost the substitution-honesty rule')
3032
+ if (mathLineEn.indexOf(MATH_SUBSTITUTION_RULE_LINE_EN) === -1) console.error('vibe-math-v5: the EN math availability line lost the substitution-honesty rule')
2926
3033
  return mathLineZh
2927
3034
  } catch (e) {
2928
3035
  // A probe failure must never break prompt construction: keep whatever we had.
@@ -2935,11 +3042,12 @@ export function apply(ctx) {
2935
3042
  const line = lang === 'en' ? mathLineEn : mathLineZh
2936
3043
  if (line) return '\n' + line
2937
3044
  // Fallback: when the dynamic line could not be built, the member still needs to know the
2938
- // tool exists, how to cite it, and (P2a) that an edited script needs a NEW receipt.
3045
+ // tool exists, how to cite it, that an edited script needs a NEW receipt, and (round 6) that
3046
+ // a substitution weakening exactness must be declared.
2939
3047
  if (String(params.mathComputation) === 'off') return ''
2940
3048
  return lang === 'en'
2941
- ? '\n' + MATH_PERSONA_TOOL_LINE + '\n' + MATH_ARCHIVE_WORKFLOW_LINE_EN
2942
- : '\n' + MATH_PERSONA_TOOL_LINE + '\n' + MATH_ARCHIVE_WORKFLOW_LINE
3049
+ ? '\n' + MATH_PERSONA_TOOL_LINE + '\n' + MATH_ARCHIVE_WORKFLOW_LINE_EN + '\n' + MATH_SUBSTITUTION_RULE_LINE_EN
3050
+ : '\n' + MATH_PERSONA_TOOL_LINE + '\n' + MATH_ARCHIVE_WORKFLOW_LINE + '\n' + MATH_SUBSTITUTION_RULE_LINE
2943
3051
  }
2944
3052
  // Push the tool section into a prompt under construction (a no-op in the 'off' mode, which
2945
3053
  // is the "off means zero mention" discipline).
@@ -3316,9 +3424,12 @@ export function apply(ctx) {
3316
3424
  // A SHORT core-rules recap, injected ONLY (a) right after a REAL compaction, or
3317
3425
  // (b) in the same wake as a soft-compact directive — never on every round. The
3318
3426
  // charter itself lives in `persona` and needs no reinforcement otherwise.
3319
- const CORE_RULES = '[核心规则] 只有 Verified/(及标记"已验证·真/假"的卡片)算已确立;' +
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/(及标记"已验证·真/假"的卡片)算已确立;' +
3320
3431
  '任何对象要进 Verified/,必须至少有 m 名有表决权者投出布尔值(恰好 1 或恰好 0)**且没有任何一张反向票**,否则留库附平均概率;' +
3321
- '你只写自己的库(Members/<你>/),可只读任何人的库;组织与分派由院士负责,但判断属于你自己;' +
3432
+ '你只写自己的库(' + instRel('Members/<你>/') + '——相对**会话工作目录**),可只读任何人的库;组织与分派由院士负责,但判断属于你自己;' +
3322
3433
  '退出时只输出一个 JSON 对象。'
3323
3434
 
3324
3435
  // ONE place accounts for context usage on EVERY reply (normal, meeting, verify,
@@ -3365,7 +3476,7 @@ export function apply(ctx) {
3365
3476
  return n
3366
3477
  }
3367
3478
  async function publishProgress(memberId, text) {
3368
- if (!memberId || !memberById(memberId)) return { ok: false, code: 'V5_MEMBER_NOT_FOUND' }
3479
+ if (!memberId || !memberById(memberId)) return memberDiagnosis('记录研究进度(vibe_v5_record_progress)', memberId)
3369
3480
  if (!String(text || '').trim()) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: 'empty progress' }
3370
3481
  const rel = 'Members/' + memberId + '/Progress/progress.md'
3371
3482
  const prev = (await readTextRel(rel)) || ''
@@ -3378,7 +3489,7 @@ export function apply(ctx) {
3378
3489
  // hard requirements. Missing fields are refused rather than silently defaulted,
3379
3490
  // so the libraries keep their meaning.
3380
3491
  async function recordCard(memberId, kind, o) {
3381
- if (!memberId || !memberById(memberId)) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'no such member' }
3492
+ if (!memberId || !memberById(memberId)) return memberDiagnosis('记录成果卡片(vibe_v5_record_proposition/method/subproblem)', memberId)
3382
3493
  const args = o || {}
3383
3494
  const missing = []
3384
3495
  if (args.value === undefined || args.value === null) missing.push('value(价值程度)')
@@ -3541,7 +3652,7 @@ export function apply(ctx) {
3541
3652
  // An unknown caller must be refused HERE as well: `owner = task.ownerId === memberId` is true
3542
3653
  // for an UNOWNED task (ownerId '') when memberId is '', so without this guard an
3543
3654
  // unidentifiable caller would count as the owner of every unclaimed task (audit L6 follow-up).
3544
- if (!memberId) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'no calling member' }
3655
+ if (!memberId) return memberDiagnosis('修改任务(vibe_v5_task_update)', memberId)
3545
3656
  const args = o || {}
3546
3657
  const id = String(args.task_id || args.taskId || '')
3547
3658
  const task = inst().tasks.find((x) => x.id === id)
@@ -3638,7 +3749,7 @@ export function apply(ctx) {
3638
3749
  // it can never make any statement true, and the assignee may object with reasons
3639
3750
  // (the objection is broadcast, not silently swallowed).
3640
3751
  async function taskAssign(memberId, o) {
3641
- if (!memberId) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'no calling member' }
3752
+ if (!memberId) return memberDiagnosis('分派任务(vibe_v5_assign)', memberId)
3642
3753
  if (!isOffice(memberId) && !(isAcademician(memberId) && params.academicianLeads)) {
3643
3754
  return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks' }
3644
3755
  }
@@ -3682,7 +3793,7 @@ export function apply(ctx) {
3682
3793
  return { ok: true, task: taskView(withMeta) }
3683
3794
  }
3684
3795
  async function taskPrioritize(memberId, o) {
3685
- if (!memberId) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'no calling member' }
3796
+ if (!memberId) return memberDiagnosis('设置任务优先级(vibe_v5_task_prioritize)', memberId)
3686
3797
  if (!isOffice(memberId) && !(isAcademician(memberId) && params.academicianLeads)) {
3687
3798
  return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can set priorities' }
3688
3799
  }
@@ -3734,7 +3845,7 @@ export function apply(ctx) {
3734
3845
  const s = inst()
3735
3846
  const lines = ['# 研究所编制表(人读镜像)|' + instituteName + '|' + fmtTime(), '',
3736
3847
  '> 权威状态在 State/<研究所>.v5state.json 里;本文件只是快照,勿手改。', '']
3737
- lines.push('- 求真门槛:m = ' + quorumM() + '(模式 ' + params.quorumMode + ')|有表决权者 ' + voterCount() + ' 人')
3848
+ lines.push('- 求真门槛:' + (voterCount() > 0 ? 'm = ' + quorumM() : '未启动(有表决权者 0 人,m 未定义)') + '(模式 ' + params.quorumMode + ')|有表决权者 ' + voterCount() + ' 人')
3738
3849
  lines.push('- 阶段:' + phase + '|运行中:' + running + '|已结题:' + autoDone)
3739
3850
  lines.push('')
3740
3851
  lines.push('| 代号 | 职位 | 状态 | 雇主 | 方向/用途 | 轮次 | 上下文% |')
@@ -3861,6 +3972,9 @@ export function apply(ctx) {
3861
3972
  }
3862
3973
  const mean = all.length ? all.reduce((a, x) => a + x, 0) / all.length : 0.5
3863
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' })
3864
3978
  if (params.quorumMode === 'all-unanimous') {
3865
3979
  const allVoted = E.length > 0 && E.every((id) => votes[id])
3866
3980
  if (!allVoted) return Object.assign(base, { outcome: 'undecided', reason: 'not every voter has voted' })
@@ -4207,7 +4321,7 @@ export function apply(ctx) {
4207
4321
  // Record one vote and, when every voter has answered, settle the round.
4208
4322
  async function castVerdict(memberId, target, verdict, reason) {
4209
4323
  const member = memberById(memberId)
4210
- if (!member) return { ok: false, code: 'V5_MEMBER_NOT_FOUND' }
4324
+ if (!member) return memberDiagnosis('投票(vibe_v5_verdict)', memberId)
4211
4325
  if (member.kind === 'temp') {
4212
4326
  // Temp workers have no vote — but their judgement still matters, so it is
4213
4327
  // relayed to the group instead of being silently dropped.
@@ -4566,7 +4680,7 @@ export function apply(ctx) {
4566
4680
  L.push('【最终论文·撰写 —— ' + kindLabel(member.kind) + ' ' + member.id + '】')
4567
4681
  L.push('本所对原问题的一致结论已经达成,现在撰写**最终论文**(第 ' + p.round + '/' + PAPER_MAX_ROUNDS + ' 轮)。')
4568
4682
  L.push('请你**只写你自己库里已有证据支撑**的内容:')
4569
- L.push('- 直接引用你的卡片(Members/' + member.id + '/Propos|Methods|Subproblems/)、Members/' + member.id + '/Progress/progress.md、你参与的表决记录;')
4683
+ L.push('- 直接引用你的卡片(' + instRel('Members/' + member.id + '/Propos|Methods|Subproblems/') + ')、' + instRel('Members/' + member.id + '/Progress/progress.md') + '、你参与的表决记录;')
4570
4684
  L.push('- **不得编造**:没有证据的推测不要写成结论;未决 / 被否证的条目必须显式标注“未定论 / 已被否证”;')
4571
4685
  L.push('- 在 evidence 里写清证据路径,附录会逐条索引。')
4572
4686
  if (p.round > 1) {
@@ -4586,7 +4700,7 @@ export function apply(ctx) {
4586
4700
  L.push(' "methods": "你创造/发现的方法、理论、思想、有价值经验、数学理解",')
4587
4701
  L.push(' "rules": "你从这些工作中归纳出的可复用规律",')
4588
4702
  L.push(' "limits": "局限、未决、被否证之处(必须诚实、显式)",')
4589
- L.push(' "evidence": ["Members/' + member.id + '/Propos/p-x.md"] } }')
4703
+ L.push(' "evidence": ["' + instRel('Members/' + member.id + '/Propos/p-x.md') + '"] } }')
4590
4704
  return L.join('\n')
4591
4705
  }
4592
4706
  function paperReviewPrompt(member, p, ofId) {
@@ -5012,8 +5126,10 @@ export function apply(ctx) {
5012
5126
  }
5013
5127
  if (ev.formalKeys.length) out.push('Formal/Index.md', 'Formal/TODO.md')
5014
5128
  for (const m of ev.members) {
5015
- out.push('Members/' + m.id + '/Progress/progress.md(' + m.id + ' 的研究日志)')
5016
- out.push('Members/' + m.id + '/{Propos,Methods,Subproblems}/(' + m.id + ' 的卡片)')
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 + ' 的卡片)')
5017
5133
  }
5018
5134
  out.push('Institutes.md(编制镜像)', 'Shared/TaskBoard.md(任务板镜像)')
5019
5135
  out.push('Paper/' + p.id + '/paper.log.md(本论文流程往来)')
@@ -5496,7 +5612,7 @@ export function apply(ctx) {
5496
5612
  return { ok: true, dismissed: id, reclaimedTasks: reclaimed, reason }
5497
5613
  }
5498
5614
  async function nudge(callerId, o) {
5499
- if (!callerId) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'no calling member' }
5615
+ if (!callerId) return memberDiagnosis('督办(vibe_v5_nudge)', callerId)
5500
5616
  if (!isOffice(callerId) && !(isAcademician(callerId) && params.academicianLeads)) {
5501
5617
  return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can nudge members' }
5502
5618
  }
@@ -6091,7 +6207,7 @@ export function apply(ctx) {
6091
6207
  memberMayRejectAssign: params.memberMayRejectAssign,
6092
6208
  researcherCount: params.researcherCount,
6093
6209
  quorumCap: params.quorumCap, quorumMode: params.quorumMode,
6094
- m: quorumM(), voterCount: voterCount(),
6210
+ m: quorumView().m, voterCount: voterCount(), started: voterCount() > 0,
6095
6211
  verdictMaxRounds: params.verdictMaxRounds,
6096
6212
  maxTempPerMember: params.maxTempPerMember, maxTempTotal: params.maxTempTotal,
6097
6213
  compactThreshold: params.compactThreshold, compactAfterRounds: params.compactAfterRounds,
@@ -6407,7 +6523,7 @@ export function apply(ctx) {
6407
6523
  // failure modes that silently drop state were invisible in the operator's view.
6408
6524
  diagnostics: s.diagnostics || [],
6409
6525
  debug: Object.assign({ scheduling, reschedule }, dbg),
6410
- quorum: { m: quorumM(), mode: params.quorumMode, voters: voters().map((m) => m.id), voterCount: voterCount() },
6526
+ quorum: quorumView(),
6411
6527
  members: s.members.map((m) => ({
6412
6528
  id: m.id, kind: m.kind, phase: m.phase, direction: m.direction, hiredBy: m.hiredBy,
6413
6529
  rounds: rounds.get(m.id) || 0, busy: busy.has(m.id), contextPct: contextPct.get(m.id) || 0,
@@ -6444,12 +6560,15 @@ export function apply(ctx) {
6444
6560
  function report() {
6445
6561
  syncParamsFromState()
6446
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()
6447
6566
  const L = []
6448
6567
  L.push('# 「' + instituteName + '」研究所汇报')
6449
6568
  L.push('')
6450
6569
  L.push('- 项目:' + project + '|阶段:' + phase + '|运行中:' + running + '|已结题:' + autoDone)
6451
6570
  L.push('- 研究对象:' + (s.problem.statement ? s.problem.statement.slice(0, 200) : '(未设定)'))
6452
- L.push('- 求真门槛:m = ' + quorumM() + '(模式 ' + params.quorumMode + ')|有表决权者 ' + voterCount() + ' 人')
6571
+ L.push('- 求真门槛:' + (qv.started ? 'm = ' + qv.m : '未启动(有表决权者 0 人,m 未定义)') + '(模式 ' + params.quorumMode + ')|有表决权者 ' + qv.voterCount + ' 人|rosterVersion ' + qv.rosterVersion)
6453
6572
  L.push('')
6454
6573
  L.push('## 编制')
6455
6574
  if (!s.members.length) L.push('(暂无成员)')
@@ -6521,8 +6640,8 @@ export function apply(ctx) {
6521
6640
  L.push('')
6522
6641
  L.push('## 文件位置')
6523
6642
  L.push('- 根目录:' + instRoot())
6524
- L.push('- 已确立:Verified/|成员库:Members/<id>/|群聊:Shared/Chat/|会议:Shared/Meetings/|辩论:Shared/Debates/')
6525
- 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 }
6526
6645
  }
6527
6646
  // Adding/removing a PERMANENT researcher is a change to the institute's public
6528
6647
  // structure, so members may only propose it; the office decides and executes.
@@ -6594,6 +6713,8 @@ export function apply(ctx) {
6594
6713
  startPaper, paperStatus: paperSummary, finalizePaperByOffice,
6595
6714
  // authorization helpers (used by tool handlers)
6596
6715
  memberIdOfAgent, isOffice, isAcademician, isProvablyOffice, officeCaller, memberById, activeMembers,
6716
+ // diagnosis helpers (defects 1/2 of the architecture self-test)
6717
+ memberDiagnosis, quorumM, quorumView, voterCount,
6597
6718
  }
6598
6719
  }
6599
6720
 
@@ -6788,7 +6909,7 @@ export function apply(ctx) {
6788
6909
  // ── member-facing controls ────────────────────────────────────────────────
6789
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) => {
6790
6911
  const from = s.memberIdOfAgent(x)
6791
- if (!from) return { ok: false, code: 'V5_MEMBER_NOT_FOUND', message: 'no calling member' }
6912
+ if (!from) return s.memberDiagnosis('发言(vibe_v5_say)', from)
6792
6913
  const to = a.to || 'all'
6793
6914
  return s.say(from, { to, text: a.text, kind: to === 'voters' ? 'voters' : (a.to ? 'dm' : 'chat') })
6794
6915
  })