dsh-lite-memory 0.18.43 → 0.18.45

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.
@@ -1,471 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
-
6
- process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-cross-'));
7
- const store = await import('../store.js');
8
- const state = await import('../state.js');
9
- const view = await import('../view.js');
10
-
11
- const {
12
- saveEntry, routeScope, visibleRules, move, resumeMove, movePending,
13
- upsert, forget, readLinks, loadIndex, scopeOf, globalScope, readEntry, pendingConflictsOf,
14
- conflictsOf, resolveConflict, dropConflict,
15
- } = store;
16
-
17
- let passed = 0;
18
- const test = async (name, fn) => {
19
- try {
20
- await fn();
21
- passed += 1;
22
- } catch (error) {
23
- console.error(`FAIL ${name}`);
24
- throw error;
25
- }
26
- };
27
-
28
- const dirs = [];
29
- const fresh = (label) => {
30
- const dir = path.join(os.tmpdir(), `lite-memory-cross-${label}-${Math.random().toString(16).slice(2, 8)}`);
31
- fs.mkdirSync(dir, { recursive: true });
32
- dirs.push(dir);
33
- return scopeOf(dir);
34
- };
35
- const stateOf = (scope) => {
36
- const file = path.join(scope.dir, 'state.json');
37
- if (!fs.existsSync(file)) return { entries: [] };
38
- return JSON.parse(fs.readFileSync(file, 'utf8')).payload;
39
- };
40
- const idsOf = (scope) => stateOf(scope).entries.map((row) => row.id);
41
- const idsOfPrev = (scope) => {
42
- const file = path.join(scope.dir, 'state.prev.json');
43
- return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')).payload.entries.map((row) => row.id) : [];
44
- };
45
-
46
- // ── S03 范围路由 ────────────────────────────────────────────────────────────
47
- await test('S03 个人偏好:模型自动归类留项目层,人明确要全局才落全局', () => {
48
- const scope = fresh('route-pref');
49
- const automatic = saveEntry(scope, { title: '回答用中文(自动)', summary: '回复语言偏好', body: '中文、简短(自动)', memoryClass: 'personal_preference' });
50
- assert.equal(automatic.chosenScope, 'project', '模型归类只是提案,不能自己跨项目');
51
- assert.equal(automatic.reasonCode, 'model_class_project');
52
- assert.equal(idsOf(scope).length, 1);
53
- const saved = saveEntry(scope, {
54
- title: '回答用中文', summary: '回复语言偏好', body: '中文、简短',
55
- memoryClass: 'personal_preference', scopeConfirmed: true, resolution: 'distinct',
56
- });
57
- assert.equal(saved.chosenScope, 'global');
58
- assert.equal(saved.reasonCode, 'class_personal_preference');
59
- assert.equal(stateOf(globalScope()).entries.filter((row) => row.current.title === '回答用中文').length, 1);
60
- });
61
-
62
- await test('S03 项目事实留在项目层', () => {
63
- const scope = fresh('route-project');
64
- const saved = saveEntry(scope, { title: '构建命令', summary: '构建用 pnpm', body: 'pnpm build', memoryClass: 'project_fact' });
65
- assert.equal(saved.chosenScope, 'project');
66
- assert.equal(idsOf(scope).length, 1);
67
- });
68
-
69
- await test('S03 模型给的类别标签不能把「本项目」抬进全局层', () => {
70
- const scope = fresh('route-project-bound');
71
- const pref = saveEntry(scope, {
72
- title: '本项目回复用中文',
73
- summary: '这个项目里回复用中文',
74
- body: '本项目统一中文、简短',
75
- memoryClass: 'personal_preference',
76
- });
77
- assert.equal(pref.chosenScope, 'project');
78
- assert.equal(pref.reasonCode, 'project_bound_preference');
79
- assert.equal(stateOf(globalScope()).entries.filter((row) => row.current.title === '本项目回复用中文').length, 0);
80
- const proc = saveEntry(scope, {
81
- title: '本项目构建',
82
- summary: '这个仓库构建用 pnpm',
83
- body: '本仓库统一 pnpm build',
84
- memoryClass: 'portable_procedure',
85
- evidenceRef: 'README',
86
- });
87
- assert.equal(proc.chosenScope, 'project');
88
- assert.equal(proc.reasonCode, 'project_bound_procedure');
89
- /** 人自己确认「就存全局」时,措辞推断不能压过明确指令。 */
90
- const human = routeScope({
91
- title: '本项目回复用中文', body: '本项目统一中文、简短', memoryClass: 'personal_preference', scope: 'global',
92
- }, { humanScope: true, project: { confirmed: true } });
93
- assert.equal(human.chosenScope, 'global');
94
- assert.equal(human.reasonCode, 'explicit');
95
- });
96
-
97
- await test('S03 设备事实没给设备条件就返回 needsScope,不落盘', () => {
98
- const scope = fresh('route-env');
99
- const saved = saveEntry(scope, { title: '本机 Node', summary: '本机 Node 版本', body: 'v24', memoryClass: 'environment_fact' });
100
- assert.equal(saved.needsScope, true);
101
- assert.equal(saved.reasonCode, 'needs_environment');
102
- assert.equal(idsOf(scope).length, 0);
103
- assert.deepEqual(saved.requiredConditions.map((row) => row.key), ['machine', 'runtime']);
104
- });
105
-
106
- await test('S03 设备事实带条件进全局层且是条件适用', () => {
107
- const scope = fresh('route-env-ok');
108
- const saved = saveEntry(scope, {
109
- title: '本机 Node', summary: '本机 Node 版本', body: 'v24', memoryClass: 'environment_fact', scopeConfirmed: true,
110
- conditions: [{ key: 'machine', value: 'mac-mini' }],
111
- });
112
- assert.equal(saved.chosenScope, 'global');
113
- assert.equal(saved.record.applicability, 'conditional');
114
- assert.equal(idsOf(scope).length, 0);
115
- });
116
-
117
- await test('S03 一次性任务参数不落盘;显式范围压过类别提案', () => {
118
- const scope = fresh('route-explicit');
119
- const task = saveEntry(scope, { title: '临时端口', summary: '这次跑在 8080', body: '8080', durability: 'task' });
120
- assert.equal(task.needsScope, true);
121
- assert.equal(task.reasonCode, 'task_state');
122
- const explicit = saveEntry(scope, { title: '项目内偏好', summary: '只在这个项目用', body: 'x', memoryClass: 'personal_preference', scope: 'project' });
123
- assert.equal(explicit.chosenScope, 'project');
124
- assert.equal(explicit.reasonCode, 'explicit');
125
- });
126
-
127
- // ── S06 两层去重 ────────────────────────────────────────────────────────────
128
- await test('S06 两层完全相同只返回已有引用,不写第二条', () => {
129
- const scope = fresh('dedupe-exact');
130
- const globalSaved = saveEntry(globalScope(), {
131
- title: '统一时区', summary: '日志都写 UTC', body: 'UTC', scope: 'global', resolution: 'distinct',
132
- });
133
- assert.equal(globalSaved.chosenScope, 'global');
134
- const again = saveEntry(scope, { title: '统一时区', summary: '日志都写 UTC', body: 'UTC' });
135
- assert.ok(again.existing);
136
- assert.equal(again.existing.id, globalSaved.record.id);
137
- assert.equal(again.existing.scope, 'global');
138
- assert.equal(idsOf(scope).length, 0);
139
- });
140
-
141
- await test('S06 两层只是相近只给候选或确认,不直接新增', () => {
142
- const scope = fresh('dedupe-near');
143
- saveEntry(globalScope(), { title: '发布前检查清单', summary: '发版前逐项确认', body: '清单', scope: 'global', resolution: 'distinct' });
144
- const near = saveEntry(scope, { title: '发布前检查清单', summary: '发版前逐项确认(项目补充)', body: '项目清单' });
145
- assert.equal(near.needsResolution, true);
146
- assert.equal(near.crossLayer, true);
147
- assert.equal(idsOf(scope).length, 0);
148
- assert.equal(near.similarList.length >= 1, true);
149
- });
150
-
151
- // ── S07 槽位:项目覆盖 / 回落 / 条件未知 ────────────────────────────────────
152
- const seedSlot = (scope, layer, extra) => saveEntry(layer === 'global' ? globalScope() : scope, {
153
- title: extra.title, summary: extra.summary, body: extra.body || '正文',
154
- topicKey: 'tooling.pm', resolution: 'distinct', ...extra,
155
- });
156
-
157
- await test('S07 项目层命中时只给项目规范,全局同名槽位被压住', () => {
158
- const scope = fresh('slot-match');
159
- seedSlot(scope, 'global', { title: '包管理器', summary: '用 pnpm', applicability: 'all_in_scope' });
160
- seedSlot(scope, 'project', {
161
- title: '包管理器', summary: '这个项目用 npm', applicability: 'conditional',
162
- conditions: [{ key: 'runtime', value: 'node' }],
163
- });
164
- const { winners } = visibleRules(scope, { runtime: 'node' });
165
- const titles = winners.filter((row) => row.key === 'tooling.pm').map((row) => row.record.summary);
166
- assert.deepEqual(titles, ['这个项目用 npm']);
167
- });
168
-
169
- await test('S07 项目层条件不匹配时回落到全局', () => {
170
- const scope = fresh('slot-fallback');
171
- seedSlot(scope, 'global', { title: '包管理器', summary: '用 pnpm', applicability: 'all_in_scope' });
172
- seedSlot(scope, 'project', {
173
- title: '包管理器', summary: '这个项目用 npm', applicability: 'conditional',
174
- conditions: [{ key: 'runtime', value: 'node' }],
175
- });
176
- const { winners } = visibleRules(scope, { runtime: 'bun' });
177
- const rows = winners.filter((row) => row.key === 'tooling.pm');
178
- assert.equal(rows.length, 1);
179
- assert.equal(rows[0].record.summary, '用 pnpm');
180
- assert.equal(rows[0].scope, 'global');
181
- });
182
-
183
- await test('S07 项目层条件未知时整组暂缓,不拿全局顶上', () => {
184
- const scope = fresh('slot-unknown');
185
- seedSlot(scope, 'global', { title: '包管理器', summary: '用 pnpm', applicability: 'all_in_scope' });
186
- seedSlot(scope, 'project', {
187
- title: '包管理器', summary: '这个项目用 npm', applicability: 'conditional',
188
- conditions: [{ key: 'runtime', value: 'node' }],
189
- });
190
- const { resolved } = visibleRules(scope, {});
191
- const group = resolved.get('tooling.pm');
192
- assert.equal(group.status, 'needs_context');
193
- assert.equal(group.record, null);
194
- });
195
-
196
- await test('S08 同层同槽位不同内容判冲突,不靠命中顺序仲裁', () => {
197
- const scope = fresh('slot-conflict');
198
- upsert(scope, { title: '包管理器一', summary: '用 pnpm', body: 'a', topicKey: 'tooling.pm', applicability: 'all_in_scope' });
199
- const second = saveEntry(scope, {
200
- title: '包管理器二', summary: '用 npm', body: 'b', topicKey: 'tooling.pm', applicability: 'all_in_scope', resolution: 'distinct',
201
- });
202
- assert.equal(second.chosenScope, 'project');
203
- const { resolved } = visibleRules(scope, {});
204
- assert.equal(resolved.get('tooling.pm').status, 'conflict');
205
- });
206
-
207
- await test('S08 同层同槽位同条件再存要确认,不静默新建', () => {
208
- const scope = fresh('slot-same');
209
- const first = upsert(scope, { title: '唯一槽位甲', summary: '槽位测试甲', body: 'a', topicKey: 'tooling.slotonly', applicability: 'all_in_scope' });
210
- const again = saveEntry(scope, { title: '唯一槽位甲 补充', summary: '槽位测试甲', body: 'b', topicKey: 'tooling.slotonly', applicability: 'all_in_scope' });
211
- assert.equal(again.needsResolution, true);
212
- assert.equal(again.sameTopic, true);
213
- assert.equal(again.similarList[0].id, first.record.id);
214
- assert.equal(idsOf(scope).length, 1);
215
- });
216
-
217
- // ── S09 / D09 搬移 ──────────────────────────────────────────────────────────
218
- await test('S09 项目搬到全局:新 id、旧 id 失效、prev 不留旧条目', () => {
219
- const scope = fresh('move-up');
220
- const saved = saveEntry(scope, {
221
- title: '提交信息格式', summary: '都用 type: 前缀', body: 'type: subject', topicKey: 'git.commit',
222
- });
223
- const before = saved.record.id;
224
- const done = move(scope, before, { target: 'global' });
225
- assert.equal(done.status, 'moved');
226
- assert.notEqual(done.id, before);
227
- assert.equal(idsOf(scope).includes(before), false);
228
- assert.equal(idsOfPrev(scope).includes(before), false);
229
- const moved = stateOf(globalScope()).entries.find((row) => row.id === done.id);
230
- assert.ok(moved);
231
- assert.equal(moved.current.body, 'type: subject');
232
- assert.equal(moved.current.topicKey, 'git.commit');
233
- assert.equal(movePending(), null);
234
- assert.equal(readEntry(globalScope(), done.id).body, 'type: subject');
235
- });
236
-
237
- await test('S09 续跑只认发起项目的身份,换项目被拒绝', () => {
238
- const a = fresh('resume-a');
239
- const b = fresh('resume-b');
240
- const saved = saveEntry(a, {
241
- title: '续跑归属', summary: '搬移中断后只能回原项目续跑', body: 'x', scope: 'project', resolution: 'distinct',
242
- });
243
- const pending = move(a, saved.record.id, { target: 'global', stopAfter: 'source' });
244
- assert.equal(pending.status, 'move_pending');
245
- assert.throws(() => resumeMove(b), (error) => error.code === 'conflict' && /另一个项目/.test(error.message));
246
- assert.ok(movePending());
247
- const done = resumeMove(a);
248
- assert.equal(done.status, 'moved');
249
- assert.equal(movePending(), null);
250
- assert.equal(idsOf(a).includes(saved.record.id), false);
251
- assert.equal(stateOf(globalScope()).entries.some((row) => row.id === done.id), true);
252
- });
253
-
254
- await test('S09 续跑前源 current 丢失时从 prev 恢复,但不会复活旧条目', () => {
255
- const scope = fresh('resume-prev');
256
- const saved = saveEntry(scope, {
257
- title: '恢复不复活', summary: 'prev 里有旧条目也不能带回来', body: 'x', scope: 'project', resolution: 'distinct',
258
- });
259
- const stale = fs.readFileSync(path.join(scope.dir, 'state.json'), 'utf8');
260
- const pending = move(scope, saved.record.id, { target: 'global', stopAfter: 'source' });
261
- assert.equal(pending.stage, 'source_cleaned');
262
- fs.rmSync(path.join(scope.dir, 'state.json'));
263
- fs.writeFileSync(path.join(scope.dir, 'state.prev.json'), stale);
264
- const done = resumeMove(scope);
265
- assert.equal(done.status, 'moved');
266
- assert.equal(idsOf(scope).includes(saved.record.id), false);
267
- assert.equal(idsOfPrev(scope).includes(saved.record.id), false);
268
- assert.equal(movePending(), null);
269
- });
270
-
271
- await test('S09 收尾会重建 prev;两侧状态对不上就保留协调文件', () => {
272
- const scope = fresh('resume-verify');
273
- const saved = saveEntry(scope, {
274
- title: '收尾校验', summary: 'prev 可重建,状态对不上不结案', body: 'x', scope: 'project', resolution: 'distinct',
275
- });
276
- const pending = move(scope, saved.record.id, { target: 'global', stopAfter: 'source' });
277
- assert.equal(pending.status, 'move_pending');
278
- fs.rmSync(path.join(globalScope().dir, 'state.prev.json'), { force: true });
279
- const repaired = resumeMove(scope);
280
- assert.equal(repaired.status, 'moved');
281
- assert.equal(idsOfPrev(globalScope()).includes(repaired.id), true);
282
- assert.equal(movePending(), null);
283
-
284
- const other = fresh('resume-verify-2');
285
- const record = saveEntry(other, {
286
- title: '两侧对不上', summary: '源层状态丢了就不假装搬完', body: 'x', scope: 'project', resolution: 'distinct',
287
- });
288
- const pending2 = move(other, record.record.id, { target: 'global', stopAfter: 'source' });
289
- assert.equal(pending2.status, 'move_pending');
290
- fs.rmSync(path.join(other.dir, 'state.json'));
291
- fs.rmSync(path.join(other.dir, 'state.prev.json'), { force: true });
292
- assert.throws(() => resumeMove(other), (error) => error.code === 'recover_failed' || error.code === 'unavailable');
293
- assert.ok(movePending());
294
- state.clearMoveIntent();
295
- });
296
-
297
- await test('S09 全局搬回项目走同一套协议', () => {
298
- const scope = fresh('move-down');
299
- const saved = saveEntry(globalScope(), { title: '回滚命令', summary: '回滚用 rollback', body: 'rollback', scope: 'global', resolution: 'distinct' });
300
- const done = move(scope, saved.record.id, { target: 'project' });
301
- assert.equal(done.status, 'moved');
302
- assert.equal(done.from, 'global');
303
- assert.equal(done.to, 'project');
304
- assert.equal(stateOf(globalScope()).entries.some((row) => row.id === saved.record.id), false);
305
- assert.ok(idsOf(scope).includes(done.id));
306
- });
307
-
308
- await test('D09 目标层已有相同内容就拒绝搬移,不落协调文件', () => {
309
- const scope = fresh('move-dup');
310
- const source = saveEntry(scope, { title: '日志目录', summary: '日志在 logs', body: 'logs/', scope: 'project', resolution: 'distinct' });
311
- saveEntry(globalScope(), { title: '日志目录', summary: '日志在 logs', body: 'logs/', scope: 'global', resolution: 'distinct' });
312
- assert.throws(() => move(scope, source.record.id, { target: 'global' }), /目标层已有相同内容/);
313
- assert.equal(movePending(), null);
314
- assert.equal(idsOf(scope).includes(source.record.id), true);
315
- });
316
-
317
- await test('S09 出边在目标层不可见就拒绝搬移', () => {
318
- const scope = fresh('move-links');
319
- const target = saveEntry(scope, { title: '发布脚本', summary: '发版执行这个', body: './release.sh', scope: 'project', resolution: 'distinct' });
320
- const source = saveEntry(scope, {
321
- title: '发布检查', summary: '发版前先检查', body: '检查', scope: 'project', resolution: 'distinct',
322
- links: [{ type: 'related', id: target.record.id }],
323
- });
324
- assert.throws(() => move(scope, source.record.id, { target: 'global' }), /在目标层不可见/);
325
- assert.equal(movePending(), null);
326
- });
327
-
328
- await test('D09 目标写入后中断:两层读取被屏障挡住,resume 收尾后只剩一份', () => {
329
- const scope = fresh('move-crash');
330
- const saved = saveEntry(scope, { title: '端口约定', summary: '本地端口 49631', body: '49631', scope: 'project', resolution: 'distinct' });
331
- const done = move(scope, saved.record.id, { target: 'global', stopAfter: 'target' });
332
- assert.equal(done.status, 'move_pending');
333
- assert.ok(movePending());
334
- assert.throws(() => loadIndex(scope), /正在跨层搬移/);
335
- assert.throws(() => loadIndex(globalScope()), /正在跨层搬移/);
336
- const resumed = resumeMove(scope);
337
- assert.equal(resumed.status, 'moved');
338
- assert.equal(movePending(), null);
339
- assert.equal(stateOf(globalScope()).entries.filter((row) => row.id === resumed.id).length, 1);
340
- assert.equal(idsOf(scope).includes(saved.record.id), false);
341
- });
342
-
343
- await test('D09 协调文件损坏时报错,不当成没有搬移', () => {
344
- const scope = fresh('move-corrupt');
345
- const saved = saveEntry(scope, { title: '缓存目录', summary: '缓存在 .cache', body: '.cache', scope: 'project', resolution: 'distinct' });
346
- fs.writeFileSync(path.join(state.memoryRoot(), 'move.intent.json'), '{broken');
347
- assert.throws(() => move(scope, saved.record.id, { target: 'global' }), /协调文件已损坏/);
348
- state.clearMoveIntent();
349
- assert.equal(movePending(), null);
350
- });
351
-
352
- // ── L01–L06 关联 ────────────────────────────────────────────────────────────
353
- await test('L01/L02 关联不能指向自己,也不能重复', () => {
354
- const scope = fresh('link-self');
355
- const saved = saveEntry(scope, { title: '部署步骤', summary: '部署按这个来', body: '步骤', scope: 'project', resolution: 'distinct' });
356
- assert.throws(() => upsert(scope, { id: saved.record.id, title: '部署步骤', links: [{ type: 'related', id: saved.record.id }] }), /不能关联自己/);
357
- const other = saveEntry(scope, { title: '回滚步骤', summary: '回滚按这个来', body: '步骤', scope: 'project', resolution: 'distinct' });
358
- assert.throws(() => upsert(scope, { id: saved.record.id, title: '部署步骤', links: [{ type: 'related', id: other.record.id }, { type: 'related', id: other.record.id }] }), /重复/);
359
- });
360
-
361
- await test('L03 关联最多 3 条', () => {
362
- const scope = fresh('link-max');
363
- const ids = [];
364
- for (const n of [1, 2, 3, 4]) {
365
- ids.push(saveEntry(scope, { title: `关联目标 ${n}`, summary: `目标 ${n}`, body: 'x', scope: 'project', resolution: 'distinct' }).record.id);
366
- }
367
- assert.throws(() => upsert(scope, { title: '关联四条', summary: '四个关联', body: 'x', links: ids.map((id) => ({ type: 'related', id })) }), /最多 3/);
368
- });
369
-
370
- await test('L04 项目条目可以关联全局条目,全局条目不能关联项目条目', () => {
371
- const scope = fresh('link-layers');
372
- const globalSaved = saveEntry(globalScope(), { title: '全局约定', summary: '全局层的约定', body: 'x', scope: 'global', resolution: 'distinct' });
373
- const projectLinked = saveEntry(scope, {
374
- title: '项目条目', summary: '关联全局', body: 'x', scope: 'project', resolution: 'distinct', links: [{ type: 'related', id: globalSaved.record.id }],
375
- });
376
- assert.equal(projectLinked.needsResolution, false);
377
- assert.equal(view.linksOf(projectLinked.record).length, 1);
378
- assert.throws(() => upsert(globalScope(), { title: '全局条目', summary: '想关联项目', body: 'x', links: [{ type: 'related', id: projectLinked.record.id }] }), /关联目标不存在/);
379
- });
380
-
381
- await test('L05 关联目标被归档后读出来是失效引用,不悄悄消失', () => {
382
- const scope = fresh('link-archive');
383
- const target = saveEntry(scope, { title: '被归档目标', summary: '之后会被归档', body: 'x', scope: 'project', resolution: 'distinct' });
384
- const source = saveEntry(scope, {
385
- title: '关联方', summary: '关联了一个目标', body: 'x', scope: 'project', resolution: 'distinct', links: [{ type: 'related', id: target.record.id }],
386
- });
387
- forget(scope, target.record.id, { mode: 'archive' });
388
- const links = readLinks(scope, source.record.id).links;
389
- assert.equal(links.length, 1);
390
- assert.equal(links[0].status, 'invalid');
391
- });
392
-
393
- await test('L06 展示视图不把关联结构塞给模型', () => {
394
- const scope = fresh('link-view');
395
- const target = saveEntry(scope, { title: '视图目标', summary: '视图测试', body: 'x', scope: 'project', resolution: 'distinct' });
396
- const source = saveEntry(scope, {
397
- title: '视图关联方', summary: '视图测试', body: 'x', scope: 'project', resolution: 'distinct', links: [{ type: 'related', id: target.record.id }],
398
- });
399
- const search = view.toModelView(source.record, 'search');
400
- assert.equal('links' in search, false);
401
- const read = view.toModelView(source.record, 'read');
402
- assert.equal('links' in read, false);
403
- assert.equal(read.related, 1);
404
- });
405
-
406
- for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true });
407
-
408
- // ── S12 冲突队列:相近只记待办,确认后才进库 ───────────────────────────────
409
- await test('S12 相近保存进待办队列,确认 distinct 后出队', () => {
410
- const scope = fresh('conflict-queue');
411
- saveEntry(globalScope(), { title: '发布前检查清单', summary: '发版前逐项确认', body: '清单', scope: 'global', resolution: 'distinct' });
412
- assert.equal(pendingConflictsOf(globalScope()), 0);
413
- const near = saveEntry(scope, { title: '发布前检查清单', summary: '发版前逐项确认(项目补充)', body: '项目清单' });
414
- assert.equal(near.needsResolution, true);
415
- assert.equal(near.pendingConflicts, 1);
416
- assert.equal(pendingConflictsOf(scope), 1);
417
- const queued = loadIndex(scope).conflicts[0];
418
- assert.equal(queued.title, '发布前检查清单');
419
- assert.equal(queued.scope, 'project');
420
- assert.equal(queued.candidates.length >= 1, true);
421
- const again = saveEntry(scope, { title: '发布前检查清单', summary: '发版前逐项确认(项目补充)', body: '项目清单' });
422
- assert.equal(pendingConflictsOf(scope), 1);
423
- const distinct = saveEntry(scope, { title: '发布前检查清单', summary: '发版前逐项确认(项目补充)', body: '项目清单', resolution: 'distinct' });
424
- assert.equal(distinct.needsResolution, false);
425
- assert.equal(pendingConflictsOf(scope), 0);
426
- });
427
-
428
- await test('S12 待裁决留可重放草稿,面板确认另存后才落库', () => {
429
- const scope = fresh('conflict-replay');
430
- saveEntry(globalScope(), { title: '提交信息格式', summary: '用一句话说清做了什么', body: '全局约定', resolution: 'distinct' });
431
- const near = saveEntry(scope, { title: '提交信息格式', summary: '本项目用中文写提交信息', body: '项目补充', when: '本仓库' });
432
- assert.equal(near.needsResolution, true);
433
- assert.equal(loadIndex(scope).entries.length, 0, '没确认前不能预写正文');
434
- const [row] = conflictsOf(scope);
435
- assert.equal(row.title, '提交信息格式');
436
- assert.equal(row.draft.body, '项目补充');
437
- assert.equal(row.draft.when, '本仓库');
438
- const kept = resolveConflict(scope, '提交信息格式');
439
- assert.equal(kept.ok, true);
440
- assert.equal(pendingConflictsOf(scope), 0);
441
- assert.equal(loadIndex(scope).entries.length, 1);
442
- assert.equal(loadIndex(scope).entries[0].body, '项目补充');
443
- assert.equal(loadIndex(scope).entries[0].when, '本仓库', 'when 经过待裁决不能丢');
444
- assert.equal(dropConflict(scope, '不存在').ok, false);
445
- });
446
-
447
- await test('S12 经过待裁决另存不改变适用范围、触发词和关联', () => {
448
- const scope = fresh('conflict-semantics');
449
- const globalOne = saveEntry(globalScope(), { title: '发票抬头规则', summary: '开票信息固定', body: '全局版本', resolution: 'distinct' });
450
- const linked = saveEntry(globalScope(), { title: '生效核验顺序', summary: '先验规则再验页面', body: '先验规则', resolution: 'distinct' });
451
- const near = saveEntry(scope, {
452
- title: '发票抬头规则', summary: '本项目发票只认这套抬头', body: '项目版本',
453
- selection: { mode: 'complete', artifactKey: 'artifact-invoice-v1' },
454
- triggers: ['artifact-hookword'],
455
- links: [{ type: 'related', id: linked.record.id }],
456
- topicKey: 'invoice',
457
- });
458
- assert.equal(near.needsResolution, true);
459
- const [row] = conflictsOf(scope);
460
- assert.deepEqual(row.draft.selection, { mode: 'complete', artifactKey: 'artifact-invoice-v1', items: [] });
461
- assert.deepEqual(row.draft.triggers, ['artifact-hookword']);
462
- assert.deepEqual(row.draft.links, [{ type: 'related', id: linked.record.id }]);
463
- const kept = resolveConflict(scope, { id: row.id, expectedRevision: row.revision });
464
- assert.equal(kept.ok, true, JSON.stringify(kept));
465
- const [record] = loadIndex(scope).entries;
466
- assert.deepEqual(record.selection, { mode: 'complete', artifactKey: 'artifact-invoice-v1', items: [] });
467
- assert.deepEqual(record.triggers, ['artifact-hookword']);
468
- assert.deepEqual(record.links, [{ type: 'related', id: linked.record.id }]);
469
- });
470
-
471
- console.log(`crossing.test.mjs: ${passed} passed`);
@@ -1,113 +0,0 @@
1
- /**
2
- * 30 条固定样本的召回基线:10 该召回、10 该拒绝、10 该等条件确认。
3
- * 只断言行为(召回率 / 误触发 / 边界外泄),措辞取用户原话视角。
4
- */
5
- import assert from 'node:assert/strict';
6
- import fs from 'node:fs';
7
- import os from 'node:os';
8
- import path from 'node:path';
9
-
10
- process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-eval-'));
11
- const view = await import('../view.js');
12
-
13
- const base = (extra) => ({
14
- status: 'active', applicability: 'all_in_scope', conditions: [], selection: null,
15
- aliases: [], tags: [], triggers: [], when: '', lastUsed: '', ...extra,
16
- });
17
-
18
- const MEM = [
19
- base({ id: 'mem_100000000001', title: '插件改完先试热刷新', summary: '热刷新不行再重启宿主', triggers: ['插件改完'] }),
20
- base({ id: 'mem_100000000002', title: '发版前跑四条 lint', summary: '文案、中文排印、风格、检测器都要清零', triggers: ['发版'] }),
21
- base({ id: 'mem_100000000003', title: '外链一律用绝对路径', summary: '聊天里的文件链接必须绝对路径,否则点开报 ENOENT' }),
22
- base({ id: 'mem_100000000004', title: '截图只在看外观时用', summary: '判断页面对不对读文本树' }),
23
- base({ id: 'mem_100000000005', title: '写盘先写临时文件再改名', summary: 'rename 之后 fsync 目录,避免半截文件' }),
24
- base({ id: 'mem_100000000006', title: '客户端包上限四十 KiB', summary: 'gzip 之后超过就不许合入' }),
25
- base({ id: 'mem_100000000007', title: '测试日志写到临时目录', summary: '长跑放后台,用 EXIT= 判断结果' }),
26
- base({ id: 'mem_100000000008', title: '推送前先跑全量测试', summary: '绿了再 commit' }),
27
- base({ id: 'mem_100000000009', title: '一次性脚本用完就删', summary: '这个脚本只跑一次,别留仓库里' }),
28
- base({ id: 'mem_100000000010', title: '中文文案不用省略号收尾', summary: '能写完就写完' }),
29
- base({
30
- id: 'mem_100000000011', title: '部署前确认目标机器', summary: '只在目标机器上执行',
31
- applicability: 'conditional', conditions: [{ key: 'taskKind', value: 'deploy' }],
32
- }),
33
- base({ id: 'mem_100000000012', title: '配色只沿用现有 token', summary: '不新造颜色', applicability: 'unresolved' }),
34
- ];
35
-
36
- /** 10 条该召回:查询是真人会说的话,答案就在标题/关键词里。 */
37
- const RECALL = [
38
- ['插件改完怎么让它生效?', '插件改完先试热刷新'],
39
- ['发版之前要跑哪些检查?', '发版前跑四条 lint'],
40
- ['聊天里给的文件链接怎么写?', '外链一律用绝对路径'],
41
- ['判断页面报错要截图吗?', '截图只在看外观时用'],
42
- ['写文件怎么保证不会写出半截?', '写盘先写临时文件再改名'],
43
- ['客户端包大小有上限吗?', '客户端包上限四十 KiB'],
44
- ['测试日志放哪里?', '测试日志写到临时目录'],
45
- ['推送代码前要做什么?', '推送前先跑全量测试'],
46
- ['临时脚本要留在仓库里吗?', '一次性脚本用完就删'],
47
- ['中文文案末尾要加省略号吗?', '中文文案不用省略号收尾'],
48
- ];
49
-
50
- /** 10 条该拒绝:字面上有常见词,但跟任何一条记忆都不相关。 */
51
- const REJECT = [
52
- '把这段视频的字幕导出来',
53
- '明早路过菜市场买点香菜',
54
- '这个季节樱桃多少钱一斤',
55
- '帮我把上周的照片按日期归类',
56
- '厨房水槽下面有点漏水',
57
- '地铁六号线末班车几点',
58
- '明天杭州会下雨吗',
59
- '给猫换一个更安静的猫砂盆',
60
- '这本书的第二章讲什么',
61
- '楼下咖啡店的营业时间',
62
- ];
63
-
64
- /** 10 条该等条件:找到了,但适用条件还没确认,不许当已适用塞进上下文。 */
65
- const AWAIT = [
66
- ['这台机器部署前要确认什么?', '部署前确认目标机器'],
67
- ['部署脚本怎么跑?', '部署前确认目标机器'],
68
- ['机器对不上能直接部署吗?', '部署前确认目标机器'],
69
- ['新页面的配色怎么定?', '配色只沿用现有 token'],
70
- ['这个组件能新造颜色吗?', '配色只沿用现有 token'],
71
- ['界面上可以加渐变吗?', '配色只沿用现有 token'],
72
- ['主题色改成什么了?', '配色只沿用现有 token'],
73
- ['图标颜色跟着谁走?', '配色只沿用现有 token'],
74
- ['按钮圆角用新的吗?', '配色只沿用现有 token'],
75
- ['换肤要动哪些变量?', '配色只沿用现有 token'],
76
- ];
77
-
78
- const titleOf = (row) => row.record.title;
79
- const rank = (query, context = {}) => view.rank(MEM, query, { limit: 3, requireApplicable: true, context });
80
-
81
- let passed = 0;
82
- const test = (name, fn) => {
83
- fn();
84
- passed += 1;
85
- console.log(`eval: ${name}`);
86
- };
87
-
88
- test('该召回的 10 条都进前三', () => {
89
- const missed = RECALL.filter(([query, want]) => !rank(query).slice(0, 3).some((row) => titleOf(row) === want));
90
- const recallAt3 = (RECALL.length - missed.length) / RECALL.length;
91
- assert.ok(recallAt3 >= 0.9, `Recall@3 ${recallAt3};漏了:${missed.map((row) => row[0]).join('、')}`);
92
- });
93
-
94
- test('该拒绝的 10 条一条都不出现', () => {
95
- const leaked = REJECT.filter((query) => rank(query).length > 0);
96
- assert.equal(leaked.length, 0, `误触发:${leaked.join('、')}`);
97
- });
98
-
99
- test('条件未确认的 10 条不会当作已适用出现', () => {
100
- const exposed = AWAIT.filter(([query, want]) => rank(query).some((row) => titleOf(row) === want));
101
- assert.equal(exposed.length, 0, `条件未知却进了上下文:${exposed.map((row) => row[0]).join('、')}`);
102
- });
103
-
104
- test('条件确认之后同一条就能用', () => {
105
- const rows = rank('这台机器部署前要确认什么?', { taskKind: 'deploy' });
106
- assert.ok(rows.some((row) => titleOf(row) === '部署前确认目标机器'));
107
- });
108
-
109
- test('措辞不同也能召回(同义说法不依赖逐字重合)', () => {
110
- assert.ok(rank('热刷新不生效是不是要重启').some((row) => titleOf(row) === '插件改完先试热刷新'));
111
- });
112
-
113
- console.log(`eval: ${passed} passed`);