dsh-vibe-math 2.1.0 → 2.2.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.md +15 -2
- package/RELEASE-NOTES-2.2.0.md +266 -0
- package/audit-v5-integrity.mjs +343 -0
- package/audit-v5-sensitivity.mjs +306 -0
- package/e2e-v5-round2.test.mjs +521 -0
- package/package.json +10 -2
- package/prompt-corpus-v5/prompt-corpus-v5.json +561 -0
- package/prompt-corpus-v5/prompt-corpus-v5.md +3804 -0
- package/prompt-v5-integrity.test.mjs +942 -0
- package/selfdrive-v5.mjs +470 -0
- package/vibe-math-v5/vibe-math-v5.js +377 -140
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +159 -3
package/README.md
CHANGED
|
@@ -64,8 +64,21 @@
|
|
|
64
64
|
- **状态存于会话日志的 host-only 投影单元**(键 `vibeMathV5`),由 DSH 负责 checkpoint/恢复;v4 的 `State/*.json` 直写、损坏静默覆盖、并发丢写、跨进程陈旧快照这一整类问题在构造上被消除。
|
|
65
65
|
- **不引入任何 npm 实验包**:v5 是 preset 内的单个 `.js` 文件。
|
|
66
66
|
- **会议与验证互斥**:验证进行中会议请求会暂存,验证做完再补开。
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
- **提示词正确性是一等公民**(v2.2.0 起):成员**读到的文字**本身有专门的测试与可人工复核的语料。
|
|
68
|
+
身份一律**显式传递、绝不猜测**;成员先落盘进编制、再构造它的入职提示词;章程快照冻结在入职时,
|
|
69
|
+
重建会话不再自称"刚入职";框头按**真实来源**标注(所办分派 ≠ 院士分派;督办 ≠ 分派);
|
|
70
|
+
框架反馈有独立发送者,一次提示词绝不重复投递同一条消息。
|
|
71
|
+
|
|
72
|
+
**提示词与交互语料(v2.2.0 新增,随包发布)**:
|
|
73
|
+
- `prompt-v5-integrity.test.mjs` — 526 条断言,逐条检查框架真正发出的提示词:身份/编制/m/轮次一致性、
|
|
74
|
+
框头署名、回执契约按职位裁剪、无重复投递、无 `?`/`undefined` 垃圾。
|
|
75
|
+
- `prompt-corpus-v5/prompt-corpus-v5.md` / `.json` — **框架真正发出的每一条提示词原文**(入职、会话重建、
|
|
76
|
+
常规轮、心跳轮、表决初评/辩论、会议、会议提议、各类收件框头、框架提示、失败就位),
|
|
77
|
+
工作区路径统一替换为 `<WS>` 以保证可 diff。**复核提示词正确性请看这里**,不必翻会话日志。
|
|
78
|
+
- `audit-v5-sensitivity.mjs` — 29 个探针,每个都故意打破一条不变式并要求对应套件**变红**;
|
|
79
|
+
其中 14 个专打提示词/交互(身份、编制、框头、章程、回执契约、节流)。
|
|
80
|
+
|
|
81
|
+
详见 `vibe-math-v5/实现方案.md`(§14.5 提示词分配的正确性)与 `RELEASE-NOTES-2.2.0.md`。
|
|
69
82
|
|
|
70
83
|
---
|
|
71
84
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# dsh-vibe-math 2.2.0 — 提示词身份错乱的修复 + 提示词完整性基线
|
|
2
|
+
|
|
3
|
+
> 上一版:2.1.0。本版**只动 v5**(v2/v3/v4 未改)。
|
|
4
|
+
> 触发本版的是一个真实事故:v2.1.0 在实地测试中,**每个成员收到的提示词都写错了身份**。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 0. 事故:提示词里的身份是错的
|
|
9
|
+
|
|
10
|
+
2026-09 的一次实地测试(`D:\wd\vibemath测试\项目测试1`)暴露了一个致命缺陷:
|
|
11
|
+
框架发给每个成员的"状态块"里,写的**不是收件人自己**。
|
|
12
|
+
|
|
13
|
+
从真实会话日志里取出的证据(四个创始成员的**入职首轮**提示词):
|
|
14
|
+
|
|
15
|
+
| 该成员实际是谁 | 提示词表头(正确) | `[状态]` 块(错误) |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 院士 | `【入职首轮 —— 院士 acad】` | `你是 ?(常驻研究员)|轮次 0|法定票数 m=1|有表决权者 0 人` |
|
|
18
|
+
| r-1 | `【入职首轮 —— 常驻研究员 r-1】` | `你是 acad(院士)|轮次 1|m=1|有表决权者 1 人` |
|
|
19
|
+
| r-2 | `【入职首轮 —— 常驻研究员 r-2】` | `你是 r-1(常驻研究员)|轮次 1|m=2|有表决权者 2 人` |
|
|
20
|
+
| r-3 | `【入职首轮 —— 常驻研究员 r-3】` | `你是 r-2(常驻研究员)|轮次 1|m=3|有表决权者 3 人` |
|
|
21
|
+
|
|
22
|
+
规律很清楚:**状态块描述的是"上一个被创建的成员",院士则拿到 `?`。**
|
|
23
|
+
|
|
24
|
+
### 0.1 根因
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// 出错前
|
|
28
|
+
function stateBlock() {
|
|
29
|
+
return briefBlock(memberById(currentMember) || activeMembers()[0] || { id: currentMember || '?', ... })
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- 状态块从一个**可变的全局变量** `currentMember`("本会话最近唤醒/创建的成员")取名,
|
|
34
|
+
而不是从"这条提示词要发给谁"取名;
|
|
35
|
+
- 而创建路径 `spawnMember` 把 `currentMember = member.id` 写在 `startContinuable(...)` **之后**——
|
|
36
|
+
于是构造入职提示词的那一刻,`currentMember` 还是**上一个**成员,第一个成员则是空串 → `?`。
|
|
37
|
+
|
|
38
|
+
### 0.2 为什么 123 条断言和 15 个探针都没发现
|
|
39
|
+
|
|
40
|
+
因为这个框架当时**没有任何一条断言检查成员实际读到的文字**。所有测试都在测工具函数的返回值、
|
|
41
|
+
投影状态、文件内容——而 `[状态]` 块只存在于提示词字符串里。
|
|
42
|
+
(更糟的是,当时的灵敏度审计因为工作目录被百分号转义而**根本没能启动被测套件**,
|
|
43
|
+
15 个探针的"全部命中"是假的;见 §2。)
|
|
44
|
+
|
|
45
|
+
### 0.3 连带发现的同类缺陷
|
|
46
|
+
|
|
47
|
+
顺着这条线全面排查"提示词分配 / 成员代号 / 交互内容",又找出 **7 处真实缺陷**:
|
|
48
|
+
|
|
49
|
+
| # | 缺陷 | 后果 |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| 1 | 新成员在**写进编制之前**就构造了它的入职提示词与章程 | 入职成员的 `[在册]` **看不到自己**;`m` 与"有表决权者 N 人"按**加入前**的编制算;章程里"你入职时的在册编制"漏掉自己 |
|
|
52
|
+
| 2 | 入职称呼被 `resume` 复用 | 会话重建的成员被告知"**你刚刚加入本所**",并被要求重新给出初始见解 |
|
|
53
|
+
| 3 | 章程里的领头人**硬编码 `acad`**,且不随编制变化 | 以 `academician:false` 建所时,章程仍然声称存在院士、要求成员"向院士汇报"、承诺"院士可以给你派活"——**指向一个不存在的人** |
|
|
54
|
+
| 4 | `memberIdOfAgent` 对"不是成员子会话"的调用者一律回退到 `currentMember` | **所办(会话根代理/人)调用 `vibe_v5_assign` 被解析成某位研究员**并以 `V5_NOT_ACADEMICIAN` 拒绝;所办的分派/督办会被署上别人的名字 |
|
|
55
|
+
| 5 | 框架给成员的反馈写成"**该成员发给该成员**" | `say()` 以 `V5_SELF_MESSAGE` 拒绝,于是"认领失败""verdict 必须是 0–1 的数值"这些反馈**永远送不到成员**,静默失效 |
|
|
56
|
+
| 6 | 一次唤醒的提示词里同一条消息**出现两遍** | 先构造好带 `[新到的消息/通知]` 的轮次提示词,再在其前面又拼一遍收件箱 —— 成员每条新消息读两遍 |
|
|
57
|
+
| 7 | `assign` 框头恒为 `【院士分派】`;`nudge` 复用 `assign` 的 kind 且正文恒为"院士督办";`voters` 广播被降级成"私信" | 所办分派伪装成院士分派;督办看起来像分派;只给表决者的广播让收件人以为被单独点名 |
|
|
58
|
+
|
|
59
|
+
此外还修了一处**调度**问题:任务板分支在成员每轮结束时**立刻**重新唤醒同一个任务所有者,
|
|
60
|
+
而每轮回复又会驱动一次调度 pass —— 一个未完成任务会变成
|
|
61
|
+
"唤醒 → 回合 → 唤醒"的**无界链**,任何参数都拦不住、`pause` 也插不进去。
|
|
62
|
+
现在该分支按 `activityTimeoutMs` 节流;空闲窗口到期时,**拥有进行中任务的成员收到的是常规工作轮**
|
|
63
|
+
(状态块里写着"我的任务"),而不是泛泛的心跳轮。
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 1. 修复内容
|
|
68
|
+
|
|
69
|
+
### 1.1 身份:显式传递,绝不猜测
|
|
70
|
+
|
|
71
|
+
- 每个提示词构造函数都接收**它所描述的那个成员**;`briefBlock(member)` 直接用它。
|
|
72
|
+
- `briefBlock` 对"没给成员"**显式失败**(新错误码 `V5_INTERNAL`),而不是退回"猜一个"。
|
|
73
|
+
猜测正是本缺陷的成因。
|
|
74
|
+
- 提示词里不再使用 `currentMember` / `activeMembers()[0]` 之类的兜底。
|
|
75
|
+
- `spawnMember` 的顺序改为**硬性**:① 以 `phase='active'` 提交进投影 → ② 置 busy →
|
|
76
|
+
③ 构造 persona 与入职提示词 → ④ 建目录、写编制镜像 → ⑤ 最后才 `startContinuable`。
|
|
77
|
+
|
|
78
|
+
**修复后的实际输出**(语料实测,`quorumCap=3`,4 名创始成员):
|
|
79
|
+
|
|
80
|
+
| 成员 | `[状态]` | `[在册]` | m | 有表决权者 |
|
|
81
|
+
|---|---|---|---|---|
|
|
82
|
+
| acad | 你是 acad(院士)|轮次 1 | acad | 1 | 1 |
|
|
83
|
+
| r-1 | 你是 r-1(常驻研究员)|轮次 1 | acad、r-1 | 2 | 2 |
|
|
84
|
+
| r-2 | 你是 r-2(常驻研究员)|轮次 1 | acad、r-1、r-2 | 3 | 3 |
|
|
85
|
+
| r-3 | 你是 r-3(常驻研究员)|轮次 1 | acad、r-1、r-2、r-3 | 3 | 4 |
|
|
86
|
+
|
|
87
|
+
### 1.2 章程:快照冻结、与真实编制一致
|
|
88
|
+
|
|
89
|
+
- 章程(persona)在**入职时冻结**,`resume` 重建会话时**原样复用**——
|
|
90
|
+
否则"你入职时的在册编制"这句话就变成假的。权威编制始终是每轮状态块(章程里已注明)。
|
|
91
|
+
- 领头人不再硬编码,取**实际在册的院士代号**;`academician:false` 时章程改为
|
|
92
|
+
"本所当前**没有在册院士**:组织与协调由全体有表决权者共同商议",并删除所有
|
|
93
|
+
"向院士汇报/院士分派/院士派活"的承诺。
|
|
94
|
+
- 章程 §五 注明 m 会随编制变化,以状态块为准。
|
|
95
|
+
|
|
96
|
+
### 1.3 会话重建 ≠ 入职
|
|
97
|
+
|
|
98
|
+
`resume` 重建的子会话现在框为 `【会话重建 —— <职位> <代号>】`,
|
|
99
|
+
并要求"先读回你自己的 Progress/ 与成果库……**不要从头再来**"。
|
|
100
|
+
|
|
101
|
+
### 1.4 身份归属:所办不再被误判成成员
|
|
102
|
+
|
|
103
|
+
调用者身份的判定改为**推导**:
|
|
104
|
+
|
|
105
|
+
- 是某成员的子会话 → 该成员;
|
|
106
|
+
- 是**会话根代理**(所办/人)→ `office`;
|
|
107
|
+
- 两者都不是 → "无成员"(成员专属写操作以 `V5_MEMBER_NOT_FOUND` 拒绝,这正是"无需猜测"的前提);
|
|
108
|
+
- 只有在完全没有会话 id 的合成上下文里,才允许回退到"最近唤醒的成员",且要求该成员真实存在。
|
|
109
|
+
|
|
110
|
+
### 1.5 框头:按真实来源标注
|
|
111
|
+
|
|
112
|
+
| kind | 框头 |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `chat` | `【研究所·群聊】<from>:…` |
|
|
115
|
+
| `dm` | `【研究所·私信 from <from>】` |
|
|
116
|
+
| `voters` | `【研究所·致全体表决者 from <from>】`(会议提议、临时工参考意见、反对分派走这条) |
|
|
117
|
+
| `office` | `【所办通知】` |
|
|
118
|
+
| `assign` | `【院士分派】` 或 **`【所办分派】`**(按 `from` 判定) |
|
|
119
|
+
| `nudge` | `【督办 from <from>】`,正文区分"院士督办/所办督办" |
|
|
120
|
+
| `notice` | `【框架提示】`(框架发给单个成员的反馈) |
|
|
121
|
+
|
|
122
|
+
### 1.6 反馈真的能送达
|
|
123
|
+
|
|
124
|
+
新增框架专属发送者 `framework`:`say('framework', { to: memberId, kind: 'notice', … })`。
|
|
125
|
+
反馈属紧急邮件,会在该成员下一次被唤醒时立即随提示词送达。现在这些动作**未生效时会回执**:
|
|
126
|
+
认领不存在的任务、`task_update` 版本过期、`verdict` 不是 0–1 的数值、提议验证/开会未受理。
|
|
127
|
+
|
|
128
|
+
### 1.7 一次提示词只投递一条消息
|
|
129
|
+
|
|
130
|
+
**先 ack 邮件、再构造本轮提示词**:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
pending = pendingFor(member) // ①
|
|
134
|
+
inbox = composeInbox(pending) // ②
|
|
135
|
+
ackPending(pending) // ③ 先销账
|
|
136
|
+
base = normalPrompt(member) // ④ 此时 pendingFor 已为空
|
|
137
|
+
prompt = inbox + base
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 1.8 回执契约补全
|
|
141
|
+
|
|
142
|
+
`replySpec` 原先**没有列出**框架实际处理的三个字段,等于把通道藏起来:
|
|
143
|
+
`reject_assign`(对分派的异议,理由会被广播)、`task_done`(直接报任务完成)、
|
|
144
|
+
`input`(会议/辩论轮的发言正文)。现已补上,并按职位裁剪
|
|
145
|
+
(临时工没有 `verdict`/`hire`/`fire`;非院士没有 `assign`/`prioritize`/`nudge`/`convene_meeting`)。
|
|
146
|
+
|
|
147
|
+
### 1.9 未就位者可见
|
|
148
|
+
|
|
149
|
+
`[在册]` 之外新增 `[未就位] <代号>(failed)`,把 provisioning 失败/被标记 failed 的成员
|
|
150
|
+
暴露给全所——此前它们被静默略去,整所不知道编制少了人。
|
|
151
|
+
|
|
152
|
+
### 1.10 编制镜像不再重复列人
|
|
153
|
+
|
|
154
|
+
`Institutes.md` 原先把已除名成员同时列在主表和"已除名"小节里。现在主表只列在册/未就位者。
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 2. 测试基线的重建(本次最重要的交付物)
|
|
159
|
+
|
|
160
|
+
### 2.1 新增:提示词完整性套件 `prompt-v5-integrity.test.mjs`
|
|
161
|
+
|
|
162
|
+
**526 条断言**,把提示词本身当作产品来测。13 个互相隔离的用例(各自独立的会话根,
|
|
163
|
+
互不污染),覆盖入职(含临时工)、会话重建、常规轮、心跳轮、表决初评、表决辩论、会议、
|
|
164
|
+
会议提议、各类收件框头、框架提示、失败就位、无院士建所、并发提议排队、会外表决停工、调度节流。
|
|
165
|
+
|
|
166
|
+
对**每一条**捕获到的提示词逐项断言:
|
|
167
|
+
|
|
168
|
+
1. `[状态]` 里的身份 = 提示词实际发给的成员 = 表头里的身份 = 随行人设指向的资料库路径 `Members/<id>/`;
|
|
169
|
+
2. `[在册]` 必含读者自身且不含重复;`有表决权者 N 人` 与 `[在册]` 中的表决者数一致;`m = min(quorumCap, N)`;
|
|
170
|
+
3. 表头轮次 = 状态块轮次;`[未就位]` 的成员不得同时出现在 `[在册]`;
|
|
171
|
+
4. 全文不含 `?` / `undefined` / `NaN` / `[object Object]`;一次提示词只有一个 `[状态]` 块、
|
|
172
|
+
一个收件箱块,且**同一条消息不出现两次**;
|
|
173
|
+
5. 表头职位 = `[状态]` 职位;
|
|
174
|
+
6. 回执契约按职位裁剪,且框架实际处理的字段**全部**出现;
|
|
175
|
+
7. 领袖叙事与实际编制一致(无院士时不得出现任何院士承诺)。
|
|
176
|
+
|
|
177
|
+
### 2.2 新增:可人工复核的提示词语料(随包发布)
|
|
178
|
+
|
|
179
|
+
`prompt-corpus-v5/prompt-corpus-v5.md` 与 `.json`:**框架真正发出的每一条提示词原文**,
|
|
180
|
+
由上述套件每次运行时重写。59 条 × 22 种,含完整人设(按成员只打印一次)。
|
|
181
|
+
工作区路径统一替换为 `<WS>`,因此内容是**确定性的、可 diff 的**。
|
|
182
|
+
|
|
183
|
+
> 这就是本框架对"提示词/交互正确性"的人工复核入口:任何
|
|
184
|
+
> "成员代号/职位/在册名单/交互署名"的问题都能在这里一眼看出,不必去翻会话日志。
|
|
185
|
+
|
|
186
|
+
### 2.3 修复:灵敏度审计此前是**假绿**
|
|
187
|
+
|
|
188
|
+
`audit-v5-sensitivity.mjs` 用 `new URL('.', import.meta.url).pathname` 取工作目录——
|
|
189
|
+
在含中文的 Windows 路径上这返回**百分号转义**的路径(`D:\wd\vibemath%E5%BC%80...`)。
|
|
190
|
+
该目录不存在,于是 `spawnSync` 全部失败、子进程 `status` 为 `null`,
|
|
191
|
+
"非零退出 = 套件变红"的判定把**启动失败**当成了**探测成功**。
|
|
192
|
+
——上一版宣称的"15 探针 0 盲点"是假的。
|
|
193
|
+
|
|
194
|
+
同一时期 `e2e-v5-round2.test.mjs` 还**完全不读 `V5_PLUGIN`**,
|
|
195
|
+
所以针对它的探针跑的是**未被变异**的插件,恒为绿。
|
|
196
|
+
|
|
197
|
+
修正后(`fileURLToPath` + 所有套件都读 `V5_PLUGIN`),并逐一检查每个变异是否真的
|
|
198
|
+
**改变行为**(相当一部分守卫是互相遮蔽的,删除任一条其实是**语义惰性**的,
|
|
199
|
+
不能拿来做探针),现在的真实结果是:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
sensitivity: 29 probes detected the break, 0 blind spots
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
其中 **14 个专打提示词/交互**:状态块错认身份、入职成员缺席自己的编制、同一条消息投递两次、
|
|
206
|
+
章程被 resume 重写、会话重建被框成入职、会议提议被误署、所办被当成成员、
|
|
207
|
+
所办分派伪装成院士分派、督办伪装成分派、框架反馈被自己挡掉、失败就位者被隐藏、
|
|
208
|
+
无院士章程凭空造领袖、回执契约藏起异议通道、任务所有者的无界重唤醒。
|
|
209
|
+
|
|
210
|
+
### 2.4 修复:`selfdrive-v5.mjs` 的表决段落是空转的
|
|
211
|
+
|
|
212
|
+
它用 `drainWakes(4)` 驱动一轮投票,但队列里还有心跳/工作/邮件唤醒,
|
|
213
|
+
预算常常在**四个表决者都投完之前**就用完 —— 于是"此时还没定论"这类断言
|
|
214
|
+
**无条件成立**,无论门槛规则被破坏成什么样(探针已证明:把 m 门槛削弱到 1,套件仍然全绿)。
|
|
215
|
+
现在改为 `drainVerifyRound(4)`:按**提示词种类**取唤醒,直到**恰好 4 张表决票**答完为止,
|
|
216
|
+
并新增 `stage === 'debate'` 的断言确认这一轮真的走完了。71 → **75 条断言**。
|
|
217
|
+
|
|
218
|
+
### 2.5 静态审计新增提示词门禁与语料门禁
|
|
219
|
+
|
|
220
|
+
`audit-v5-integrity.mjs` 的"理念门禁"从 11 条扩到 **31 条**,其中 20 条专门守提示词/交互的
|
|
221
|
+
结构修复(状态块显式接收成员、未知身份显式失败、先落盘后构造、章程冻结、重建框头、
|
|
222
|
+
所办解析、框头来源、反馈发送者、未就位可见、回执契约字段、节流……)。
|
|
223
|
+
另外新增 3 项**交付物门禁**:提示词套件、语料 Markdown、语料 JSON 必须随包存在,
|
|
224
|
+
且语料必须覆盖 22 种交互、不得含 `你是 ?` 这类错身份行。当前:**clean**。
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 3. 验收
|
|
229
|
+
|
|
230
|
+
| 套件 | 结果 |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `prompt-v5-integrity.test.mjs` | **526 断言**(新增) |
|
|
233
|
+
| `selfdrive-v5.mjs` | **75 断言**(+4,表决段落改为真正完成整轮) |
|
|
234
|
+
| `e2e-v5-round2.test.mjs` | **53 断言**(并修正为读 `V5_PLUGIN`) |
|
|
235
|
+
| `audit-v5-sensitivity.mjs` | **29 探针全部命中 / 0 盲点**(此前为假绿) |
|
|
236
|
+
| `audit-v5-integrity.mjs` | **clean**(31 条理念门禁 + 3 项交付物门禁 + 语料校验) |
|
|
237
|
+
| 其余回归 | v4 selfdrive、v4-fixes、v3、v3-roundtrip、multisession、business、regression、d9-d13、8 个历史审计套件、installer e2e —— 全绿 |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 4. 已发布产物内的自证
|
|
242
|
+
|
|
243
|
+
- 提示词语料随包发布,可直接对照 §0 的事故证据:语料里**不存在** `你是 ?`,
|
|
244
|
+
且 `[状态] 你是 acad(院士)`、`[状态] 你是 r-3(常驻研究员)` 等行由套件断言保证。
|
|
245
|
+
- 提示词套件与语料都属于 `package.json` 的 `files`,因此 `npm pack` 之后即可复核。
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 5. 已知边界(未变)
|
|
250
|
+
|
|
251
|
+
- `[状态]`/章程的正确性由**提示词套件 + 语料**保证;它们验证的是**框架生成的文本**,
|
|
252
|
+
不是模型对该文本的解读。真机冒烟(在 GUI 里开一个 Vibe Math V5 会话、确认 `vibe_v5_*` 工具出现)
|
|
253
|
+
仍是最后一步。
|
|
254
|
+
- 一个"回合永不结束"的成员仍不会被强行释放(与 v4 同一边界);心跳总会重武装,
|
|
255
|
+
因此调度器不会永久冻结。
|
|
256
|
+
- `resume` 后 `rounds` 计数(内存态)从 1 重新开始;这是设计使然(该计数只用于节流与压缩,
|
|
257
|
+
权威进度在成员自己的 `Progress/`)。
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 6. 升级建议
|
|
262
|
+
|
|
263
|
+
- 直接升级:`npm i dsh-vibe-math@latest`(或让 bundle 安装器自动更新),然后**重启**会话。
|
|
264
|
+
- 已经用 v2.1.0 跑过 v5 的研究所:成员**正在使用的 persona** 是入职时冻结的旧章程,
|
|
265
|
+
里面可能有"向 acad 汇报"之类的旧措辞;如需让新章程生效,请结束该研究所并在新会话里重开。
|
|
266
|
+
持久状态(投影)与文件树**不需要迁移**。
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// V5 INTEGRITY AUDIT — static self-check of vibe-math-v5.js against the ways this
|
|
3
|
+
// kind of single-file plugin actually breaks:
|
|
4
|
+
// 1. a function CALLED but never defined (ReferenceError at runtime only)
|
|
5
|
+
// 2. a `params.X` read for a key that DEFAULT_PARAMS never declares (silent undefined)
|
|
6
|
+
// 3. a tool handler calling `s.NAME(...)` on the session API object that the session
|
|
7
|
+
// never returns (TypeError only when that tool is used)
|
|
8
|
+
// 4. documented error codes that are never raised, and raised codes never documented
|
|
9
|
+
// 5. leftover development markers / TODO scaffolding
|
|
10
|
+
// Run: node audit-v5-integrity.mjs (exit 1 on any finding)
|
|
11
|
+
// ============================================================
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
13
|
+
|
|
14
|
+
const FILE = new URL('./vibe-math-v5/vibe-math-v5.js', import.meta.url)
|
|
15
|
+
const raw = readFileSync(FILE, 'utf8')
|
|
16
|
+
// Strip comments and string literals before any identifier scan. Without this the
|
|
17
|
+
// heuristic matches English words inside comments that merely precede a '(' (e.g.
|
|
18
|
+
// "// per unit (" becomes a phantom call to unit()), drowning the real findings.
|
|
19
|
+
function stripNoise(s) {
|
|
20
|
+
let out = ''
|
|
21
|
+
let i = 0
|
|
22
|
+
const n = s.length
|
|
23
|
+
while (i < n) {
|
|
24
|
+
const c = s[i], c2 = s[i + 1]
|
|
25
|
+
if (c === '/' && c2 === '/') { while (i < n && s[i] !== '\n') i++; continue }
|
|
26
|
+
if (c === '/' && c2 === '*') { i += 2; while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++; i += 2; continue }
|
|
27
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
28
|
+
const q = c
|
|
29
|
+
i++
|
|
30
|
+
while (i < n) {
|
|
31
|
+
if (s[i] === '\\') { i += 2; continue }
|
|
32
|
+
if (s[i] === q) { i++; break }
|
|
33
|
+
if (q !== '`' && s[i] === '\n') break
|
|
34
|
+
i++
|
|
35
|
+
}
|
|
36
|
+
out += q + q // keep a placeholder so `X: ''` shape survives
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
out += c
|
|
40
|
+
i++
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
const src = stripNoise(raw)
|
|
45
|
+
const findings = []
|
|
46
|
+
const notes = []
|
|
47
|
+
|
|
48
|
+
const lineOf = (idx) => src.slice(0, idx).split('\n').length
|
|
49
|
+
|
|
50
|
+
// ---- 1. called-but-undefined -------------------------------------------
|
|
51
|
+
const defined = new Set()
|
|
52
|
+
for (const m of src.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)) defined.add(m[1])
|
|
53
|
+
for (const m of src.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g)) defined.add(m[1])
|
|
54
|
+
for (const m of src.matchAll(/(?:const|let|var)\s+\{([^}]+)\}\s*=/g)) {
|
|
55
|
+
for (const part of m[1].split(',')) { const n = part.split(':').pop().trim(); if (n) defined.add(n) }
|
|
56
|
+
}
|
|
57
|
+
for (const m of src.matchAll(/catch\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) defined.add(m[1])
|
|
58
|
+
// Function/method PARAMETERS are locally bound identifiers too, not free calls.
|
|
59
|
+
for (const m of src.matchAll(/function\s*[A-Za-z_$]*\s*\(([^)]*)\)/g)) {
|
|
60
|
+
for (const raw of m[1].split(',')) {
|
|
61
|
+
const name = raw.trim().replace(/[={].*$/s, '').trim()
|
|
62
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) defined.add(name)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const m of src.matchAll(/\(([^()]*)\)\s*=>/g)) {
|
|
66
|
+
for (const raw of m[1].split(',')) {
|
|
67
|
+
const name = raw.trim().replace(/[={].*$/s, '').trim()
|
|
68
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) defined.add(name)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const m of src.matchAll(/([A-Za-z_$][\w$]*)\s*=>/g)) defined.add(m[1])
|
|
72
|
+
// JS/DSH globals that are legitimately free
|
|
73
|
+
const GLOBALS = new Set([
|
|
74
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'function', 'new', 'await', 'delete', 'void', 'do', 'else', 'try',
|
|
75
|
+
'Array', 'Object', 'JSON', 'Number', 'String', 'Boolean', 'Math', 'Date', 'Promise', 'Set', 'Map', 'WeakRef', 'WeakMap', 'Symbol', 'Error',
|
|
76
|
+
'isFinite', 'isNaN', 'parseInt', 'parseFloat', 'encodeURIComponent', 'decodeURIComponent', 'structuredClone',
|
|
77
|
+
'AbortSignal', 'console', 'require', 'import', 'super', 'this', 'of', 'in', 'instanceof',
|
|
78
|
+
'RegExp', 'Proxy', 'Reflect', 'AggregateError', 'TextEncoder', 'TextDecoder', 'URL', 'BigInt', 'Intl', 'Buffer', 'process',
|
|
79
|
+
])
|
|
80
|
+
const LOCAL_METHODS = new Set()
|
|
81
|
+
for (const m of src.matchAll(/[{,]\s*([A-Za-z_$][\w$]*)\s*[:(]/g)) LOCAL_METHODS.add(m[1])
|
|
82
|
+
for (const m of src.matchAll(/\.\s*([A-Za-z_$][\w$]*)\s*\(/g)) LOCAL_METHODS.add(m[1])
|
|
83
|
+
|
|
84
|
+
const called = new Map()
|
|
85
|
+
for (const m of src.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
86
|
+
const name = m[1]
|
|
87
|
+
if (GLOBALS.has(name) || LOCAL_METHODS.has(name)) continue
|
|
88
|
+
if (!called.has(name)) called.set(name, { n: 0, line: lineOf(m.index) })
|
|
89
|
+
called.get(name).n++
|
|
90
|
+
}
|
|
91
|
+
for (const [name, info] of called) {
|
|
92
|
+
if (!defined.has(name)) findings.push('line ' + info.line + ': called but never defined: ' + name + '() (' + info.n + ' call site(s))')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- 2. params keys ----------------------------------------------------
|
|
96
|
+
const dpBlock = /const DEFAULT_PARAMS = \{([\s\S]*?)\n \}/.exec(src)
|
|
97
|
+
if (!dpBlock) findings.push('could not locate DEFAULT_PARAMS')
|
|
98
|
+
else {
|
|
99
|
+
const declared = new Set()
|
|
100
|
+
for (const m of dpBlock[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)) declared.add(m[1])
|
|
101
|
+
const used = new Map()
|
|
102
|
+
for (const m of src.matchAll(/\bparams\.([A-Za-z_$][\w$]*)/g)) used.set(m[1], (used.get(m[1]) || 0) + 1)
|
|
103
|
+
for (const [k, n] of used) {
|
|
104
|
+
if (!declared.has(k)) findings.push('params.' + k + ' read but not declared in DEFAULT_PARAMS (' + n + ' use(s))')
|
|
105
|
+
}
|
|
106
|
+
notes.push('DEFAULT_PARAMS keys: ' + declared.size + '; params.* reads: ' + used.size)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- 3. session-API surface used by tool handlers -----------------------
|
|
110
|
+
// The returned API object is the last `return { ... }` inside makeSession.
|
|
111
|
+
const apiStart = src.lastIndexOf(' return {\n sessionId,')
|
|
112
|
+
if (apiStart === -1) findings.push('could not locate the session API return object')
|
|
113
|
+
else {
|
|
114
|
+
const apiEnd = src.indexOf('\n }\n }', apiStart)
|
|
115
|
+
const apiText = src.slice(apiStart, apiEnd === -1 ? apiStart + 4000 : apiEnd)
|
|
116
|
+
const apiKeys = new Set()
|
|
117
|
+
for (const m of apiText.matchAll(/(?:^|[\s{,])([A-Za-z_$][\w$]*)\s*:/g)) apiKeys.add(m[1])
|
|
118
|
+
for (const m of apiText.matchAll(/(?:^|[\s{,])([A-Za-z_$][\w$]*)\s*,/g)) apiKeys.add(m[1])
|
|
119
|
+
const usedOnS = new Map()
|
|
120
|
+
for (const m of src.matchAll(/(?<![\w$.])s\.([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
121
|
+
if (!usedOnS.has(m[1])) usedOnS.set(m[1], lineOf(m.index))
|
|
122
|
+
}
|
|
123
|
+
for (const [k, line] of usedOnS) {
|
|
124
|
+
if (!apiKeys.has(k)) findings.push('line ' + line + ': tool handler calls s.' + k + '() but the session API does not export it')
|
|
125
|
+
}
|
|
126
|
+
notes.push('session API keys: ' + apiKeys.size + '; s.*() called: ' + usedOnS.size)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- 4. error codes ----------------------------------------------------
|
|
130
|
+
// Scan the RAW source: these patterns live inside string literals, which `src` strips.
|
|
131
|
+
const raised = new Set()
|
|
132
|
+
for (const m of raw.matchAll(/v5err\(\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
133
|
+
for (const m of raw.matchAll(/code:\s*'(V5_[A-Z_]+)'/g)) raised.add(m[1])
|
|
134
|
+
const docs = readFileSync(new URL('./vibe-math-v5/实现方案.md', import.meta.url), 'utf8')
|
|
135
|
+
const documented = new Set()
|
|
136
|
+
for (const m of docs.matchAll(/`(V5_[A-Z_]+)`/g)) documented.add(m[1])
|
|
137
|
+
const onlyDocs = new Set()
|
|
138
|
+
for (const m of docs.matchAll(/(V5_[A-Z_]+)/g)) onlyDocs.add(m[1])
|
|
139
|
+
const raisedNotDocumented = [...raised].filter(c => !onlyDocs.has(c))
|
|
140
|
+
const documentedNotRaised = [...documented].filter(c => !raised.has(c))
|
|
141
|
+
if (raisedNotDocumented.length) findings.push('error codes raised but absent from 实现方案.md: ' + raisedNotDocumented.join(', '))
|
|
142
|
+
if (documentedNotRaised.length) notes.push('documented but not raised in code (ok if advisory): ' + documentedNotRaised.join(', '))
|
|
143
|
+
notes.push('error codes raised: ' + raised.size)
|
|
144
|
+
|
|
145
|
+
// ---- 5. leftover scaffolding -------------------------------------------
|
|
146
|
+
for (const bad of ['@@V5_SECTION@@', 'TODO', 'FIXME', 'XXX', 'PLACEHOLDER']) {
|
|
147
|
+
if (src.includes(bad)) findings.push('leftover development marker: ' + bad)
|
|
148
|
+
}
|
|
149
|
+
// every event type declared in EV must be appended somewhere
|
|
150
|
+
const evBlock = /const EV = \{([\s\S]*?)\n\}/.exec(raw)
|
|
151
|
+
if (evBlock) {
|
|
152
|
+
const keys = [...evBlock[1].matchAll(/([A-Za-z]+):\s*'(vibe5\/[a-z]+)'/g)]
|
|
153
|
+
for (const [, prop, literal] of keys) {
|
|
154
|
+
const uses = (raw.match(new RegExp("EV\\." + prop + "\\b", 'g')) || []).length
|
|
155
|
+
if (uses <= 1) findings.push('event type EV.' + prop + ' (' + literal + ') is declared but never committed')
|
|
156
|
+
}
|
|
157
|
+
notes.push('event types declared: ' + keys.length)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- 6. composition sanity --------------------------------------------
|
|
161
|
+
// The v5 preset is a composition, and a typo in a row `name` is a mounting failure that
|
|
162
|
+
// no unit test of the plugin can catch (the mock calls apply() directly and never goes
|
|
163
|
+
// through the loader). v4 is a proven-good composition mounted on the same hosts, so any
|
|
164
|
+
// package row v5 names that v4 does not is either a genuine new dependency or a typo —
|
|
165
|
+
// and a new dependency must be justified, so surface it for review.
|
|
166
|
+
function packageRows(yaml) {
|
|
167
|
+
const rows = []
|
|
168
|
+
const lines = yaml.split('\n')
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const idm = /^\s*-\s*id:\s*(\S+)\s*$/.exec(lines[i])
|
|
171
|
+
if (!idm) continue
|
|
172
|
+
const row = { id: idm[1], name: '', disabled: false, line: i + 1 }
|
|
173
|
+
for (let j = i + 1; j < lines.length && j < i + 12; j++) {
|
|
174
|
+
if (/^\s*-\s*id:\s*\S+\s*$/.test(lines[j])) break
|
|
175
|
+
const nm = /^\s*name:\s*'?([^'\n]+?)'?\s*$/.exec(lines[j])
|
|
176
|
+
if (nm && !row.name) row.name = nm[1].trim()
|
|
177
|
+
if (/^\s*disabled:\s*true/.test(lines[j])) row.disabled = true
|
|
178
|
+
}
|
|
179
|
+
if (row.name) rows.push(row)
|
|
180
|
+
}
|
|
181
|
+
return rows
|
|
182
|
+
}
|
|
183
|
+
const v5yaml = readFileSync(new URL('./vibe-math-v5/agent.cordis.yml', import.meta.url), 'utf8')
|
|
184
|
+
const v4yaml = readFileSync(new URL('./vibe-math-v4/agent.cordis.yml', import.meta.url), 'utf8')
|
|
185
|
+
const v5rows = packageRows(v5yaml)
|
|
186
|
+
const v4names = new Set(packageRows(v4yaml).map(r => r.name))
|
|
187
|
+
const RELATIVE_OK = new Set(['./vibe-math-v5.js'])
|
|
188
|
+
if (!v5rows.length) findings.push('composition: no rows parsed out of agent.cordis.yml (is the file still YAML?)')
|
|
189
|
+
for (const r of v5rows) {
|
|
190
|
+
if (r.name === 'cordis:group') continue
|
|
191
|
+
if (r.name.startsWith('./')) {
|
|
192
|
+
if (!RELATIVE_OK.has(r.name)) findings.push('composition line ' + r.line + ': relative row name "' + r.name + '" has no matching file in the preset directory')
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if (!v4names.has(r.name)) findings.push('composition line ' + r.line + ': row "' + r.id + '" names "' + r.name + '", which v4 does not — verify the package/name is correct')
|
|
196
|
+
}
|
|
197
|
+
// the preset must reference its own plugin row, and that file must exist
|
|
198
|
+
if (!v5rows.some(r => r.name === './vibe-math-v5.js')) findings.push('composition: the preset never mounts ./vibe-math-v5.js')
|
|
199
|
+
// prefix AND text are required on the persona row for the DSH schema and for back-compat
|
|
200
|
+
const personaBlock = /- id:\s*persona[\s\S]*?(?=\n-\s*id:)/.exec(v5yaml)
|
|
201
|
+
if (!personaBlock) findings.push('composition: no persona row found')
|
|
202
|
+
else {
|
|
203
|
+
if (!/^\s*prefix:\s*\|/m.test(personaBlock[0])) findings.push('composition: persona row lacks the required `prefix` key')
|
|
204
|
+
if (!/^\s*text:\s*\|/m.test(personaBlock[0])) findings.push('composition: persona row lacks the legacy `text` key')
|
|
205
|
+
}
|
|
206
|
+
notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5rows.filter(r => r.name !== 'cordis:group' && !r.name.startsWith('./') && !v4names.has(r.name)).length)
|
|
207
|
+
|
|
208
|
+
// ---- 7. requirements traceability against the plan ---------------------
|
|
209
|
+
// "No missing logic" is only checkable mechanically if the SPEC is machine-readable.
|
|
210
|
+
// The plan's §15 names every tool and §16 every parameter, so compare those sets
|
|
211
|
+
// against the implementation: a name in the plan but not the code is an unimplemented
|
|
212
|
+
// requirement, and a name in the code but not the plan is undocumented surface.
|
|
213
|
+
{
|
|
214
|
+
const plan = readFileSync(new URL('./vibe-math-v5/实现方案.md', import.meta.url), 'utf8')
|
|
215
|
+
const planTools = new Set()
|
|
216
|
+
for (const m of plan.matchAll(/\bvibe_v5_[a-z_]+/g)) planTools.add(m[0])
|
|
217
|
+
const codeTools = new Set()
|
|
218
|
+
for (const m of raw.matchAll(/registerTool\(\s*'(vibe_v5_[a-z_]+)'/g)) codeTools.add(m[1])
|
|
219
|
+
// documented-but-wildcarded placeholders are not real tools
|
|
220
|
+
const IGNORE = new Set(['vibe_v5_', 'vibe_v5_record_'])
|
|
221
|
+
for (const t of planTools) {
|
|
222
|
+
if (IGNORE.has(t)) continue
|
|
223
|
+
if (!codeTools.has(t)) findings.push('plan names tool ' + t + ' but the plugin never registers it')
|
|
224
|
+
}
|
|
225
|
+
const undocumented = [...codeTools].filter(t => !planTools.has(t))
|
|
226
|
+
if (undocumented.length) notes.push('tools registered but not named in the plan: ' + undocumented.join(', '))
|
|
227
|
+
notes.push('plan tools: ' + planTools.size + '; registered tools: ' + codeTools.size)
|
|
228
|
+
|
|
229
|
+
// Parameters: only the §16 default table, NOT the §15 tool tables (whose rows also
|
|
230
|
+
// begin with a backticked name).
|
|
231
|
+
const declared2 = new Set()
|
|
232
|
+
if (dpBlock) for (const m of dpBlock[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)) declared2.add(m[1])
|
|
233
|
+
const sec16 = /##\s*16\.[\s\S]*?(?=\n##\s*17\.)/.exec(plan)
|
|
234
|
+
const paramRows = new Set()
|
|
235
|
+
if (sec16) {
|
|
236
|
+
for (const m of sec16[0].matchAll(/^\|\s*`([A-Za-z][\w]*)`(?:\s*\/\s*`([A-Za-z][\w]*)`)?\s*\|/gm)) {
|
|
237
|
+
paramRows.add(m[1])
|
|
238
|
+
if (m[2]) paramRows.add(m[2])
|
|
239
|
+
}
|
|
240
|
+
} else findings.push('could not locate the plan\'s §16 parameter table')
|
|
241
|
+
for (const p of paramRows) {
|
|
242
|
+
if (!declared2.has(p)) findings.push('plan documents parameter `' + p + '` but DEFAULT_PARAMS does not declare it')
|
|
243
|
+
}
|
|
244
|
+
notes.push('plan §16 parameter rows: ' + paramRows.size)
|
|
245
|
+
|
|
246
|
+
// The plan's philosophy is enforced by concrete gates; assert the load-bearing ones
|
|
247
|
+
// still exist so a future edit cannot quietly drop a guard.
|
|
248
|
+
const GATES = [
|
|
249
|
+
['temp workers cannot vote', "if (m.kind === 'temp') return // no vote"],
|
|
250
|
+
['temp workers cannot hire', "if (caller.kind === 'temp') return { ok: false, code: 'V5_NOT_VOTER'"],
|
|
251
|
+
['only the academician assigns', "code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks'"],
|
|
252
|
+
['only the academician sets priorities', "code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can set priorities'"],
|
|
253
|
+
['permanent-staff changes need the office', "code: 'V5_NOT_ACADEMICIAN', message: '解聘常驻研究员只能向所办提议,由所办批准(成员不能直接执行)'"],
|
|
254
|
+
['assignment objections are broadcast', 'if (p.reject_assign && typeof p.reject_assign === \'object\' && params.memberMayRejectAssign)'],
|
|
255
|
+
['the academician has no extra vote weight', 'const E = voters().map((m) => m.id)'],
|
|
256
|
+
['members may reject an assignment', "memberMayRejectAssign: true,"],
|
|
257
|
+
['the charter states the progress definition', "' · Progress/<你>/progress.md —— **你的研究日志**(叙述体,可追加)。'"],
|
|
258
|
+
['the charter describes the academician as organizer', "' 【四、你的组织职责与边界(院士)】'"],
|
|
259
|
+
['the framework never assigns on its own', "agenda: '本所较长时间没有新进展。请你们自行讨论:现在最该推进的是什么?谁来做?是否需要发起验证?'"],
|
|
260
|
+
|
|
261
|
+
// ── PROMPT / INTERACTION CORRECTNESS GATES ─────────────────────────────
|
|
262
|
+
// The 2026-09 field test shipped a framework whose every member brief named the WRONG
|
|
263
|
+
// member. These gates keep the structural fixes in place, and the companion
|
|
264
|
+
// prompt-v5-integrity.test.mjs asserts the TEXT those fixes produce.
|
|
265
|
+
['the status block takes the member it describes', 'function briefBlock(member) {'],
|
|
266
|
+
['an unknown identity fails loudly instead of being guessed', "throw v5err('V5_INTERNAL', 'briefBlock: a member is required"],
|
|
267
|
+
['the status block is built from that member, never a global', 'function stateBlock(member) {\n return briefBlock(member)\n }'],
|
|
268
|
+
['the joiner is committed to the roster BEFORE its brief is built', "member.phase = 'active'\n member.childId = ''\n await putMember(member)"],
|
|
269
|
+
['the charter is frozen at hire and reused on resume', 'const persona = member.persona || memberPersona(member)'],
|
|
270
|
+
['a rebuilt session is framed as a rebuild', "const resume = mode === 'resume'"],
|
|
271
|
+
['leadership text follows the live roster', 'function academicianId() {'],
|
|
272
|
+
['no charter invents a leader when there is none', "本所当前**没有在册院士**"],
|
|
273
|
+
['the office resolves as the office, never as a guessed member', "try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }"],
|
|
274
|
+
['the mailbox is acked BEFORE the round prompt is built', 'if (pending.length) await ackPending(pending)\n const base = typeof baseFn === \'function\' ? baseFn() : baseFn'],
|
|
275
|
+
['framework feedback has its own sender (never a self-message)', "return await say('framework', { to: memberId, kind: 'notice', text: String(text) })"],
|
|
276
|
+
['an assignment is framed by its true origin', "if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text"],
|
|
277
|
+
['a nudge is framed as supervision, not as an assignment', "to, kind: 'nudge',"],
|
|
278
|
+
['a voters-only broadcast is framed as such', "if (m.kind === 'voters') return '【研究所·致全体表决者 from ' + m.from + '】' + m.text"],
|
|
279
|
+
['relayed messages carry their true sender', "await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })"],
|
|
280
|
+
['a member that failed to provision stays visible', "b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))"],
|
|
281
|
+
['the objection channel is documented in the reply spec', '"reject_assign": {"task_id":"t-3"'],
|
|
282
|
+
['the task-done channel is documented in the reply spec', '"task_done": "t-3",'],
|
|
283
|
+
['the meeting input channel is documented in the reply spec', '"input": "本轮会议/辩论的发言正文'],
|
|
284
|
+
['the work push is paced, not an unbounded loop', 'if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue'],
|
|
285
|
+
]
|
|
286
|
+
for (const [label, needle] of GATES) {
|
|
287
|
+
if (!raw.includes(needle)) findings.push('philosophy gate missing from the implementation: ' + label)
|
|
288
|
+
}
|
|
289
|
+
notes.push('philosophy gates checked: ' + GATES.length)
|
|
290
|
+
|
|
291
|
+
// The prompt corpus is a SHIPPED deliverable, not a build artifact: a human must be able
|
|
292
|
+
// to read the exact text every member receives without decoding session logs.
|
|
293
|
+
const REPO_ROOT = new URL('.', import.meta.url)
|
|
294
|
+
const needFiles = [
|
|
295
|
+
['the prompt-integrity suite is shipped', 'prompt-v5-integrity.test.mjs'],
|
|
296
|
+
['the machine-readable prompt corpus is shipped', 'prompt-corpus-v5/prompt-corpus-v5.json'],
|
|
297
|
+
['the human-readable prompt corpus is shipped', 'prompt-corpus-v5/prompt-corpus-v5.md'],
|
|
298
|
+
]
|
|
299
|
+
for (const [label, rel] of needFiles) {
|
|
300
|
+
let ok = false
|
|
301
|
+
try { ok = existsSync(new URL(rel, REPO_ROOT)) } catch (e) { ok = false }
|
|
302
|
+
if (!ok) findings.push('missing shipped prompt-correctness artifact: ' + label + ' (' + rel + ')')
|
|
303
|
+
}
|
|
304
|
+
notes.push('prompt-correctness artifacts checked: ' + needFiles.length)
|
|
305
|
+
|
|
306
|
+
// The corpus must actually contain the interactions a reviewer needs to see, and must
|
|
307
|
+
// never contain a wrong-identity brief.
|
|
308
|
+
try {
|
|
309
|
+
const corpusPath = new URL('prompt-corpus-v5/prompt-corpus-v5.json', REPO_ROOT)
|
|
310
|
+
const c = JSON.parse(readFileSync(corpusPath, 'utf8'))
|
|
311
|
+
const kinds = new Set((c.prompts || []).map(p => p.kind))
|
|
312
|
+
const need = ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
|
|
313
|
+
'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
|
|
314
|
+
'inbox-office', 'inbox-assign', 'inbox-nudge', 'inbox-office-assign', 'inbox-office-nudge',
|
|
315
|
+
'notice', 'notice-claim', 'after-failure']
|
|
316
|
+
for (const k of need) if (!kinds.has(k)) findings.push('the prompt corpus is missing a ' + k + ' prompt')
|
|
317
|
+
const all = (c.prompts || []).map(p => p.prompt + '\n' + (p.charter || '')).join('\n')
|
|
318
|
+
if (/你是 \?/.test(all)) findings.push('the prompt corpus contains a wrong-identity "你是 ?" brief')
|
|
319
|
+
if (/\[状态\][^\n]*你是\s+(\S+?)[^\n]*\n/.test(all)) {
|
|
320
|
+
// every [状态] line must name a real member id, never a placeholder
|
|
321
|
+
for (const m of all.match(/\[状态\][^\n]*/g) || []) {
|
|
322
|
+
const id = (/\[状态\]\s*你是\s+(\S+?)(/.exec(m) || [])[1]
|
|
323
|
+
if (!id || id === '?' || id === 'undefined') findings.push('the prompt corpus has a bad identity line: ' + m.slice(0, 60))
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
notes.push('prompt corpus: ' + (c.total || 0) + ' prompts, ' + kinds.size + ' kinds')
|
|
327
|
+
} catch (e) {
|
|
328
|
+
findings.push('the prompt corpus could not be read/parsed: ' + String((e && e.message) || e))
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---- report ------------------------------------------------------------
|
|
333
|
+
console.log('-- V5 integrity audit --')
|
|
334
|
+
for (const n of notes) console.log(' note: ' + n)
|
|
335
|
+
console.log('')
|
|
336
|
+
if (findings.length) {
|
|
337
|
+
for (const f of findings) console.error(' FINDING: ' + f)
|
|
338
|
+
console.error('')
|
|
339
|
+
console.error(findings.length + ' finding(s)')
|
|
340
|
+
process.exit(1)
|
|
341
|
+
}
|
|
342
|
+
console.log('clean: no undefined calls, no undeclared params keys, no missing session API, no leftover markers')
|
|
343
|
+
process.exit(0)
|