cs-bots 0.1.1 → 0.7.5
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/CHANGELOG.md +539 -0
- package/README.md +65 -10
- package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +364 -0
- package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +284 -0
- package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +567 -0
- package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +569 -0
- package/docs/ARCHITECTURE.md +277 -0
- package/docs/adr/ADR-001-/351/241/265/347/255/276/347/273/223/346/236/204/345/233/236/345/210/260/344/270/203/351/241/265.md +70 -0
- package/docs/steps/step-01-/345/216/237/346/236/266/346/236/204/345/244/215/345/216/237/344/270/216IA/350/257/212/346/226/255.md +80 -0
- package/docs/steps/step-02-/351/241/265/347/255/276/346/224/266/346/225/233/344/270/216/345/215/207/347/272/247/351/200/232/351/201/223.md +51 -0
- package/docs/steps/step-03-/346/236/266/346/236/204/350/265/204/346/226/231/346/210/220/346/226/207.md +16 -0
- package/docs/steps/step-04-/344/270/212/347/272/277/344/270/216/351/252/214/350/257/201.md +46 -0
- package/docs//344/272/244/344/273/230/350/257/264/346/230/216-v0.2.0.md +95 -0
- package/docs//346/223/215/344/275/234/346/211/213/345/206/214-v1.md +509 -0
- package/docs//350/277/201/347/247/273/346/226/271/346/241/210-v1.md +64 -0
- package/docs//351/225/277/346/234/237/350/256/260/345/277/206/346/226/271/346/241/210-v1.md +85 -0
- package/package.json +4 -2
- package/src/admission.js +365 -0
- package/src/api.js +4097 -420
- package/src/board.js +883 -14
- package/src/botprofile.js +190 -3
- package/src/client.js +6596 -1082
- package/src/cycle.js +516 -0
- package/src/delivery-watch.js +255 -0
- package/src/dirbrowse.js +20 -0
- package/src/discipline.js +1188 -0
- package/src/fence.js +142 -0
- package/src/index.js +387 -4
- package/src/kernel.js +636 -24
- package/src/library.js +1224 -11
- package/src/memory.js +617 -37
- package/src/metrics.js +339 -0
- package/src/prompts.js +1318 -0
- package/src/reviewer.js +709 -0
- package/src/room.js +655 -14
- package/src/roster.js +346 -26
- package/src/routing/complexity.js +320 -0
- package/src/routing/hints.js +707 -0
- package/src/scratch.js +477 -0
- package/src/settings.js +97 -0
- package/src/stage.js +263 -0
- package/src/store.js +133 -1
- package/src/subscription.js +571 -0
- package/src/suggest.js +1203 -0
- package/src/teams.js +527 -0
- package/src/todo.js +1301 -0
- package/src/tools.js +734 -10
- package/src/verify.js +356 -0
package/src/admission.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cs-bots Admission Gate —— 房间消息的**确定性准入闸**(改造09 · 借鉴 superagent 的两段式准入)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* 现状:任务订阅房间后,房间里每条消息都会扇出上下文;
|
|
7
|
+
* 要不要因此**唤醒**执行者,只有一个布尔开关 `wakeOnFanout`(默认关)。
|
|
8
|
+
* 那是全有或全无:要么谁都别醒,要么每条消息都醒 —— 后者等于把执行者变成
|
|
9
|
+
* 消息回音壁,一分钟花掉几块钱,还让它迷失在无关闲聊里。
|
|
10
|
+
*
|
|
11
|
+
* superagent 的答案是把准入拆成**两段**(docs/002-authorization.md §Admission):
|
|
12
|
+
* 1. **Gate(确定性、可审计、零模型成本)**:这条消息是
|
|
13
|
+
* `explicit`(明确要你,直接做)/ `inferred`(候选,需要评估)/ `ignore`(不要)。
|
|
14
|
+
* 查表得出,答案固定、可复现。
|
|
15
|
+
* 2. **Assessment(模型判断)**:Gate 判为 `inferred` 的,由模型判断值不值得动手。
|
|
16
|
+
* —— 这一段贵,所以**只在 Gate 放行之后**才发生。
|
|
17
|
+
*
|
|
18
|
+
* 本模块只做第 1 段。第 2 段交给执行者(它本来就是模型)。
|
|
19
|
+
* 于是钱花在「真的相关」的消息上,而不是每条闲聊上。
|
|
20
|
+
*
|
|
21
|
+
* ## 准入表(三值,与 superagent docs/002:58-73 逐格对齐)
|
|
22
|
+
|
|
23
|
+
行 = 接收方是否在名单上;列 = 消息是否点名。
|
|
24
|
+
|
|
25
|
+
| | 点名找你 | 没点名 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| **在名单上** | `explicit`(**不在表里**,直接明确) | `inferred`(缺省) |
|
|
28
|
+
| **不在名单上** | `ignore`(缺省) | `ignore`(缺省) |
|
|
29
|
+
|
|
30
|
+
⚠️ 四个反直觉但关键的点(都有官方出处):
|
|
31
|
+
|
|
32
|
+
1. **不在名单上的人点名你,缺省是 `ignore`。**
|
|
33
|
+
官方原文:`A listed sender who addresses the agent always makes an explicit request`
|
|
34
|
+
——「明确」的前提是**在名单上**。不在名单上就点名,
|
|
35
|
+
默认当没看见;要让他能直接派活,得**把这个格子改成 explicit**(表格的 `unlistedAddressed` 格)。
|
|
36
|
+
放行等于绕过订阅权限 —— 正确做法是让他订阅,而不是让 Gate 开后门。
|
|
37
|
+
|
|
38
|
+
2. **`inferred` 不是「放行」,是「候选」。**
|
|
39
|
+
它意味着上下文可以记,但要不要动手要由评估决定;
|
|
40
|
+
且 `off` 模式下**根本不会**成为候选。
|
|
41
|
+
把「候选」当成「放行」会让无关消息全部进来。
|
|
42
|
+
|
|
43
|
+
3. **「没点名」那两格不是所有来源都开放。**
|
|
44
|
+
官方:`Cells for unaddressed messages are available only where the Source receives every message`
|
|
45
|
+
—— 房间不是「收全部消息」时,未点名的消息**不可能成为候选**。
|
|
46
|
+
本闸用 `receivesEveryMessage` 表达该约束,缺省 false(保守)。
|
|
47
|
+
|
|
48
|
+
4. **机器人永远不能造出 `explicit`。**
|
|
49
|
+
官方:`They cannot make explicit requests or stop Tickets.`
|
|
50
|
+
⇒ 机器人即使在 `unlistedAddressed` 格被设为 explicit,也只算候选。
|
|
51
|
+
|
|
52
|
+
## 三条铁律
|
|
53
|
+
*
|
|
54
|
+
* 1. **纯函数、无副作用**:给定同样的输入,永远同样的输出。审计靠它。
|
|
55
|
+
* 2. **绝不猜**:证据不足一律 `ignore`;「本会准入」只在 `observe` 下作为**记录**出现,
|
|
56
|
+
* 绝不冒充成「已准入」。
|
|
57
|
+
* 3. **reason 必须是人话**:这条消息为什么醒了/为什么没醒,人要能读懂并据此改规则。
|
|
58
|
+
*
|
|
59
|
+
* ## 依赖面
|
|
60
|
+
*
|
|
61
|
+
* node 内置都不用。零依赖(CI 检查 1 覆盖)。
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/** 准入模式。 */
|
|
65
|
+
export const ACCEPT_MODES = Object.freeze(['off', 'observe', 'auto'])
|
|
66
|
+
|
|
67
|
+
/** 缺省模式 —— 保守优先,先观察再放权。 */
|
|
68
|
+
export const DEFAULT_ACCEPT_MODE = 'observe'
|
|
69
|
+
|
|
70
|
+
/** 准入结论(三值,对齐 superagent 的 Admission)。 */
|
|
71
|
+
export const ADMISSIONS = Object.freeze({
|
|
72
|
+
EXPLICIT: 'explicit', // 明确要你 —— 直接做,无需评估
|
|
73
|
+
INFERRED: 'inferred', // 候选 —— 记下上下文,需要评估后才能动手
|
|
74
|
+
IGNORE: 'ignore', // 不相关 —— 连上下文都不记
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 准入表的三个格子(对齐 superagent `src/admission.ts` 的 `ADMISSION_OPTIONS`)。
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ 官方约束:**「没点名」那两格不是所有来源都开放**。
|
|
81
|
+
* `Cells for unaddressed messages are available only where the Source receives every message`
|
|
82
|
+
* —— 也就是说,房间不是「收全部消息」时,没点名的消息**根本不能成为候选**。
|
|
83
|
+
* 这里用 `receivesEveryMessage` 输入表达该约束,缺省 false(保守)。
|
|
84
|
+
*/
|
|
85
|
+
export const ADMISSION_CELLS = Object.freeze({
|
|
86
|
+
LISTED_UNADDRESSED: 'listedUnaddressed',
|
|
87
|
+
UNLISTED_ADDRESSED: 'unlistedAddressed',
|
|
88
|
+
UNLISTED_UNADDRESSED: 'unlistedUnaddressed',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** 每格可选值(官方 ADMISSION_OPTIONS 逐字对齐)。 */
|
|
92
|
+
export const ADMISSION_OPTIONS = Object.freeze({
|
|
93
|
+
listedUnaddressed: Object.freeze(['inferred', 'ignore']),
|
|
94
|
+
unlistedAddressed: Object.freeze(['explicit', 'inferred', 'ignore']),
|
|
95
|
+
unlistedUnaddressed: Object.freeze(['inferred', 'ignore']),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/** 缺省表(官方 ADMISSION_DEFAULTS 逐字对齐)。 */
|
|
99
|
+
export const ADMISSION_DEFAULTS = Object.freeze({
|
|
100
|
+
listedUnaddressed: 'inferred',
|
|
101
|
+
unlistedAddressed: 'ignore',
|
|
102
|
+
unlistedUnaddressed: 'ignore',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
/** 决策来源(审计用:知道是谁拍的板)。 */
|
|
106
|
+
export const DECIDED_BY = Object.freeze({
|
|
107
|
+
LISTED_ADDRESSED: 'listed-addressed', // 名单上的人点名 —— 明确,无需评估
|
|
108
|
+
TABLE_EXPLICIT: 'table-explicit', // 表格把「不在名单但点名」格设为 explicit
|
|
109
|
+
LISTED_UNADDRESSED: 'listed-unaddressed', // 在名单上但没点名 —— 候选
|
|
110
|
+
TABLE_INFERRED: 'table-inferred', // 表格把「不在名单且没点名」格设为 inferred
|
|
111
|
+
MODE_OFF: 'mode-off', // 模式把候选关掉了
|
|
112
|
+
MODE_OBSERVE: 'mode-observe', // 观察模式:记候选但不唤醒
|
|
113
|
+
NOT_ON_LIST: 'not-on-list', // 不在名单上 —— 不相关
|
|
114
|
+
STOPPED: 'stopped', // 订阅已停
|
|
115
|
+
INVALID: 'invalid', // 输入不合法
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 消息是否**点名**找某个接收者。
|
|
120
|
+
*
|
|
121
|
+
* 认三种形状(历史演进过,读侧必须宽容):
|
|
122
|
+
* - `message.to === recipientId`(定向发言)
|
|
123
|
+
* - `message.mentions` 含 `recipientId`
|
|
124
|
+
* - `message.text` 里出现 `@recipientId`(兜底,最弱但常见)
|
|
125
|
+
*
|
|
126
|
+
* @param {object} message 房间消息
|
|
127
|
+
* @param {string} recipientId 接收者(bot 会话 id / 任务 id 皆可)
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function isAddressed(message, recipientId) {
|
|
131
|
+
if (!message || typeof message !== 'object') return false
|
|
132
|
+
if (typeof recipientId !== 'string' || recipientId === '') return false
|
|
133
|
+
if (message.to === recipientId) return true
|
|
134
|
+
if (Array.isArray(message.mentions) && message.mentions.includes(recipientId)) return true
|
|
135
|
+
const text = typeof message.text === 'string' ? message.text : ''
|
|
136
|
+
// `@id` 后必须跟非标识符字符(或结尾),否则 `@bot` 会误命中 `@bot-2`
|
|
137
|
+
if (text) {
|
|
138
|
+
const idx = text.indexOf(`@${recipientId}`)
|
|
139
|
+
if (idx >= 0) {
|
|
140
|
+
const after = text.slice(idx + 1 + recipientId.length)
|
|
141
|
+
if (after === '' || !/[A-Za-z0-9_-]/.test(after[0])) return true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 规则是否匹配这条消息(用于把某个格子设为 inferred/explicit 时加约束)。
|
|
149
|
+
*
|
|
150
|
+
* 规则形状(**只要有一条命中就算命中**):
|
|
151
|
+
* - `{ from: 'bot-xxx' }` —— 发信人
|
|
152
|
+
* - `{ kind: 'notice' }` —— 消息类型
|
|
153
|
+
* - `{ contains: '上线' }` —— 正文包含(大小写不敏感)
|
|
154
|
+
* - `{ regex: '^部署' }` —— 正文正则(**编译失败视为不匹配**)
|
|
155
|
+
*
|
|
156
|
+
* 未知键规则 → 不匹配(严格:认不出的规则不能变成万能放行)。
|
|
157
|
+
*
|
|
158
|
+
* @param {object} message
|
|
159
|
+
* @param {object} rule
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
export function matchesRule(message, rule) {
|
|
163
|
+
if (!message || typeof message !== 'object') return false
|
|
164
|
+
if (!rule || typeof rule !== 'object') return false
|
|
165
|
+
const keys = Object.keys(rule)
|
|
166
|
+
if (keys.length === 0) return false
|
|
167
|
+
const text = typeof message.text === 'string' ? message.text : ''
|
|
168
|
+
for (const key of keys) {
|
|
169
|
+
if (key === 'from') {
|
|
170
|
+
if (message.from !== rule.from) return false
|
|
171
|
+
} else if (key === 'kind') {
|
|
172
|
+
if (message.kind !== rule.kind) return false
|
|
173
|
+
} else if (key === 'contains') {
|
|
174
|
+
const needle = String(rule.contains ?? '')
|
|
175
|
+
if (needle === '' || !text.toLowerCase().includes(needle.toLowerCase())) return false
|
|
176
|
+
} else if (key === 'regex') {
|
|
177
|
+
let re
|
|
178
|
+
try {
|
|
179
|
+
re = new RegExp(String(rule.regex ?? ''), 'i')
|
|
180
|
+
} catch {
|
|
181
|
+
return false // 坏规则 = 不匹配。绝不让手滑变成全放行。
|
|
182
|
+
}
|
|
183
|
+
if (!re.test(text)) return false
|
|
184
|
+
} else {
|
|
185
|
+
return false // 认不出的键:不放行
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 判定一条消息对某个接收者的准入结果。
|
|
193
|
+
*
|
|
194
|
+
* @param {object} input
|
|
195
|
+
* @param {object} input.message 房间消息(`from`/`to`/`kind`/`text`/`mentions`)
|
|
196
|
+
* @param {string} input.recipientId 接收者标识(会话 id 或任务 id)
|
|
197
|
+
* @param {boolean} [input.subscribed] 是否订阅了该房间(在名单上)
|
|
198
|
+
* @param {string} [input.subscriptionState] 订阅状态(`active` 才算在名单上)
|
|
199
|
+
* @param {string} [input.acceptMode] `off` | `observe` | `auto`,缺省 observe
|
|
200
|
+
* @param {object} [input.rules] 该格子上的规则列表(用于把 inferred/explicit 收窄到特定消息)
|
|
201
|
+
* @returns {{
|
|
202
|
+
* admission: 'explicit'|'inferred'|'ignore',
|
|
203
|
+
* decidedBy: string,
|
|
204
|
+
* reason: string,
|
|
205
|
+
* addressed: boolean,
|
|
206
|
+
* onList: boolean,
|
|
207
|
+
* mode: string,
|
|
208
|
+
* wouldAccept: boolean,
|
|
209
|
+
* matchedRules: number,
|
|
210
|
+
* wake: boolean,
|
|
211
|
+
* }}
|
|
212
|
+
*/
|
|
213
|
+
export function decideAdmission(input) {
|
|
214
|
+
const {
|
|
215
|
+
message,
|
|
216
|
+
recipientId,
|
|
217
|
+
subscribed,
|
|
218
|
+
subscriptionState,
|
|
219
|
+
acceptMode,
|
|
220
|
+
table,
|
|
221
|
+
receivesEveryMessage,
|
|
222
|
+
senderIsBot,
|
|
223
|
+
} = input && typeof input === 'object' ? input : {}
|
|
224
|
+
|
|
225
|
+
// 模式归一:不认识的值 **退回 observe**(不是退回 auto)
|
|
226
|
+
const mode = ACCEPT_MODES.includes(acceptMode) ? acceptMode : DEFAULT_ACCEPT_MODE
|
|
227
|
+
|
|
228
|
+
if (!message || typeof message !== 'object') {
|
|
229
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.INVALID, '没有可判定的消息对象 —— 按不相关处理(不猜)', false, false, mode)
|
|
230
|
+
}
|
|
231
|
+
if (typeof recipientId !== 'string' || recipientId.trim() === '') {
|
|
232
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.INVALID, '没有接收者标识 —— 无法判定,按不相关处理', false, false, mode)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 表格归一:非法格子值退回缺省(**绝不退回更宽的 explicit**)
|
|
236
|
+
const cells = {
|
|
237
|
+
listedUnaddressed: pickCell(table, 'listedUnaddressed'),
|
|
238
|
+
unlistedAddressed: pickCell(table, 'unlistedAddressed'),
|
|
239
|
+
unlistedUnaddressed: pickCell(table, 'unlistedUnaddressed'),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 「在名单上」= 订阅存在 **且** 处于 active。停掉的订阅不算在名单上。
|
|
243
|
+
const onList = subscribed === true && (subscriptionState === undefined || subscriptionState === 'active')
|
|
244
|
+
const addressed = isAddressed(message, recipientId)
|
|
245
|
+
|
|
246
|
+
// ① 在名单上 + 点名 —— **这一格不在表里**(官方:`no table cell`)。
|
|
247
|
+
// 官方原文:`A listed sender who addresses the agent always makes an explicit request`
|
|
248
|
+
// ⇒ 三档模式都准入,无需评估。
|
|
249
|
+
if (onList && addressed) {
|
|
250
|
+
return build(ADMISSIONS.EXPLICIT, DECIDED_BY.LISTED_ADDRESSED,
|
|
251
|
+
`名单上的人点名找了 ${recipientId} —— 明确要你,直接做(无需评估)`, true, true, mode)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ② 不在名单上 —— 查表的 unlisted 两格
|
|
255
|
+
if (!onList) {
|
|
256
|
+
if (!addressed) {
|
|
257
|
+
// 未点名 + 不在名单:只有当「收全部消息」且格子为 inferred 才成为候选。
|
|
258
|
+
// 官方:`Cells for unaddressed messages are available only where the Source receives every message`
|
|
259
|
+
if (receivesEveryMessage !== true) {
|
|
260
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.NOT_ON_LIST,
|
|
261
|
+
`没订阅该房间,且该房间不是「收全部消息」—— 未点名的消息不可能是候选`, false, false, mode)
|
|
262
|
+
}
|
|
263
|
+
if (cells.unlistedUnaddressed === 'inferred') {
|
|
264
|
+
return candidateOrIgnore(mode, cells.unlistedUnaddressed,
|
|
265
|
+
`不在名单上、未点名,但该房间收全部消息且格子设为 inferred —— 候选`, false, false, mode)
|
|
266
|
+
}
|
|
267
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.NOT_ON_LIST,
|
|
268
|
+
`不在名单上、未点名 —— 表格该格为 ignore`, false, false, mode)
|
|
269
|
+
}
|
|
270
|
+
// 点名 + 不在名单:官方缺省 ignore;只有把格子改成 explicit 才准入
|
|
271
|
+
if (senderIsBot === true) {
|
|
272
|
+
// 官方:`They cannot make explicit requests` —— 机器人不能造出 explicit
|
|
273
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.NOT_ON_LIST,
|
|
274
|
+
`机器人消息点名找了 ${recipientId},但它没订阅该房间 —— 机器人不能造出 explicit 请求`, true, false, mode)
|
|
275
|
+
}
|
|
276
|
+
if (cells.unlistedAddressed === 'explicit') {
|
|
277
|
+
return build(ADMISSIONS.EXPLICIT, DECIDED_BY.TABLE_EXPLICIT,
|
|
278
|
+
`不在名单上但点名了 ${recipientId},且表格把该格设为 explicit —— 准入(如专用支持群)`, true, false, mode)
|
|
279
|
+
}
|
|
280
|
+
if (cells.unlistedAddressed === 'inferred') {
|
|
281
|
+
return candidateOrIgnore(mode, 'inferred',
|
|
282
|
+
`不在名单上但点名了 ${recipientId},表格该格设为 inferred —— 候选(不是明确)`, true, false, mode)
|
|
283
|
+
}
|
|
284
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.NOT_ON_LIST,
|
|
285
|
+
`消息点名找了 ${recipientId},但它没订阅该房间 —— 被点名不等于有权限收(表格缺省 ignore)`, true, false, mode)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ③ 在名单上但没点名 —— 查 listedUnaddressed 格
|
|
289
|
+
if (!addressed && receivesEveryMessage !== true) {
|
|
290
|
+
// 在名单上也只能收到点名消息 ⇒ 未点名的消息不会成为候选
|
|
291
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.LISTED_UNADDRESSED,
|
|
292
|
+
'在名单上但未点名,且该房间不是「收全部消息」—— 未点名的消息不会成为候选', false, true, mode)
|
|
293
|
+
}
|
|
294
|
+
if (cells.listedUnaddressed === 'ignore') {
|
|
295
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.LISTED_UNADDRESSED,
|
|
296
|
+
'在名单上但未点名,表格该格设为 ignore —— 不相关', false, true, mode)
|
|
297
|
+
}
|
|
298
|
+
return candidateOrIgnore(mode, 'inferred',
|
|
299
|
+
'在名单上但未点名 —— 候选,记下上下文,是否动手由评估决定', false, true, mode)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 候选成立时的收尾:`off` 把候选抹掉;`observe`/`auto` 都产出 `inferred`。
|
|
304
|
+
*
|
|
305
|
+
* ⚠️ 官方语义(docs/002 §Accept mode and rules):
|
|
306
|
+
* - `off`:Nothing inferred becomes a candidate。
|
|
307
|
+
* - `observe`(缺省):Inferred candidates **are assessed**;
|
|
308
|
+
* 正向评估被记为 ignore 并标记 `would accept`,**什么都不接受**。
|
|
309
|
+
* - `auto`:正向评估的候选被接受。
|
|
310
|
+
*
|
|
311
|
+
* 也就是说 `observe` 与 `auto` **都产出候选、都要评估** —— 差别在评估之后。
|
|
312
|
+
* 把 `observe` 做成「不评估」是错的:那样「would accept」永远没有依据。
|
|
313
|
+
* 本闸只负责「是不是候选」;评估与放权是下一段的事。
|
|
314
|
+
*/
|
|
315
|
+
function candidateOrIgnore(mode, cellValue, reason, addressed, onList, m) {
|
|
316
|
+
const modeActual = m ?? mode
|
|
317
|
+
if (modeActual === 'off') {
|
|
318
|
+
return build(ADMISSIONS.IGNORE, DECIDED_BY.MODE_OFF,
|
|
319
|
+
`${reason.replace(/→.*$/, '')}但模式是 off —— 候选不成立(off = 没有候选)`, addressed, onList, modeActual)
|
|
320
|
+
}
|
|
321
|
+
return build(ADMISSIONS.INFERRED, DECIDED_BY.LISTED_UNADDRESSED,
|
|
322
|
+
modeActual === 'observe'
|
|
323
|
+
? `${reason}(观察模式:评估会照做,但正向结论只记为 would accept,不会真的接受)`
|
|
324
|
+
: `${reason}(自动模式:正向评估的候选会被接受)`,
|
|
325
|
+
addressed, onList, modeActual)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** 取格子值:非法值退回缺省(绝不退回更宽的 explicit)。 */
|
|
329
|
+
function pickCell(table, key) {
|
|
330
|
+
const v = table && typeof table === 'object' ? table[key] : undefined
|
|
331
|
+
const allowed = ADMISSION_OPTIONS[key]
|
|
332
|
+
return allowed.includes(v) ? v : ADMISSION_DEFAULTS[key]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** 构造返回形状(保证字段恒存在,调用方不用做 null 判断)。 */
|
|
336
|
+
function build(admission, decidedBy, reason, addressed, onList, mode, extra = {}) {
|
|
337
|
+
const wake = extra.wake === true
|
|
338
|
+
return {
|
|
339
|
+
admission,
|
|
340
|
+
decidedBy,
|
|
341
|
+
reason,
|
|
342
|
+
addressed,
|
|
343
|
+
onList,
|
|
344
|
+
mode,
|
|
345
|
+
// ⚠️ `wouldAccept` 由**评估**阶段决定,Gate 只判「是不是候选」。
|
|
346
|
+
// Gate 阶段恒为 false —— 否则就是把「评估结论」提前伪造出来。
|
|
347
|
+
// (官方:observe 下正向评估记为 ignore 且标 "would accept")
|
|
348
|
+
wouldAccept: extra.wouldAccept === true,
|
|
349
|
+
matchedRules: extra.matchedRules ?? 0,
|
|
350
|
+
wake,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export default {
|
|
355
|
+
ACCEPT_MODES,
|
|
356
|
+
DEFAULT_ACCEPT_MODE,
|
|
357
|
+
ADMISSIONS,
|
|
358
|
+
ADMISSION_CELLS,
|
|
359
|
+
ADMISSION_OPTIONS,
|
|
360
|
+
ADMISSION_DEFAULTS,
|
|
361
|
+
DECIDED_BY,
|
|
362
|
+
isAddressed,
|
|
363
|
+
matchesRule,
|
|
364
|
+
decideAdmission,
|
|
365
|
+
}
|