@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/pitfall-score.mjs
CHANGED
|
@@ -1,185 +1,262 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export const
|
|
23
|
-
/**
|
|
24
|
-
export const
|
|
25
|
-
/**
|
|
26
|
-
export const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
* @param {number}
|
|
46
|
-
* @param {number}
|
|
47
|
-
* @
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
* @returns {
|
|
95
|
-
*/
|
|
96
|
-
export function
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
1
|
+
// 坑的权重与升级阶梯 —— 全是纯函数,给定输入就给定输出(回归时有 golden 值盯着)。
|
|
2
|
+
//
|
|
3
|
+
// 两个概念:
|
|
4
|
+
// 权重:踩得越勤越重,但会随时间自然衰减 —— count × 0.5^(距上次踩坑的天数 / 半衰期)。
|
|
5
|
+
// 沉下去不等于删掉:超过一个半衰期没再踩,只标"疑似已修复",条目仍留在库里。
|
|
6
|
+
// 阶梯:踩 1 次入库 / 2 次上浮 / 3 次以上且跨 2 个会话才生成升级候选。
|
|
7
|
+
// 候选有三种落点(自动检查项 → 内联技能 → persona 铁律),越靠后越重;
|
|
8
|
+
// 本件只负责"建议哪一种",从不自己动手改 persona 或技能。
|
|
9
|
+
//
|
|
10
|
+
// 形态判定先查映射表(同目录的 pitfall-form-map.mjs):成因确定的坑交给表,表里没有才回落到
|
|
11
|
+
// 按文本特征猜。起因很实在 —— 有类坑的文本里净是路径,一猜就猜成"路径存在性预检",
|
|
12
|
+
// 方向完全反了。
|
|
13
|
+
//
|
|
14
|
+
// 口径约定:count = 同签名的总出现次数;衰减的天数取末次出现时间(lastSeen)。
|
|
15
|
+
|
|
16
|
+
import { lookupFormByMap } from './pitfall-form-map.mjs';
|
|
17
|
+
|
|
18
|
+
// 本版起 suggestForm 换了判定顺序:先查表、再启发式(表在本件之外,见上面的 import)。
|
|
19
|
+
export const PITFALL_SCORE_VERSION = '0.2.0';
|
|
20
|
+
|
|
21
|
+
/** 权重半衰期(天):每过这么多天,权重乘 1/2 */
|
|
22
|
+
export const HALF_LIFE_DAYS = 30;
|
|
23
|
+
/** 距末次出现超过这么多天 → 标"疑似已修复"(只沉底,不删除) */
|
|
24
|
+
export const LIKELY_FIXED_DAYS = 30;
|
|
25
|
+
/** 升级候选的两道门槛:出现次数、跨会话数 */
|
|
26
|
+
export const CANDIDATE_MIN_COUNT = 3;
|
|
27
|
+
export const CANDIDATE_MIN_SESSIONS = 2;
|
|
28
|
+
/** 够格进坑库的门槛(seed 条目另算) */
|
|
29
|
+
export const PITFALL_MIN_COUNT = 2;
|
|
30
|
+
/** seed 条目(历史文字坑)的基准权重:不数 count,只进衰减池 */
|
|
31
|
+
export const SEED_BASE_SCORE = 2;
|
|
32
|
+
|
|
33
|
+
const DAY_MS = 86_400_000;
|
|
34
|
+
const ROUND_SCALE = 10000;
|
|
35
|
+
const DECAY_BASE = 0.5;
|
|
36
|
+
|
|
37
|
+
// 权重统一保留 4 位小数:先放大再取整再缩回,避免 0.1+0.2 式的尾巴进报告
|
|
38
|
+
function round4(value) {
|
|
39
|
+
return Math.round(value * ROUND_SCALE) / ROUND_SCALE;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 距上次踩坑过了多少天。
|
|
44
|
+
* 没有时间戳、时间戳非法、或者算出来是负数(时钟回拨)一律按 0 天处理。
|
|
45
|
+
* @param {number} lastSeenMs
|
|
46
|
+
* @param {number} nowMs
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
export function daysSince(lastSeenMs, nowMs) {
|
|
50
|
+
const seen = Number(lastSeenMs);
|
|
51
|
+
if (!Number.isFinite(seen) || seen <= 0) return 0;
|
|
52
|
+
const elapsed = (Number(nowMs) - seen) / DAY_MS;
|
|
53
|
+
return elapsed > 0 ? elapsed : 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 动态权重:count × 0.5^(天数 / 半衰期)。
|
|
58
|
+
* @param {number} count 同签名总次数
|
|
59
|
+
* @param {number} lastSeenMs 末次出现时间(epoch ms)
|
|
60
|
+
* @param {number} nowMs
|
|
61
|
+
* @param {number} [halfLifeDays]
|
|
62
|
+
* @returns {number} 保留 4 位小数;count 为 0 直接 0
|
|
63
|
+
*/
|
|
64
|
+
export function decayScore(count, lastSeenMs, nowMs, halfLifeDays = HALF_LIFE_DAYS) {
|
|
65
|
+
const occurrences = Number(count) || 0;
|
|
66
|
+
if (occurrences === 0) return 0;
|
|
67
|
+
const factor = Math.pow(DECAY_BASE, daysSince(lastSeenMs, nowMs) / halfLifeDays);
|
|
68
|
+
return round4(occurrences * factor);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 一条坑的当前权重。两种来源分开算:
|
|
73
|
+
* scan 条目 —— count 走衰减;
|
|
74
|
+
* seed 条目 —— 没有 count,用基准权重(seedBase)走衰减。
|
|
75
|
+
* @param {object} pitfall
|
|
76
|
+
* @param {number} nowMs
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
export function scoreOf(pitfall, nowMs) {
|
|
80
|
+
if (pitfall?.source === 'seed') {
|
|
81
|
+
const baseline = Number(pitfall.seedBase ?? SEED_BASE_SCORE) || SEED_BASE_SCORE;
|
|
82
|
+
return round4(baseline * Math.pow(DECAY_BASE, daysSince(pitfall.lastSeen, nowMs) / HALF_LIFE_DAYS));
|
|
83
|
+
}
|
|
84
|
+
return decayScore(pitfall?.count, pitfall?.lastSeen, nowMs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 是否"疑似已修复"(够久没再踩;不删条目,只是沉底并打标) */
|
|
88
|
+
export function isLikelyFixed(lastSeenMs, nowMs, days = LIKELY_FIXED_DAYS) {
|
|
89
|
+
return daysSince(lastSeenMs, nowMs) >= days;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 阶梯档位。
|
|
94
|
+
* @returns {'recorded'|'surfaced'|'candidate'}
|
|
95
|
+
*/
|
|
96
|
+
export function ladderStage(count, sessionCount) {
|
|
97
|
+
const hits = Number(count) || 0;
|
|
98
|
+
const sessions = Number(sessionCount) || 0;
|
|
99
|
+
if (hits >= CANDIDATE_MIN_COUNT && sessions >= CANDIDATE_MIN_SESSIONS) return 'candidate';
|
|
100
|
+
return hits >= PITFALL_MIN_COUNT ? 'surfaced' : 'recorded';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 够不够格进坑库:seed 条目豁免(它是人工挑进去的历史文字坑) */
|
|
104
|
+
export function isRecordable(pitfall) {
|
|
105
|
+
if (pitfall?.source === 'seed') return true;
|
|
106
|
+
return (Number(pitfall?.count) || 0) >= PITFALL_MIN_COUNT;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 会话数:优先认显式字段,没有就数 sessions 数组的长度;两种情况都过一遍 Number
|
|
110
|
+
function sessionTally(pitfall) {
|
|
111
|
+
const explicit = pitfall.sessionCount;
|
|
112
|
+
if (explicit === undefined || explicit === null) {
|
|
113
|
+
return Array.isArray(pitfall.sessions) ? pitfall.sessions.length : 0;
|
|
114
|
+
}
|
|
115
|
+
return Number(explicit);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 排序规则写成一个比较器:权重降序 → 次数降序 → 末次时间降序 → id 升序(保证可复现)
|
|
119
|
+
function rankOrder(left, right) {
|
|
120
|
+
return right.score - left.score
|
|
121
|
+
|| (Number(right.count) || 0) - (Number(left.count) || 0)
|
|
122
|
+
|| (Number(right.lastSeen) || 0) - (Number(left.lastSeen) || 0)
|
|
123
|
+
|| String(left.id).localeCompare(String(right.id));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 排序并补齐派生字段。
|
|
128
|
+
* @param {object[]} pitfalls
|
|
129
|
+
* @param {number} nowMs
|
|
130
|
+
* @returns {object[]} 新数组(附 score / stage / likelyFixed / daysSinceLast)
|
|
131
|
+
*/
|
|
132
|
+
export function rankPitfalls(pitfalls, nowMs) {
|
|
133
|
+
const decorated = (pitfalls ?? []).map((pitfall) => {
|
|
134
|
+
const sessionCount = sessionTally(pitfall);
|
|
135
|
+
return {
|
|
136
|
+
...pitfall,
|
|
137
|
+
sessionCount,
|
|
138
|
+
score: scoreOf(pitfall, nowMs),
|
|
139
|
+
stage: ladderStage(pitfall.count, sessionCount),
|
|
140
|
+
likelyFixed: isLikelyFixed(pitfall.lastSeen, nowMs),
|
|
141
|
+
daysSinceLast: Math.round(daysSince(pitfall.lastSeen, nowMs) * 10) / 10,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
decorated.sort(rankOrder);
|
|
145
|
+
return decorated;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 开工前看一眼的 Top N:跳过被排除的;全是"疑似已修复"时退回原始前 N(免得空手)。
|
|
150
|
+
* @param {object[]} pitfalls
|
|
151
|
+
* @param {number} n
|
|
152
|
+
* @param {number} nowMs
|
|
153
|
+
* @returns {object[]}
|
|
154
|
+
*/
|
|
155
|
+
export function topN(pitfalls, n, nowMs) {
|
|
156
|
+
const usable = rankPitfalls(pitfalls, nowMs).filter((row) => !row.excluded);
|
|
157
|
+
const fresh = usable.filter((row) => !row.likelyFixed);
|
|
158
|
+
return (fresh.length ? fresh : usable).slice(0, n);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 没有稳定错误码、只能靠签名特征认的坑(按顺序试,先命中者胜)
|
|
162
|
+
const SIGNATURE_FORMS = [
|
|
163
|
+
{
|
|
164
|
+
pattern: /FS_SANDBOX_DENIED/,
|
|
165
|
+
form: 'check',
|
|
166
|
+
why: '沙箱拒绝=权限配置问题,可做成"写生产路径前先验权限"的检查项',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
pattern: /FS_NOT_TEXT|FS_NOT_FOUND/,
|
|
170
|
+
form: 'check',
|
|
171
|
+
why: '读文件前可先验类型/存在性,适合做成检查项',
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
// 按分类给形态:分类信息只有一层,逐条 if 不如查表
|
|
176
|
+
const CATEGORY_FORMS = new Map([
|
|
177
|
+
['守护鱼', { form: 'check', why: '守护鱼类坑适合做成只读健康检查项(dsh-selfcheck 同款)' }],
|
|
178
|
+
['插件', { form: 'skill', why: '插件类坑适合内联进 plugin-deploy 技能开头' }],
|
|
179
|
+
['同步', { form: 'skill', why: '同步类坑适合内联进同步/备份相关技能或脚本注释' }],
|
|
180
|
+
['会话', { form: 'skill', why: '会话类坑适合内联进 session-cleanup / dsh-restart 技能' }],
|
|
181
|
+
['pwsh', { form: 'skill', why: 'pwsh 类坑适合内联进相关技能;若能脚本化则优先检查项' }],
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const PATH_HINT_RE = /路径|path|<PATH>/i;
|
|
185
|
+
const PATH_FORM = { form: 'check', why: '路径假设类坑可做成"路径存在性预检"检查项' };
|
|
186
|
+
const FALLBACK_FORM = { form: 'skill', why: '默认建议内联技能;只有反复踩且影响全局的铁律级才动 persona' };
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 建议的升级形态。
|
|
190
|
+
* 判定顺序:① 映射表(错误码 / 签名)命中即用 → ② 签名特征 → ③ 分类 → ④ 文本里提到路径 → ⑤ 默认。
|
|
191
|
+
* 前两步之外都是启发式,只在前面的都认不出来时才轮到。
|
|
192
|
+
* @param {object} pitfall
|
|
193
|
+
* @returns {{form:'check'|'skill'|'persona', why:string, ruleId?:string, via?:string}}
|
|
194
|
+
*/
|
|
195
|
+
export function suggestForm(pitfall) {
|
|
196
|
+
const fromMap = lookupFormByMap(pitfall);
|
|
197
|
+
if (fromMap) return { form: fromMap.form, why: fromMap.why, ruleId: fromMap.ruleId, via: fromMap.via };
|
|
198
|
+
|
|
199
|
+
const signature = String(pitfall?.sigKey ?? '');
|
|
200
|
+
for (const entry of SIGNATURE_FORMS) {
|
|
201
|
+
if (entry.pattern.test(signature)) return { form: entry.form, why: entry.why };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const byCategory = CATEGORY_FORMS.get(pitfall?.category ?? '其他');
|
|
205
|
+
if (byCategory) return { form: byCategory.form, why: byCategory.why };
|
|
206
|
+
|
|
207
|
+
const body = String(pitfall?.normText ?? pitfall?.sigKey ?? '');
|
|
208
|
+
const chosen = PATH_HINT_RE.test(body) ? PATH_FORM : FALLBACK_FORM;
|
|
209
|
+
return { ...chosen };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 三个选项的文案固定:谁排第一、哪个被标"最优",都是方案定死的,不按输入变
|
|
213
|
+
function candidateOptions(suggestion, category) {
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
id: 'check',
|
|
217
|
+
label: '① 做成自动检查项(最优:坑不可能再犯)',
|
|
218
|
+
hint: suggestion.form === 'check' ? suggestion.why : '若可脚本化判定则首选',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'skill',
|
|
222
|
+
label: '② 内联进相关技能(加载即见)',
|
|
223
|
+
hint: suggestion.form === 'skill' ? suggestion.why : `建议内联进与「${category}」相关的技能开头`,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'persona',
|
|
227
|
+
label: '③ 铁律级才动 persona(需 persona-maintain 流程)',
|
|
228
|
+
hint: '仅当该坑影响全局且反复踩时才考虑;采纳后必须走 validate-persona 验证',
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 生成升级候选(次数 ≥3 且跨 ≥2 会话)。
|
|
235
|
+
* 只是"候选":本函数永不改 persona / 技能,落不落、落哪里由人拍。
|
|
236
|
+
* @param {object} pitfall 已 rank 过的坑(最好带 score/stage)
|
|
237
|
+
* @param {number} [nowMs]
|
|
238
|
+
* @returns {object|null} 不够格 / seed 条目 → null
|
|
239
|
+
*/
|
|
240
|
+
export function buildCandidate(pitfall, nowMs = Date.now()) {
|
|
241
|
+
if (!pitfall || pitfall.source === 'seed') return null;
|
|
242
|
+
if (ladderStage(pitfall.count, pitfall.sessionCount ?? 0) !== 'candidate') return null;
|
|
243
|
+
|
|
244
|
+
const suggestion = suggestForm(pitfall);
|
|
245
|
+
return {
|
|
246
|
+
id: pitfall.id,
|
|
247
|
+
pitfallId: pitfall.id,
|
|
248
|
+
sigKey: pitfall.sigKey,
|
|
249
|
+
category: pitfall.category,
|
|
250
|
+
count: pitfall.count,
|
|
251
|
+
sessionCount: pitfall.sessionCount,
|
|
252
|
+
score: pitfall.score ?? decayScore(pitfall.count, pitfall.lastSeen, nowMs),
|
|
253
|
+
tools: pitfall.tools ?? {},
|
|
254
|
+
normText: pitfall.normText ?? '',
|
|
255
|
+
suggested: suggestion.form,
|
|
256
|
+
why: suggestion.why,
|
|
257
|
+
options: candidateOptions(suggestion, pitfall.category),
|
|
258
|
+
status: 'pending',
|
|
259
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
260
|
+
decidedAt: null,
|
|
261
|
+
};
|
|
262
|
+
}
|