@modusensus/dsh-mneme 0.10.1 → 0.12.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 +17 -6
- package/bin/dsh-mneme-choro.mjs +254 -0
- package/docs/CONFIGURATION.md +10 -1
- package/lib/api-standalone.js +17 -21
- package/lib/api.js +414 -12
- package/lib/bootstrap-http.js +105 -0
- package/lib/bootstrap.js +5 -1
- package/lib/choro-command.js +104 -0
- package/lib/client.js +1637 -110
- package/lib/config-keys.js +144 -0
- package/lib/config.js +60 -1
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/index.js +87 -5
- package/lib/inject.js +18 -4
- package/lib/lang.js +82 -0
- package/lib/profile-facts.js +356 -0
- package/lib/quality-filter.js +48 -12
- package/lib/service.js +112 -4
- package/lib/settings.js +13 -1
- package/lib/store.js +978 -27
- package/lib/tools.js +244 -1
- package/lib/user-settings.js +63 -7
- package/lib/wiki/inject.js +164 -0
- package/lib/wiki/partition.js +200 -0
- package/lib/wiki/plan.js +214 -0
- package/lib/wiki/queries.js +360 -0
- package/lib/wiki/render.js +187 -0
- package/lib/wiki/scan.js +371 -0
- package/lib/wiki/t0.js +463 -0
- package/lib/wiki/t1.js +496 -0
- package/lib/write-admission.js +5 -3
- package/package.json +4 -2
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +17 -21
- package/src/api.js +414 -12
- package/src/bootstrap-http.js +105 -0
- package/src/bootstrap.js +5 -1
- package/src/choro-command.js +104 -0
- package/src/config-keys.js +144 -0
- package/src/config.js +60 -1
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/index.js +87 -5
- package/src/inject.js +18 -4
- package/src/lang.js +82 -0
- package/src/profile-facts.js +356 -0
- package/src/quality-filter.js +48 -12
- package/src/service.js +112 -4
- package/src/settings.js +13 -1
- package/src/store.js +978 -27
- package/src/tools.js +244 -1
- package/src/user-settings.js +63 -7
- package/src/wiki/inject.js +164 -0
- package/src/wiki/partition.js +200 -0
- package/src/wiki/plan.js +214 -0
- package/src/wiki/queries.js +360 -0
- package/src/wiki/render.js +187 -0
- package/src/wiki/scan.js +371 -0
- package/src/wiki/t0.js +463 -0
- package/src/wiki/t1.js +496 -0
- package/src/write-admission.js +5 -3
- package/test/api-routes-gaps.test.js +131 -1
- package/test/api.test.js +9 -1
- package/test/bootstrap.test.js +8 -0
- package/test/choro-command.test.js +218 -0
- package/test/client.test.js +802 -21
- package/test/config-keys-routes.test.js +233 -0
- package/test/config.test.js +79 -0
- package/test/contrast.test.js +14 -7
- package/test/document.test.js +32 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/inject.test.js +51 -0
- package/test/llm-audit.test.js +29 -1
- package/test/persona-settings.test.js +31 -0
- package/test/profile-facts.test.js +492 -0
- package/test/quality-backfill.test.js +290 -0
- package/test/quality-filter.test.js +113 -4
- package/test/service.test.js +1 -1
- package/test/session-gate.test.js +1 -1
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/wiki-api.test.js +273 -0
- package/test/wiki-inject.test.js +265 -0
- package/test/wiki-partition.test.js +112 -0
- package/test/wiki-plan.test.js +80 -0
- package/test/wiki-queries.test.js +270 -0
- package/test/wiki-scan.test.js +208 -0
- package/test/wiki-store.test.js +190 -0
- package/test/wiki-t0.test.js +380 -0
- package/test/wiki-t1.test.js +235 -0
- package/test/wiki-tools.test.js +176 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// issue #386 第 6 条(面板 bootstrap 入口)与第 7 条(配置键覆盖面)的服务端契约。
|
|
2
|
+
//
|
|
3
|
+
// 这一层要钉住的不是「路由存在」,而是三件容易在改动里悄悄走样的事:
|
|
4
|
+
// 1. /bootstrap 的 policy 是 token(写库),且先判 body 形状、再判能力接线——
|
|
5
|
+
// 顺序反了会把「你没给目录」报成「能力没接线」,把人引去查错的排查方向。
|
|
6
|
+
// 2. /config-keys 只回键名与计数,一个值都不回。apiToken 的名字必须出现(那正是
|
|
7
|
+
// 用户想知道它存在的原因),值必须不出现(那会是一处新泄漏面)。
|
|
8
|
+
// 3. 三份清单是真算出来的:panelEditable = 有默认值的白名单键,
|
|
9
|
+
// fileOnly = schema 声明 − 白名单,undefaulted = 白名单 − 有默认值。
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
13
|
+
import { createStore } from "../src/store.js";
|
|
14
|
+
import { createService } from "../src/service.js";
|
|
15
|
+
import { createApi } from "../src/api.js";
|
|
16
|
+
import { createSettings } from "../src/settings.js";
|
|
17
|
+
import { Config } from "../src/config.js";
|
|
18
|
+
import { describeConfigKeys, schemaLeafKeys, defaultedLeafKeys, flagKeysFromSpec } from "../src/config-keys.js";
|
|
19
|
+
import { bootstrapReply } from "../src/bootstrap-http.js";
|
|
20
|
+
|
|
21
|
+
class FakeRes extends EventEmitter {
|
|
22
|
+
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
23
|
+
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
24
|
+
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function req(path, { method = "GET", body = null } = {}) {
|
|
28
|
+
const r = new EventEmitter();
|
|
29
|
+
r.url = path;
|
|
30
|
+
r.method = method;
|
|
31
|
+
r.headers = {};
|
|
32
|
+
// readBody 是事件式的(data/end),不是 async iterable:必须真的把事件发出去,
|
|
33
|
+
// 否则 handler 会永远挂在那个 Promise 上(第一版测试就是这么挂死的)。
|
|
34
|
+
setImmediate(() => {
|
|
35
|
+
if (body !== null) r.emit("data", Buffer.from(body, "utf8"));
|
|
36
|
+
r.emit("end");
|
|
37
|
+
});
|
|
38
|
+
return r;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setup({ config = {}, apiToken = "" } = {}) {
|
|
42
|
+
const store = createStore(":memory:");
|
|
43
|
+
const service = createService({ store, mirror: null, config });
|
|
44
|
+
const settings = createSettings(store.db);
|
|
45
|
+
const commands = { add: () => {}, remove: () => {}, list: () => [] };
|
|
46
|
+
const routes = [];
|
|
47
|
+
const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
48
|
+
createApi(ctx, service, settings, commands, null, undefined, apiToken, config);
|
|
49
|
+
return { store, service, settings, routes };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BOOTSTRAP = "/api/dsh-mneme/bootstrap";
|
|
53
|
+
const CONFIG_KEYS = "/api/dsh-mneme/config-keys";
|
|
54
|
+
|
|
55
|
+
async function call(routes, path, opts) {
|
|
56
|
+
const route = routes.find((r) => r.path === path);
|
|
57
|
+
assert.ok(route, `route ${path} must be registered`);
|
|
58
|
+
const res = new FakeRes();
|
|
59
|
+
await route.handler(req(path, opts), res);
|
|
60
|
+
return { route, res, json: res.body ? JSON.parse(res.body) : null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- /bootstrap -------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
test("bootstrap route is registered with the token policy", () => {
|
|
66
|
+
const { routes } = setup({ apiToken: "secret-token" });
|
|
67
|
+
const route = routes.find((r) => r.path === BOOTSTRAP);
|
|
68
|
+
assert.ok(route, "route must be registered");
|
|
69
|
+
// 它写库(虽然幂等),所以不能是 open;也不该借 token-write 的读写双义——这条
|
|
70
|
+
// 路由没有 GET 分支,声明 token-write 只会让 SECURITY.md 的开放读清单多一条假的。
|
|
71
|
+
assert.equal(route.policy, "token");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("bootstrap demands a token when one is configured", async () => {
|
|
75
|
+
const { routes } = setup({ apiToken: "secret-token" });
|
|
76
|
+
const { res } = await call(routes, BOOTSTRAP, { method: "POST", body: JSON.stringify({ dir: "/tmp" }) });
|
|
77
|
+
assert.equal(res.statusCode, 401);
|
|
78
|
+
assert.equal(JSON.parse(res.body).error, "unauthorized");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("bootstrap rejects GET with 404 (no read branch exists)", async () => {
|
|
82
|
+
const { routes } = setup();
|
|
83
|
+
const { res } = await call(routes, BOOTSTRAP, { method: "GET" });
|
|
84
|
+
assert.equal(res.statusCode, 404);
|
|
85
|
+
assert.equal(JSON.parse(res.body).error, "not-found");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("bootstrap separates malformed JSON from a non-object body", async () => {
|
|
89
|
+
const { routes } = setup();
|
|
90
|
+
const bad = await call(routes, BOOTSTRAP, { method: "POST", body: "{not json" });
|
|
91
|
+
assert.equal(bad.res.statusCode, 400);
|
|
92
|
+
assert.equal(bad.json.error, "invalid-json");
|
|
93
|
+
// 数组是合法 JSON 但不是「对象」:两个码分开,调用方才知道该改哪里。
|
|
94
|
+
const arr = await call(routes, BOOTSTRAP, { method: "POST", body: "[]" });
|
|
95
|
+
assert.equal(arr.res.statusCode, 400);
|
|
96
|
+
assert.equal(arr.json.error, "invalid-body");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("bootstrap validates dir before it decides whether the capability is wired", async () => {
|
|
100
|
+
// 这条是顺序锁:给一个没有 saveWithDedupe 的装配 + 缺 dir 的 body。若先判能力,
|
|
101
|
+
// 用户会看到「能力没接线」——而真正的问题是他没填目录。
|
|
102
|
+
const store = createStore(":memory:");
|
|
103
|
+
const settings = createSettings(store.db);
|
|
104
|
+
const routes = [];
|
|
105
|
+
const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
106
|
+
createApi(ctx, {}, settings, { add: () => {}, remove: () => {}, list: () => [] }, null, undefined, "", {});
|
|
107
|
+
const { res, json } = await call(routes, BOOTSTRAP, { method: "POST", body: JSON.stringify({}) });
|
|
108
|
+
assert.equal(res.statusCode, 400);
|
|
109
|
+
assert.equal(json.error, "missing-dir", "缺 dir 必须报 missing-dir,不能报 bootstrap-unavailable");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("bootstrap reports the coded error when the directory does not exist", async () => {
|
|
113
|
+
const { routes, service } = setup();
|
|
114
|
+
const { res, json } = await call(routes, BOOTSTRAP, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: JSON.stringify({ dir: "/definitely/not/here/386-bootstrap" })
|
|
117
|
+
});
|
|
118
|
+
assert.equal(res.statusCode, 400);
|
|
119
|
+
assert.equal(json.error, "dir-not-found");
|
|
120
|
+
// 失败路径不能留下半截记忆:目录都没读到,一条都不该落库。
|
|
121
|
+
assert.equal(service.count({}), 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("bootstrapReply survives a logger that throws (the layer never throws)", async () => {
|
|
125
|
+
// 这一层的契约是「从不抛」:warn 裸调时,宿主 logger 自己坏掉会从 catch 块里再抛出去,
|
|
126
|
+
// 而两侧 handler 的外层 try 在 .then 回调里早已退栈——现场就是「一个字节都没回」
|
|
127
|
+
// (#410,share-http.js 的 warnSafely 注释记着同一件事)。守卫必须住在层里。
|
|
128
|
+
const reply = await bootstrapReply({
|
|
129
|
+
bootstrapper: () => Promise.reject(new Error("disk on fire")),
|
|
130
|
+
dir: "/tmp",
|
|
131
|
+
warn: () => { throw new Error("logger is broken too"); }
|
|
132
|
+
});
|
|
133
|
+
assert.equal(reply.status, 500);
|
|
134
|
+
assert.deepEqual(reply.body, { error: "internal" });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("bootstrap imports deterministic artifacts from a real directory", async () => {
|
|
138
|
+
const { routes, service } = setup();
|
|
139
|
+
const os = await import("node:os");
|
|
140
|
+
const fs = await import("node:fs");
|
|
141
|
+
const path = await import("node:path");
|
|
142
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mneme-386-"));
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(path.join(dir, "README.md"), "# sample project\n\nA tiny fixture.\n", "utf8");
|
|
145
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "sample", scripts: { build: "tsc -p ." } }), "utf8");
|
|
146
|
+
const { res, json } = await call(routes, BOOTSTRAP, { method: "POST", body: JSON.stringify({ dir }) });
|
|
147
|
+
assert.equal(res.statusCode, 200);
|
|
148
|
+
assert.ok(json.created >= 2, `expected at least 2 imported rows, got ${JSON.stringify(json)}`);
|
|
149
|
+
assert.equal(json.dir, fs.realpathSync(dir));
|
|
150
|
+
// 「返回成功不等于生效」:去看库里到底有没有那几行。
|
|
151
|
+
const rows = service.list({ limit: 20, includeArchived: false });
|
|
152
|
+
assert.ok(rows.length >= 2, "the response must correspond to rows that actually exist");
|
|
153
|
+
assert.ok(rows.every((m) => (m.tags || []).includes("bootstrap")), "every imported row carries the bootstrap tag");
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// --- /config-keys -----------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
test("config-keys route is open and read-only", async () => {
|
|
162
|
+
const { routes } = setup({ apiToken: "secret-token" });
|
|
163
|
+
const { route, res } = await call(routes, CONFIG_KEYS, { method: "GET" });
|
|
164
|
+
assert.equal(route.policy, "open");
|
|
165
|
+
assert.equal(res.statusCode, 200, "read-only route must not demand a token");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("config-keys rejects non-GET", async () => {
|
|
169
|
+
const { routes } = setup();
|
|
170
|
+
const { res } = await call(routes, CONFIG_KEYS, { method: "POST", body: "{}" });
|
|
171
|
+
assert.equal(res.statusCode, 404);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("config-keys returns key names but never the apiToken value", async () => {
|
|
175
|
+
const { routes } = setup({ config: { apiToken: "super-secret-value" }, apiToken: "super-secret-value" });
|
|
176
|
+
const { res } = await call(routes, CONFIG_KEYS, { method: "GET" });
|
|
177
|
+
assert.equal(res.statusCode, 200);
|
|
178
|
+
// 名字必须在:用户正是要靠这份清单知道「apiToken 只能改配置文件」。
|
|
179
|
+
assert.ok(res.body.includes("apiToken"), "the key name must be listed");
|
|
180
|
+
// 值必须不在:这条断言是这整条路由存在的安全边界。
|
|
181
|
+
assert.ok(!res.body.includes("super-secret-value"), "the value must never leave the server");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("config-keys only reads the schema — it never echoes a configured value", async () => {
|
|
185
|
+
// 上一条锁的是「没漏出去」,这条锁的是「不是靠脱敏才没漏」:即便装了一个
|
|
186
|
+
// 非敏感键的值,响应里也不该出现它——这份清单只谈键名与计数。
|
|
187
|
+
const { routes } = setup({ config: { memoryDir: "/unique/marker/path/386" } });
|
|
188
|
+
const { res } = await call(routes, CONFIG_KEYS, { method: "GET" });
|
|
189
|
+
assert.ok(!res.body.includes("/unique/marker/path/386"), "no configured value may appear in this response");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("config-keys partitions the two source lists without losing anything", async () => {
|
|
193
|
+
const d = describeConfigKeys();
|
|
194
|
+
const declared = schemaLeafKeys();
|
|
195
|
+
const defaulted = defaultedLeafKeys();
|
|
196
|
+
const flags = flagKeysFromSpec();
|
|
197
|
+
assert.equal(d.counts.schemaLeaf, declared.length);
|
|
198
|
+
assert.equal(d.counts.flag, flags.length);
|
|
199
|
+
// 并集必须正好铺满「schema 声明 ∪ 白名单」,一个键都不许漏在三个桶之外。
|
|
200
|
+
const union = new Set([...d.panelEditable, ...d.fileOnly, ...d.undefaulted]);
|
|
201
|
+
const expected = new Set([...declared, ...flags]);
|
|
202
|
+
assert.deepEqual([...union].sort(), [...expected].sort());
|
|
203
|
+
// 两个等式同时成立,才说明三个桶互不相交且覆盖完整。注意第一个等式要把
|
|
204
|
+
// undefaulted 也加进去:dreamProvider / dreamModel 既是 schema 声明的键、又挂在
|
|
205
|
+
// 白名单上,它既不属于 panelEditable(没默认值)也不属于 fileOnly(白名单有它),
|
|
206
|
+
// 只落在 undefaulted 这一个桶里。
|
|
207
|
+
assert.equal(d.panelEditable.length + d.fileOnly.length + d.undefaulted.length, declared.length);
|
|
208
|
+
assert.equal(d.panelEditable.length + d.undefaulted.length, flags.length);
|
|
209
|
+
// 白名单里每个键都必须在 schema 里声明过——否则那是登记了一个不存在的键。
|
|
210
|
+
for (const key of flags) assert.ok(expected.has(key), `flag ${key} is not declared in the schema`);
|
|
211
|
+
// 有默认值的键必须都被 schema 声明覆盖(反向包含,防 schema 漏声明)。
|
|
212
|
+
for (const key of defaulted) assert.ok(declared.includes(key), `defaulted key ${key} is missing from the schema declaration`);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("config-keys flags keys that the whitelist registers without a schema default", async () => {
|
|
216
|
+
const d = describeConfigKeys();
|
|
217
|
+
// dreamProvider / dreamModel 在 settings.js 白名单里,但 config.js 只有
|
|
218
|
+
// z.string().description(...) 没有 .default()。面板因此有个永远不进 effective 的
|
|
219
|
+
// 输入框——这正是 #386 第 7 条说的「不对称」,必须被告知而不是被悄悄抹平。
|
|
220
|
+
assert.deepEqual(d.undefaulted.sort(), ["dreamModel", "dreamProvider"]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("config-keys lists file-only keys that users actually hit (apiToken, memoryDir, maxInjectedItems)", async () => {
|
|
224
|
+
const d = describeConfigKeys();
|
|
225
|
+
// apiToken 是最要紧的一个:它没有默认值,所以「拿解析结果当 schema」的写法会把它
|
|
226
|
+
// 整个漏掉——第一版正是这么错的,这条用例是那个错的回归锁。
|
|
227
|
+
assert.ok(d.fileOnly.includes("apiToken"), "apiToken must be visible as file-only");
|
|
228
|
+
// 另外两个是 issue 原文点名的键:用户能理解、却在面板上找不到。
|
|
229
|
+
assert.ok(d.fileOnly.includes("memoryDir"));
|
|
230
|
+
assert.ok(d.fileOnly.includes("maxInjectedItems"));
|
|
231
|
+
// 反向:一个面板确实能改的键,绝不能被列进 fileOnly。
|
|
232
|
+
assert.ok(!d.fileOnly.includes("autoInject"));
|
|
233
|
+
});
|
package/test/config.test.js
CHANGED
|
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Config } from "../src/config.js";
|
|
6
|
+
import { isCodingTask } from "../src/service.js";
|
|
6
7
|
|
|
7
8
|
test("session incognito is opt-in, with a default-on subordinate selector", () => {
|
|
8
9
|
const cfg = Config({});
|
|
@@ -72,3 +73,81 @@ test("startup probe: the reranker module never statically imports transformers/o
|
|
|
72
73
|
"loader 不得静态导入 transformers,否则惰性加载就失效了"
|
|
73
74
|
);
|
|
74
75
|
});
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------- codingKeywords 工程协作词(2026-11-06)
|
|
78
|
+
// 生产日志里「合并 / 开 PR / 看 CI / 发版 / 验收 / 冲突 / 分支 / issue」这类
|
|
79
|
+
// 工程协作话在原 56 项词表里一个都不命中,被判成非编码任务,于是
|
|
80
|
+
// rejected_solution / pitfall / constraint 三类编码记忆被 codingGate 整类滤掉。
|
|
81
|
+
// 实测这类 query 占非编码调用的 17.1~17.8%。本组用例锁住补词后的词表与行为。
|
|
82
|
+
|
|
83
|
+
test("codingKeywords 覆盖工程协作动作,且不含歧义词", () => {
|
|
84
|
+
const { codingKeywords } = Config({});
|
|
85
|
+
|
|
86
|
+
// 这 34 项就是补进去的工程协作词,一个都不能少(少了就等于白改)。
|
|
87
|
+
// 不另加「长度必须是 90」的断言:那是把「逐词覆盖」和「不多不少」两个语义
|
|
88
|
+
// 绑在一处,删一个词会同时触发两条断言,看报告的人会误以为两条用例各自
|
|
89
|
+
// 独立兜底。逐词断言已经足够——多出来的词由下面的「不得含」三条反向兜住。
|
|
90
|
+
//
|
|
91
|
+
// 「审评」必须与「评审」并列:她真实说的是「等审评」「看CI和审评」(8 条 /
|
|
92
|
+
// 779 次),「评审」只有 4 条 / 420 次。同义不同形,子串匹配两套都得分开放。
|
|
93
|
+
const engineering = [
|
|
94
|
+
"提交", "合并", "分支", "冲突", "验收", "审查", "评审", "审评", "发版",
|
|
95
|
+
"仓库", "回滚", "推送", "拉取", "变基", "上线", "打包", "迁移",
|
|
96
|
+
"字段", "鉴权", "超时", "并发", "闪退", "白屏", "端口", "进程",
|
|
97
|
+
"守护", "定时任务", "索引", "缓存", "死锁", "内存泄漏", "日志",
|
|
98
|
+
"版本号", "环境变量"
|
|
99
|
+
];
|
|
100
|
+
for (const kw of engineering) {
|
|
101
|
+
assert.ok(codingKeywords.includes(kw), `缺少工程协作词「${kw}」`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 刻意不加的两个:
|
|
105
|
+
// 「事务」唯一命中是政治学简答「公共事务委托给企业和社会组织」,加进去
|
|
106
|
+
// 会把考研答题场景判成编码任务,方向正好相反。
|
|
107
|
+
// 「依赖包」纯冗余——「依赖」已在表内,它永远不会单独生效。
|
|
108
|
+
assert.ok(!codingKeywords.includes("事务"), "不得加「事务」:会把政治学简答判成编码任务");
|
|
109
|
+
assert.ok(!codingKeywords.includes("依赖包"), "不得加「依赖包」:「依赖」已在表内,纯冗余");
|
|
110
|
+
|
|
111
|
+
// 无重复
|
|
112
|
+
assert.equal(new Set(codingKeywords).size, codingKeywords.length, "词表不得有重复项");
|
|
113
|
+
|
|
114
|
+
// 英文词一律不加:isCodingTask 是纯子串匹配,无词边界的短英文词会误命中。
|
|
115
|
+
// 实测 pr 命中 69 条/6002 次,其中无词边界 30 条/3477 次(57.9%)是
|
|
116
|
+
// C:/Program File/GitHub CLI、pnpm 路径、prompt 这类文本。
|
|
117
|
+
for (const kw of ["pr", "ci", "push", "pull", "rebase", "revert", "diff"]) {
|
|
118
|
+
assert.ok(!codingKeywords.includes(kw), `不得加英文短词「${kw}」:无词边界会误命中`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("工程协作话被判成编码任务,考研答题话仍判成非编码", () => {
|
|
123
|
+
// 走生产的 isCodingTask,不在这里另写一份判断逻辑。
|
|
124
|
+
// 此前这条用例自己写 const isCoding = (t) => codingKeywords.some(...),
|
|
125
|
+
// 测的是「词表内容对不对」,不是「生产函数怎么用它」——实测把
|
|
126
|
+
// src/service.js 的 isCodingTask 改成永远 return false(整个编码门废掉),
|
|
127
|
+
// 全仓 177 个相关用例仍然全绿。所以这里必须 import 真函数。
|
|
128
|
+
const { codingKeywords } = Config({});
|
|
129
|
+
|
|
130
|
+
// 这些是她真实说过的工程协作话,补词前全部被判成非编码
|
|
131
|
+
for (const q of [
|
|
132
|
+
"提交这个issue然后你在主分支的基础上开分支修",
|
|
133
|
+
"先回帖再开分支。",
|
|
134
|
+
"合并",
|
|
135
|
+
"看看#536,解决冲突",
|
|
136
|
+
"看看ci和评审",
|
|
137
|
+
"A+B,今天就能发版",
|
|
138
|
+
"等审评。你看看#35那个pr。",
|
|
139
|
+
"按照你说的,先合并完前面的再开#493"
|
|
140
|
+
]) {
|
|
141
|
+
assert.equal(isCodingTask(q, codingKeywords), true, `「${q}」应判成编码任务`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 考研答题话:补词后仍然不能被判成编码任务,否则她的答题场景会注入编码踩坑
|
|
145
|
+
for (const q of [
|
|
146
|
+
"1.B,2.这个就是……政府职能转变,公共事务委托给企业和社会组织",
|
|
147
|
+
"背95单词",
|
|
148
|
+
"英语阅读 1 篇(2016 起正序)",
|
|
149
|
+
"631 专业课块 · 15:30–17:00"
|
|
150
|
+
]) {
|
|
151
|
+
assert.equal(isCodingTask(q, codingKeywords), false, `「${q}」应判成非编码任务`);
|
|
152
|
+
}
|
|
153
|
+
});
|
package/test/contrast.test.js
CHANGED
|
@@ -233,13 +233,16 @@ test("面板引用的宿主 token 必须真实存在(幻影 token 回归锁)
|
|
|
233
233
|
assert.deepEqual(unknown, [], "面板引用了宿主快照里没有的 token(幻影名或快照过期):" + unknown.join(", "));
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
//
|
|
237
|
-
// body(.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap
|
|
238
|
-
// portal 到宿主侧边栏(.mneme-topentry
|
|
239
|
-
//
|
|
240
|
-
|
|
236
|
+
// 变量层必须挂满五个渲染根:面板不只长在 .mneme-x 下——老宿主上记忆库 sheet
|
|
237
|
+
// portal 到 body(.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap)、
|
|
238
|
+
// 顶部入口 portal 到宿主侧边栏(.mneme-topentry);新宿主的侧栏图标长在宿主
|
|
239
|
+
// panelGlyph 里、不在 .mneme-x 子树内(.mneme-paneliglyph),它挂着冲突红点,
|
|
240
|
+
// 必须吃到 --mneme-danger。
|
|
241
|
+
// 少挂一个,那个根下的文字/图形就退回宿主的低对比度 token,而且上面的主锁
|
|
242
|
+
// **照样全绿**(主锁只算颜色,不知道变量有没有继承到)。
|
|
243
|
+
test("面板语义色层覆盖五个渲染根", () => {
|
|
241
244
|
const sel = layer.sel;
|
|
242
|
-
assert.equal(sel, ".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry");
|
|
245
|
+
assert.equal(sel, ".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry,.mneme-paneliglyph");
|
|
243
246
|
});
|
|
244
247
|
|
|
245
248
|
// 图形色的对比度(WCAG 1.4.11,非文字 3:1)。上面那条主锁只看 `color:`,
|
|
@@ -275,7 +278,11 @@ const GRAPHICS = [
|
|
|
275
278
|
{ sel: ".mneme-heatcell.l1", prop: "background" },
|
|
276
279
|
{ sel: ".mneme-heatcell.l2", prop: "background" },
|
|
277
280
|
{ sel: ".mneme-heatcell.l3", prop: "background" },
|
|
278
|
-
{ sel: ".mneme-heatcell.l4", prop: "background" }
|
|
281
|
+
{ sel: ".mneme-heatcell.l4", prop: "background" },
|
|
282
|
+
// 8 周沉淀的两根柱:高度差就是「进来的 vs 稳住的」这层信息本身(新增高、
|
|
283
|
+
// 巩固矮=库在膨胀),柱子掉回浅灰等于把结论擦掉,所以按图形色 3:1 锁。
|
|
284
|
+
{ sel: ".mneme-sinkbar--added", prop: "background" },
|
|
285
|
+
{ sel: ".mneme-sinkbar--merged", prop: "background" }
|
|
279
286
|
];
|
|
280
287
|
test("图形色(热度/打卡/连线)在浅深两主题下都达到 3:1", () => {
|
|
281
288
|
const bySel = new Map(rules.map((r) => [r.sel, r]));
|
package/test/document.test.js
CHANGED
|
@@ -227,6 +227,38 @@ test("re-registering a document whose evidence was absorbed keeps the references
|
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
// #593 遗留缺口(#593 把 pin 从「按类型推断」改成显式存储后新出现的旁路):
|
|
231
|
+
// 上一版只问 pinnedTypes.has(row.type),于是被显式钉住的 pattern / decision 在升格
|
|
232
|
+
// 吸收时仍被 setArchived —— 用户钉过的行被文档悄悄收走,pin 语义(「这条我要长期
|
|
233
|
+
// 带着」)当场失效。这条锁住「显式 pin 表是第二判据」,且用 pattern 类型(它不在
|
|
234
|
+
// PINNED_MEMORY_TYPES 里),否则用例会被类型口径顺手护住、测不到缺口。
|
|
235
|
+
test("explicitly pinned evidence rows survive absorption even when their type is not in the pinned pool", async () => {
|
|
236
|
+
const { store, service, close } = makeService({ documentMemoryEnabled: true });
|
|
237
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
238
|
+
try {
|
|
239
|
+
const pinnedPattern = service.saveWithDedupe({ type: "pattern", title: "P", content: "用户反复这样操作" }).memory;
|
|
240
|
+
const plainPattern = service.saveWithDedupe({ type: "pattern", title: "P2", content: "另一条没钉的模式" }).memory;
|
|
241
|
+
store.pinMemory(pinnedPattern.id, { note: "显式钉住" });
|
|
242
|
+
const p = await writeDoc("pin-survives.md");
|
|
243
|
+
|
|
244
|
+
const res = await service.registerDocument({
|
|
245
|
+
path: p, title: "PinSurvives", summary: "s", evidence: [pinnedPattern.id, plainPattern.id]
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
assert.equal(res.evidence_kept, 2);
|
|
249
|
+
assert.equal(res.evidence_archived, 1, "只收走没被钉的那条");
|
|
250
|
+
assert.equal(
|
|
251
|
+
service.getById(pinnedPattern.id).archived,
|
|
252
|
+
false,
|
|
253
|
+
"被显式 pin 的行不能被升格吸收静默归档(pin 表是第二判据,不能只按类型猜)"
|
|
254
|
+
);
|
|
255
|
+
assert.equal(service.getById(plainPattern.id).archived, true, "没钉的同类型行照旧被吸收(补丁不放宽口径)");
|
|
256
|
+
} finally {
|
|
257
|
+
cleanup();
|
|
258
|
+
close();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
230
262
|
test("registrar without the pinned pool wiring archives nothing (fail-safe)", async () => {
|
|
231
263
|
// 拿不到 pinned 集合就不做这一步:宁可少做,也不能把保真池当普通行收走。
|
|
232
264
|
const store = createStore(":memory:");
|