@kira905/dsh-memory-toolkit 0.1.3 → 0.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/CHANGELOG.md +92 -0
- package/README.en.md +225 -2
- package/README.md +180 -2
- package/lib/deploy-guard.mjs +463 -275
- package/lib/guard-http.mjs +59 -62
- package/lib/pitfall-form-map.mjs +94 -84
- package/lib/pitfall-score.mjs +262 -185
- package/lib/session-file-name.mjs +116 -100
- package/lib/zstd-frames.mjs +88 -67
- package/memory-db-tools/README.md +29 -1
- package/memory-db-tools/audit-analyze.mjs +1 -1
- package/memory-db-tools/audit-export.mjs +60 -16
- package/memory-db-tools/audit-struct-probe.mjs +29 -8
- package/memory-db-tools/lib/known-devices.mjs +137 -0
- package/memory-engine/README.md +58 -4
- package/memory-engine/bench/bench.mjs +1 -1
- package/memory-engine/cli/band-sentinel.mjs +164 -0
- package/memory-engine/cli/distill-rollback.mjs +112 -44
- package/memory-engine/cli/distill-sessions.mjs +237 -0
- package/memory-engine/cli/distill.mjs +196 -95
- package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
- package/memory-engine/cli/doc-index.mjs +47 -38
- package/memory-engine/cli/domain-tags.mjs +246 -162
- package/memory-engine/cli/layer-migrate.mjs +176 -0
- package/memory-engine/cli/layer-move.mjs +189 -0
- package/memory-engine/cli/pitfall-hook.mjs +136 -70
- package/memory-engine/cli/recall.mjs +243 -110
- package/memory-engine/cli/selfcheck.mjs +93 -44
- package/memory-engine/cli/serve-start.mjs +221 -140
- package/memory-engine/cli/sweep.mjs +83 -44
- package/memory-engine/cli/sync.mjs +93 -57
- package/memory-engine/config/audit-devices.json +6 -0
- package/memory-engine/config/bands.json +7 -0
- package/memory-engine/config/layers.json +7 -0
- package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
- package/memory-engine/dict/lexicon-config.json +11 -0
- package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
- package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
- package/memory-engine/lib/band-sentinel.mjs +417 -0
- package/memory-engine/lib/cli-strict.mjs +128 -0
- package/memory-engine/lib/cli-util.mjs +147 -125
- package/memory-engine/lib/config.mjs +83 -96
- package/memory-engine/lib/dirty-ledger.mjs +127 -94
- package/memory-engine/lib/distill-rollback.mjs +173 -133
- package/memory-engine/lib/distill.mjs +878 -585
- package/memory-engine/lib/doc-index-store.mjs +208 -235
- package/memory-engine/lib/doc-index.mjs +351 -350
- package/memory-engine/lib/doc-scan.mjs +178 -156
- package/memory-engine/lib/doc-summarize.mjs +220 -205
- package/memory-engine/lib/domain-tags.mjs +358 -277
- package/memory-engine/lib/embedder.mjs +175 -194
- package/memory-engine/lib/hashes.mjs +22 -7
- package/memory-engine/lib/layer-migrate.mjs +330 -0
- package/memory-engine/lib/layer-move.mjs +307 -0
- package/memory-engine/lib/layers.mjs +191 -0
- package/memory-engine/lib/lexicon-recall.mjs +473 -0
- package/memory-engine/lib/memory-source.mjs +78 -80
- package/memory-engine/lib/pitfall-source.mjs +115 -114
- package/memory-engine/lib/policy-source.mjs +279 -261
- package/memory-engine/lib/recall-log.mjs +285 -0
- package/memory-engine/lib/recall.mjs +565 -240
- package/memory-engine/lib/selfcheck.mjs +142 -106
- package/memory-engine/lib/session-distill.mjs +603 -0
- package/memory-engine/lib/session-source.mjs +421 -0
- package/memory-engine/lib/sweep.mjs +107 -130
- package/memory-engine/lib/sync.mjs +110 -140
- package/memory-engine/lib/tick-hook.mjs +123 -148
- package/memory-engine/lib/vectors-store.mjs +213 -188
- package/memory-engine/test/12-layering.test.mjs +539 -0
- package/memory-engine/test/13-session-distill.test.mjs +616 -0
- package/memory-engine/tools/bridge-smoke.mjs +94 -55
- package/memory-engine/tools/fetch-model.mjs +167 -87
- package/memory-engine/tools/smoke-recall.mjs +94 -38
- package/memory-engine/tools/verify-recall-chain.mjs +413 -0
- package/package.json +1 -1
package/lib/guard-http.mjs
CHANGED
|
@@ -1,93 +1,90 @@
|
|
|
1
|
-
//
|
|
2
|
-
// guard-http.mjs · 守护鱼/分身「HTTP 存活判据」单一真源(H1 · 2026-09-22)
|
|
3
|
-
// ------------------------------------------------------------
|
|
4
|
-
// ⚠️ 本模块**纯函数、零依赖、零 IO**(不 import 任何东西、不碰网络/进程/文件):
|
|
5
|
-
// 传输层(curl 调用形态)仍留在各消费者里(guard.mjs / repair.mjs),
|
|
6
|
-
// 本模块只回答一个问题 —— **"这个 HTTP 响应算不算'服务活着'"**。
|
|
1
|
+
// HTTP「还活着吗」的判据 —— 纯函数,零依赖,不读盘、不联网、不起进程。
|
|
7
2
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// `http://127.0.0.1:<port>/` → **401**(带 token → 303 落 cookie)。
|
|
11
|
-
// 旧判据 = 「必须 HTTP 200」(`(r.stdout||'').trim() === '200'`)⇒ 401 被判
|
|
12
|
-
// "服务没起来" ⇒ KILL → 拉起 → 再杀 的死循环:主包一升 0.1.5,守护鱼就会
|
|
13
|
-
// 把生产 dsh 打进 KILL/CRASH。
|
|
3
|
+
// 它只回答一个问题:一次探测拿回来的状态码,能不能当作"服务在监听"。
|
|
4
|
+
// 传输怎么探测(用 curl 还是内置 http、超时给多少)留在各自的调用方,本件不插嘴。
|
|
14
5
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// │ 2xx (200/204) 活 原有唯一判活值,必须保持不变(0.1.1 线 200)
|
|
19
|
-
// │ 3xx (301/303) 活 0.1.5 带 token 访问 `/` = 303(重定向落 cookie);
|
|
20
|
-
// │ 重定向 = 应用在正常处理请求,不是"没起来"
|
|
21
|
-
// │ 401 活 **本次改造核心**:token 认证门 —— 服务在监听,
|
|
22
|
-
// │ 并主动拒绝未认证请求(响应体 "dsh web authentication required")
|
|
23
|
-
// │ 403 活 同属"授权类拒绝":HTTP 服务确实在应答,只是不给内容;
|
|
24
|
-
// │ 与 401 同类(认得出的是"HTTP 服务在",不是"内容对不对")
|
|
25
|
-
// │ 404 等其它 4xx 死 0.1.1 与 0.1.5 的 `/` 都不会 404 ⇒ 出现 404 说明 3080 上的
|
|
26
|
-
// │ 监听者不是 dsh 本体(如端口被别的进程占了)⇒ 正需要"卡死→重启";
|
|
27
|
-
// │ 取择 = 与旧口径一致(宁可误杀重拉,也不把"不是 dsh"当健康)
|
|
28
|
-
// │ 5xx 死 服务进程在但应用层报错/未就绪:保留旧"再探 → 必要时重启"语义,
|
|
29
|
-
// │ 并避免 START 阶段在 bundle 未就绪时过早判 OK
|
|
30
|
-
// │ 无应答/超时 死 与旧口径等价(curl 无输出/000/失败)
|
|
31
|
-
// │ 解析不了(abc) 死 fail-closed:宁可多探一轮,不把未知当健康
|
|
32
|
-
// └
|
|
6
|
+
// 为什么不能只认 200:带 cookie token 认证的构建,匿名访问根路径会回 401 ——
|
|
7
|
+
// 那恰恰证明 HTTP 层还站着、还在应答、只是拒绝没凭证的请求。若判据是"必须 200",
|
|
8
|
+
// 一个健康的进程会被当成死掉,杀死、拉起、再杀死,越修越坏。
|
|
33
9
|
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
10
|
+
// 定下的口径(按顺序取第一条命中的):
|
|
11
|
+
// 2xx 活 —— 最老的口径,必须继续认
|
|
12
|
+
// 3xx 活 —— 认证通过后落到 cookie 的跳转;重定向本身就是应用在处理请求
|
|
13
|
+
// 401 / 403 活 —— 授权类拒绝,说明"有个 HTTP 服务在答话"
|
|
14
|
+
// 404 不活 —— 正常部署下根路径不该 404;出现了说明占着端口的是别的东西
|
|
15
|
+
// 其它 4xx 不活 —— 与老口径保持一致:宁可多探一轮重拉,也不把陌生应答当健康
|
|
16
|
+
// 5xx 不活 —— 进程在但应用层还没就绪,保留"再探、必要时重启"
|
|
17
|
+
// 000 / 非数字 不活 —— 无应答、超时、解析不了,一律不算活(未知不当健康)
|
|
18
|
+
//
|
|
19
|
+
// 状态码判定只写在这里:调用方拿 judgeHttp 的结果,别在业务代码里再写一遍整数比较。
|
|
39
20
|
|
|
40
|
-
/**
|
|
21
|
+
/** 口径版本:写进日志与报告,事后能对上"当时跑的是哪一版判据" */
|
|
41
22
|
export const RULE_VERSION = 'h1-20260922';
|
|
42
23
|
|
|
43
|
-
/**
|
|
24
|
+
/** 判活的状态码集合(对外公布的一份,供文档与别的工具复用) */
|
|
44
25
|
export const ALIVE_CODES = [200, 204, 301, 302, 303, 307, 308, 401, 403];
|
|
45
26
|
|
|
46
|
-
/**
|
|
27
|
+
/** 判活规则的一句话说明(报告 / 日志 / 帮助文本共用) */
|
|
47
28
|
export const ALIVE_RULE_TEXT = '2xx/3xx/401/403 = 活;404 等其它 4xx、5xx、无应答 = 不活';
|
|
48
29
|
|
|
30
|
+
const DIGITS_3 = /^\d{3}$/;
|
|
31
|
+
|
|
32
|
+
// 判定表:自上而下取第一条命中的。顺序本身是语义 —— 401/403/404 必须排在
|
|
33
|
+
// "其它 4xx" 之前,否则永远轮不到它们。
|
|
34
|
+
const VERDICT_TABLE = [
|
|
35
|
+
{ reason: 'ok-2xx', alive: true, accept: (code) => code >= 200 && code <= 299 },
|
|
36
|
+
{ reason: 'redirect-3xx', alive: true, accept: (code) => code >= 300 && code <= 399 },
|
|
37
|
+
{ reason: 'auth-401', alive: true, accept: (code) => code === 401 },
|
|
38
|
+
{ reason: 'auth-403', alive: true, accept: (code) => code === 403 },
|
|
39
|
+
{ reason: 'not-found-404', alive: false, accept: (code) => code === 404 },
|
|
40
|
+
{ reason: 'client-4xx', alive: false, accept: (code) => code >= 400 && code <= 499 },
|
|
41
|
+
{ reason: 'server-5xx', alive: false, accept: (code) => code >= 500 && code <= 599 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** 统一的结果形状 —— 四个字段恒定,不按分支增减 */
|
|
45
|
+
function report(code, alive, reason) {
|
|
46
|
+
return { code, alive, reason, rule: RULE_VERSION };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* 解析探测命令打印出来的状态码。
|
|
51
|
+
* 只认严格的三个数字:无应答时的 `000` 会解析成 0,`abc`、空串、`20x` 这类一律 null。
|
|
52
52
|
* @param {unknown} raw
|
|
53
|
-
* @returns {number|null}
|
|
53
|
+
* @returns {number|null}
|
|
54
54
|
*/
|
|
55
55
|
export function parseHttpCode(raw) {
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
return Number(
|
|
56
|
+
const text = String(raw ?? '').trim();
|
|
57
|
+
if (!DIGITS_3.test(text)) return null;
|
|
58
|
+
return Number(text);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
* @param {unknown} raw
|
|
62
|
+
* 判活并给出理由(消费者只需要这一个入口)。
|
|
63
|
+
* @param {unknown} raw 探测输出里的状态码原串
|
|
64
64
|
* @returns {{ code: number|null, alive: boolean, reason: string, rule: string }}
|
|
65
65
|
* reason ∈ ok-2xx | redirect-3xx | auth-401 | auth-403 | not-found-404 |
|
|
66
66
|
* client-4xx | server-5xx | no-answer | unparsable
|
|
67
67
|
*/
|
|
68
68
|
export function judgeHttp(raw) {
|
|
69
69
|
const code = parseHttpCode(raw);
|
|
70
|
-
if (code === null) return
|
|
71
|
-
if (code === 0) return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (code >= 400 && code <= 499) return { code, alive: false, reason: 'client-4xx', rule: RULE_VERSION };
|
|
78
|
-
if (code >= 500 && code <= 599) return { code, alive: false, reason: 'server-5xx', rule: RULE_VERSION };
|
|
79
|
-
return { code, alive: false, reason: 'unparsable', rule: RULE_VERSION };
|
|
70
|
+
if (code === null) return report(null, false, 'unparsable');
|
|
71
|
+
if (code === 0) return report(0, false, 'no-answer');
|
|
72
|
+
for (const row of VERDICT_TABLE) {
|
|
73
|
+
if (row.accept(code)) return report(code, row.alive, row.reason);
|
|
74
|
+
}
|
|
75
|
+
// 三位数字但落在 1xx / 6xx 之后:没见过的区间,交给"未知不算活"
|
|
76
|
+
return report(code, false, 'unparsable');
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
/**
|
|
79
|
+
/** 只要一个布尔值(与老的探测函数返回形状对齐,调用方改一行就能接上) */
|
|
83
80
|
export function isHttpAlive(raw) {
|
|
84
81
|
return judgeHttp(raw).alive;
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
/**
|
|
84
|
+
/** 一行日志描述:`code=401 (auth-401)` / `no-answer (code=000)` / `unparsable` */
|
|
88
85
|
export function describeHttp(raw) {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
92
|
-
return `code=${
|
|
86
|
+
const judged = judgeHttp(raw);
|
|
87
|
+
if (judged.code === null) return judged.reason;
|
|
88
|
+
if (judged.code === 0) return 'no-answer (code=000)';
|
|
89
|
+
return `code=${judged.code} (${judged.reason})`;
|
|
93
90
|
}
|
package/lib/pitfall-form-map.mjs
CHANGED
|
@@ -1,130 +1,140 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 错误码 / 签名 → 建议形态:查表在前,启发式在后。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// 一个坑反复踩,就得替它挑一个"以后怎么才能不再踩"的落点,形态只有三种:
|
|
4
|
+
// check —— 能脚本化判定的,做成自动检查项(最优:坑不再有第二次机会)
|
|
5
|
+
// skill —— 只在动手那一刻要看到的,内联进相关技能(加载即见)
|
|
6
|
+
// persona —— 真正全局、反复踩的铁律级,才值得动 persona
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// ③ 都没命中 → 回落 `pitfall-score.mjs` 的原启发式(行为与改动前完全一致)
|
|
8
|
+
// 为什么不能全靠文本特征猜:成因确定的坑,文本里往往只留下表象。举例来说,
|
|
9
|
+
// "写保护区没先走审计"这种坑的文本里会带一串路径,按文本猜出来就成了"路径存在性预检",
|
|
10
|
+
// 方向完全错了。已知错误码的坑,成因是确定的 —— 先让表说话;表里没有,再交回启发式。
|
|
12
11
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// 判定顺序(命中即返回,不往下走):
|
|
13
|
+
// ① 错误码:pitfall.code → sigKey 的第 2 段 → normText 里出现的 SCREAMING_SNAKE
|
|
14
|
+
// ② 签名正则:没有稳定错误码、只能靠文本特征认的坑走这条
|
|
15
|
+
// ③ 都没命中 → 返回 null,由调用方回落到原来的启发式
|
|
15
16
|
//
|
|
16
|
-
//
|
|
17
|
+
// 本件是纯数据 + 纯函数:不读状态、不写盘、没有副作用。
|
|
18
|
+
// 加一个坑只需要加一条规则(列表里加一项就行),面板与渲染侧不用改。
|
|
17
19
|
|
|
18
20
|
export const FORM_MAP_VERSION = '0.1.0';
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
// 规则工厂:按固定字段顺序拼出规则对象,缺的可选字段不占位(保持对象形状干净)
|
|
23
|
+
function rule({ id, codes, sigRe, form, why }) {
|
|
24
|
+
const built = { id };
|
|
25
|
+
if (codes) built.codes = codes;
|
|
26
|
+
if (sigRe) built.sigRe = sigRe;
|
|
27
|
+
built.form = form;
|
|
28
|
+
built.why = why;
|
|
29
|
+
return built;
|
|
30
|
+
}
|
|
28
31
|
|
|
29
|
-
/** @type {FormRule[]} */
|
|
30
32
|
export const FORM_RULES = [
|
|
31
|
-
{
|
|
32
|
-
id: 'audit-not-passed',
|
|
33
|
-
codes: ['AUDIT_NOT_PASSED'],
|
|
34
|
-
form: 'skill',
|
|
33
|
+
rule({
|
|
34
|
+
id: 'audit-not-passed', codes: ['AUDIT_NOT_PASSED'], form: 'skill',
|
|
35
35
|
why: '流程类(不是路径问题):写 persona / 技能 / 核心启动链前必须「先落临时文件 → butler-audit-cli 审计 → 用 write 写同一内容」;建议内联进 persona-maintain / P5 写入守门人相关技能',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: 'audit-hash-mismatch',
|
|
39
|
-
codes: ['AUDIT_HASH_MISMATCH'],
|
|
40
|
-
form: 'skill',
|
|
36
|
+
}),
|
|
37
|
+
rule({
|
|
38
|
+
id: 'audit-hash-mismatch', codes: ['AUDIT_HASH_MISMATCH'], form: 'skill',
|
|
41
39
|
why: '流程类:审计凭证与被写内容 hash 绑定——一处一审计、改完再验 hash,staged 内容必须与写入内容逐字节一致;建议内联进 persona-maintain 技能',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'fs-not-observed',
|
|
45
|
-
codes: ['FS_NOT_OBSERVED'],
|
|
46
|
-
form: 'persona',
|
|
40
|
+
}),
|
|
41
|
+
rule({
|
|
42
|
+
id: 'fs-not-observed', codes: ['FS_NOT_OBSERVED'], form: 'persona',
|
|
47
43
|
why: '铁律级(全局、高频):改/覆盖已存在文件前必须先 read;已进 persona【铁律·edit/write 必须先读】,此处不再新增检查项',
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: 'fs-stale-version',
|
|
51
|
-
codes: ['FS_STALE_VERSION'],
|
|
52
|
-
form: 'persona',
|
|
44
|
+
}),
|
|
45
|
+
rule({
|
|
46
|
+
id: 'fs-stale-version', codes: ['FS_STALE_VERSION'], form: 'persona',
|
|
53
47
|
why: '铁律级(全局):junction 路径与同文件多处修改必须重读后**串行 edit**(并行 edit 会互相刷新 mtime);已进 persona 对应铁律',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 'search-failed',
|
|
57
|
-
codes: ['SEARCH_FAILED'],
|
|
58
|
-
form: 'check',
|
|
48
|
+
}),
|
|
49
|
+
rule({
|
|
50
|
+
id: 'search-failed', codes: ['SEARCH_FAILED'], form: 'check',
|
|
59
51
|
why: '路径假设类:rg/grep/glob 目标不存在或不可达,可做成"搜索前先验路径存在"的只读检查项',
|
|
60
|
-
},
|
|
52
|
+
}),
|
|
61
53
|
];
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
for (const
|
|
66
|
-
for (const
|
|
55
|
+
// 码 → 规则(大小写不敏感;后注册者覆盖前者,与表内顺序一致)
|
|
56
|
+
const RULE_BY_CODE = new Map();
|
|
57
|
+
for (const entry of FORM_RULES) {
|
|
58
|
+
for (const code of entry.codes ?? []) RULE_BY_CODE.set(String(code).toUpperCase(), entry);
|
|
67
59
|
}
|
|
68
60
|
|
|
69
|
-
/** SCREAMING_SNAKE
|
|
61
|
+
/** SCREAMING_SNAKE 形态的错误码(至少一个下划线 —— 免得把 FsError 这类类名当码) */
|
|
70
62
|
export const ERROR_CODE_RE = /\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
const
|
|
64
|
+
// 拿不到 code 时签名里写的占位,不是真错误码
|
|
65
|
+
const PLACEHOLDER_CODES = new Set(['NO_CODE']);
|
|
66
|
+
|
|
67
|
+
// 单个码的"像不像码"判据(提取与查表口径一致:大写 + 恰好的形态)
|
|
68
|
+
const CODE_SHAPE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
69
|
+
|
|
70
|
+
function asCode(value) {
|
|
71
|
+
const upper = String(value ?? '').toUpperCase().trim();
|
|
72
|
+
if (!upper) return null;
|
|
73
|
+
if (PLACEHOLDER_CODES.has(upper)) return null;
|
|
74
|
+
return CODE_SHAPE.test(upper) ? upper : null;
|
|
75
|
+
}
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
|
-
*
|
|
78
|
+
* 按优先级把这条坑里出现的错误码捞出来。
|
|
77
79
|
* @param {object} pitfall
|
|
78
|
-
* @returns {string[]}
|
|
80
|
+
* @returns {string[]} 去重后的候选码,越靠前越权威
|
|
79
81
|
*/
|
|
80
82
|
export function extractErrorCodes(pitfall) {
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
if (!/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(s)) return;
|
|
86
|
-
if (!out.includes(s)) out.push(s);
|
|
83
|
+
const found = [];
|
|
84
|
+
const remember = (value) => {
|
|
85
|
+
const code = asCode(value);
|
|
86
|
+
if (code && !found.includes(code)) found.push(code);
|
|
87
87
|
};
|
|
88
|
-
push(pitfall?.code);
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
remember(pitfall?.code);
|
|
90
|
+
|
|
91
|
+
const signature = String(pitfall?.sigKey ?? '');
|
|
92
|
+
if (signature) {
|
|
93
|
+
remember(signature.split('|')[1]);
|
|
94
|
+
for (const hit of signature.matchAll(ERROR_CODE_RE)) remember(hit[1]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const body = String(pitfall?.normText ?? '');
|
|
98
|
+
if (body) {
|
|
99
|
+
for (const hit of body.matchAll(ERROR_CODE_RE)) remember(hit[1]);
|
|
95
100
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
return found;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 命中结果的统一形状(why 前面挂规则 id,便于日志里直接看出"哪条规则判的")
|
|
105
|
+
function mapped(entry, via, matched) {
|
|
106
|
+
return { form: entry.form, why: `【映射表 ${entry.id}】${entry.why}`, ruleId: entry.id, via, matched };
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
/**
|
|
102
110
|
* 查表:错误码优先,其次签名正则。
|
|
103
111
|
* @param {object} pitfall
|
|
104
112
|
* @returns {{form:'check'|'skill'|'persona', why:string, ruleId:string, via:'code'|'sig', matched:string}|null}
|
|
105
|
-
*
|
|
113
|
+
* 未命中返回 null(调用方回落启发式)
|
|
106
114
|
*/
|
|
107
115
|
export function lookupFormByMap(pitfall) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (rule) return { form: rule.form, why: `【映射表 ${rule.id}】${rule.why}`, ruleId: rule.id, via: 'code', matched: c };
|
|
116
|
+
for (const code of extractErrorCodes(pitfall)) {
|
|
117
|
+
const entry = RULE_BY_CODE.get(code);
|
|
118
|
+
if (entry) return mapped(entry, 'code', code);
|
|
112
119
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
|
|
121
|
+
const haystack = [pitfall?.name, pitfall?.code, pitfall?.sigKey, pitfall?.normText]
|
|
122
|
+
.map((part) => String(part ?? ''))
|
|
123
|
+
.join('|');
|
|
124
|
+
for (const entry of FORM_RULES) {
|
|
125
|
+
if (!entry.sigRe) continue;
|
|
126
|
+
if (!entry.sigRe.test(haystack)) continue;
|
|
127
|
+
return mapped(entry, 'sig', String(entry.sigRe));
|
|
118
128
|
}
|
|
119
129
|
return null;
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
/**
|
|
132
|
+
/** 这个码表里有没有(面板 / 调试用) */
|
|
123
133
|
export function isMappedCode(code) {
|
|
124
|
-
return
|
|
134
|
+
return RULE_BY_CODE.has(String(code ?? '').toUpperCase());
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
/**
|
|
137
|
+
/** 表内全部错误码,字典序(单测断言用) */
|
|
128
138
|
export function mappedCodes() {
|
|
129
|
-
return [...
|
|
139
|
+
return [...RULE_BY_CODE.keys()].sort();
|
|
130
140
|
}
|