@modusensus/dsh-mneme 0.13.0 → 0.14.0-beta.1
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 +6 -4
- package/bin/dsh-mneme-choro.mjs +9 -1
- package/docs/CONFIGURATION.md +5 -3
- package/lib/api.js +211 -5
- package/lib/client.js +1240 -354
- package/lib/config.js +27 -0
- package/lib/dream/decisions.js +39 -1
- package/lib/dream/sleep.js +146 -6
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +123 -8
- package/lib/hot-memory.js +14 -2
- package/lib/index.js +31 -1
- package/lib/inject.js +14 -2
- package/lib/lang.js +11 -0
- package/lib/service.js +76 -4
- package/lib/settings.js +16 -1
- package/lib/store.js +238 -5
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/queries.js +168 -2
- package/lib/wiki/render.js +74 -0
- package/lib/wiki/t0.js +43 -6
- package/lib/wiki/t1.js +138 -31
- package/package.json +15 -1
- package/scripts/release-docs.mjs +7 -1
- package/scripts/release-notes.mjs +4 -1
- package/src/api.js +211 -5
- package/src/config.js +27 -0
- package/src/dream/decisions.js +39 -1
- package/src/dream/sleep.js +146 -6
- package/src/dream/triage.js +139 -0
- package/src/dream.js +123 -8
- package/src/hot-memory.js +14 -2
- package/src/index.js +31 -1
- package/src/inject.js +14 -2
- package/src/lang.js +11 -0
- package/src/service.js +76 -4
- package/src/settings.js +16 -1
- package/src/store.js +238 -5
- package/src/wiki/claims.js +267 -0
- package/src/wiki/queries.js +168 -2
- package/src/wiki/render.js +74 -0
- package/src/wiki/t0.js +43 -6
- package/src/wiki/t1.js +138 -31
- package/test/api.test.js +11 -1
- package/test/client.test.js +275 -34
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/contrast.test.js +124 -12
- package/test/dream-baseline-clamp.test.js +174 -0
- package/test/graph-api.test.js +124 -0
- package/test/hot-memory.test.js +48 -0
- package/test/inject-parent-gate.test.js +6 -3
- package/test/inject.test.js +59 -0
- package/test/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -0
- package/test/release-docs.test.js +20 -0
- package/test/release-notes.test.js +25 -0
- package/test/security-doc.test.js +10 -0
- package/test/sleep.test.js +37 -0
- package/test/wiki-api.test.js +110 -3
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +121 -0
- package/test/wiki-t0.test.js +77 -1
- package/test/wiki-t1.test.js +226 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createStore } from "../src/store.js";
|
|
4
|
+
import { Config } from "../src/config.js";
|
|
5
|
+
import { triagePair, recordLedgerTriage, LEDGER_TIERS } from "../src/dream/triage.js";
|
|
6
|
+
|
|
7
|
+
// Issue #641(beta 第一阶段):冲突分诊账本。锁三件事,防的回归是:
|
|
8
|
+
// ① 分诊判据漂移:四档的时间/相似度边界被「顺手调一下」而没有任何记录——
|
|
9
|
+
// shadow 期分布与真实库 46/20/5/27 的可比性全靠判据稳定,边界一动,
|
|
10
|
+
// shadow 周就白跑(每条边界用例的注释都写明它锚的真实库依据);
|
|
11
|
+
// ② 账本不幂等/首见时刻被刷新:同一对每轮重写 first_seen_at,挂账时长与
|
|
12
|
+
// 同召率统计全部失真;
|
|
13
|
+
// ③ 记账反噬相位或误入人工队列:shadow 档的写入是纯测量,抛错必须只计
|
|
14
|
+
// failed、绝不打断巩固,也绝不涨 countConflictPending 的面板徽标。
|
|
15
|
+
|
|
16
|
+
const mem = (id, over = {}) => ({
|
|
17
|
+
id,
|
|
18
|
+
title: `t-${id}`,
|
|
19
|
+
content: `c-${id}`,
|
|
20
|
+
created_at: "2026-11-01T00:00:00.000Z",
|
|
21
|
+
agent_scope: undefined,
|
|
22
|
+
workspace_scope: undefined,
|
|
23
|
+
...over
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const HOUR = 3_600_000;
|
|
27
|
+
const DAY = 86_400_000;
|
|
28
|
+
|
|
29
|
+
// ---- triagePair:四档判据与边界 ----
|
|
30
|
+
|
|
31
|
+
test("issue#641 跨作用域对判 cross-scope-dup(任一维键不同即算,#170 口径)", () => {
|
|
32
|
+
const r1 = triagePair(mem("a", { agent_scope: "x" }), mem("b", { agent_scope: "y" }), 0.9);
|
|
33
|
+
assert.equal(r1.tier, "cross-scope-dup");
|
|
34
|
+
assert.equal(r1.signals.crossScope, true);
|
|
35
|
+
// workspace 单维不同也算——与 sleep.js isCrossScopePair 同一判据
|
|
36
|
+
const r2 = triagePair(mem("a", { workspace_scope: "w1" }), mem("b", { workspace_scope: "w2" }), 0.86);
|
|
37
|
+
assert.equal(r2.tier, "cross-scope-dup");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("issue#641 两侧都未标注不算跨作用域,但 annotated=false 显式落账(#616 的洞在测量层可见)", () => {
|
|
41
|
+
const r = triagePair(mem("a"), mem("b"), 0.91, { now: Date.UTC(2026, 10, 1, 2) });
|
|
42
|
+
assert.equal(r.tier, "dup");
|
|
43
|
+
assert.equal(r.signals.crossScope, false);
|
|
44
|
+
assert.equal(r.signals.annotated, false);
|
|
45
|
+
// 一侧标注一侧未标注:scopeKeyOf(null) !== scopeKeyOf("x") → 跨作用域
|
|
46
|
+
const r2 = triagePair(mem("a", { agent_scope: "x" }), mem("b"), 0.86);
|
|
47
|
+
assert.equal(r2.tier, "cross-scope-dup");
|
|
48
|
+
assert.equal(r2.signals.annotated, false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("issue#641 同日双录(<24h)+ sim≥dupMin 判 dup;真实库 76% 的对落这一档的依据", () => {
|
|
52
|
+
const a = mem("a", { created_at: "2026-11-01T00:00:00.000Z" });
|
|
53
|
+
const b = mem("b", { created_at: "2026-11-01T02:00:00.000Z" });
|
|
54
|
+
const r = triagePair(a, b, 0.91, { now: Date.UTC(2026, 10, 1) });
|
|
55
|
+
assert.equal(r.tier, "dup");
|
|
56
|
+
assert.equal(r.signals.gapDays, 0.083, "gapDays 落账时四舍五入到 3 位(audit JSON 保持干净)");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("issue#641 dup 边界:sim 恰在 dupMin 判 dup(>=),低一分保守归 conflict", () => {
|
|
60
|
+
const a = mem("a", { created_at: "2026-11-01T00:00:00.000Z" });
|
|
61
|
+
const b = mem("b", { created_at: "2026-11-01T01:00:00.000Z" });
|
|
62
|
+
assert.equal(triagePair(a, b, 0.9, { now: Date.UTC(2026, 10, 1) }).tier, "dup");
|
|
63
|
+
assert.equal(triagePair(a, b, 0.899, { now: Date.UTC(2026, 10, 1) }).tier, "conflict");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("issue#641 时间边界:差恰好 24h 不算 dup(1-7 天的模糊带宁保守归 conflict);≥7 天判 evolution", () => {
|
|
67
|
+
const a = mem("a", { created_at: "2026-11-01T00:00:00.000Z" });
|
|
68
|
+
const day1 = mem("b", { created_at: "2026-11-02T00:00:00.000Z" });
|
|
69
|
+
assert.equal(triagePair(a, day1, 0.95, { now: Date.UTC(2026, 10, 1) }).tier, "conflict");
|
|
70
|
+
const day30 = mem("b", { created_at: "2026-12-01T00:00:00.000Z" });
|
|
71
|
+
// evolution 的判据只有时间——sim 再高也先走取代链候选(enforce 阶段接线)
|
|
72
|
+
const r = triagePair(a, day30, 0.99, { now: Date.UTC(2026, 10, 1) });
|
|
73
|
+
assert.equal(r.tier, "evolution");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("issue#641 created_at 缺失不下结论:归 conflict、gapDays=null,不编造时间信号", () => {
|
|
77
|
+
const r = triagePair(mem("a", { created_at: undefined }), mem("b"), 0.91);
|
|
78
|
+
assert.equal(r.tier, "conflict");
|
|
79
|
+
assert.equal(r.signals.gapDays, null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("issue#641 dupMin 可经 opts 覆盖(配置键 conflictLedgerDupMinSimilarity 的直通口)", () => {
|
|
83
|
+
const a = mem("a", { created_at: "2026-11-01T00:00:00.000Z" });
|
|
84
|
+
const b = mem("b", { created_at: "2026-11-01T01:00:00.000Z" });
|
|
85
|
+
assert.equal(triagePair(a, b, 0.87, { now: Date.UTC(2026, 10, 1), dupMinSimilarity: 0.86 }).tier, "dup");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- recordLedgerTriage:best-effort 记账 ----
|
|
89
|
+
|
|
90
|
+
test("issue#641 分诊记账:逐对落账、tally 与档位计数一致", async () => {
|
|
91
|
+
const saved = [];
|
|
92
|
+
const service = { saveConflictLedgerEntry: (r) => saved.push(r) };
|
|
93
|
+
const pairs = [
|
|
94
|
+
{ a: mem("a", { created_at: "2026-11-01T00:00:00.000Z" }), b: mem("b", { created_at: "2026-11-01T01:00:00.000Z" }), similarity: 0.91 },
|
|
95
|
+
{ a: mem("c", { agent_scope: "x" }), b: mem("d", { agent_scope: "y" }), similarity: 0.86 },
|
|
96
|
+
{ a: mem("e", { created_at: "2026-10-01T00:00:00.000Z" }), b: mem("f", { created_at: "2026-11-01T00:00:00.000Z" }), similarity: 0.8 },
|
|
97
|
+
{ a: mem("g", { created_at: "2026-11-02T00:00:00.000Z" }), b: mem("h", { created_at: "2026-11-01T00:00:00.000Z" }), similarity: 0.86 }
|
|
98
|
+
];
|
|
99
|
+
const tally = await recordLedgerTriage(service, "run-1", pairs, { now: Date.UTC(2026, 10, 1) });
|
|
100
|
+
assert.deepEqual(tally, { dup: 1, "cross-scope-dup": 1, evolution: 1, conflict: 1, recorded: 4, failed: 0, truncatedAt: null });
|
|
101
|
+
assert.equal(saved.length, 4);
|
|
102
|
+
assert.equal(saved[0].tier, "dup");
|
|
103
|
+
assert.equal(saved[0].run_id, "run-1");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("issue#641 记账失败只计 failed 不打断:纯测量绝不反噬巩固相位", async () => {
|
|
107
|
+
let calls = 0;
|
|
108
|
+
const service = {
|
|
109
|
+
saveConflictLedgerEntry: () => {
|
|
110
|
+
calls++;
|
|
111
|
+
if (calls === 2) throw new Error("db busy");
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const pairs = [1, 2, 3].map((i) => ({ a: mem(`a${i}`), b: mem(`b${i}`), similarity: 0.91 }));
|
|
115
|
+
const warns = [];
|
|
116
|
+
const tally = await recordLedgerTriage(service, "run-1", pairs, {
|
|
117
|
+
now: Date.UTC(2026, 10, 1),
|
|
118
|
+
logger: { warn: (m) => warns.push(m) }
|
|
119
|
+
});
|
|
120
|
+
assert.equal(tally.recorded, 2);
|
|
121
|
+
assert.equal(tally.failed, 1);
|
|
122
|
+
assert.equal(warns.length, 1, "失败要留下 warn,不是静默吞");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("issue#641 记账分块:大池逐块落账不丢样本(shadow 分母必须完整)", async () => {
|
|
126
|
+
const saved = [];
|
|
127
|
+
const service = { saveConflictLedgerEntry: (r) => saved.push(r) };
|
|
128
|
+
const pairs = Array.from({ length: 120 }, (_, i) => ({
|
|
129
|
+
a: mem(`a${i}`, { created_at: "2026-11-01T00:00:00.000Z" }),
|
|
130
|
+
b: mem(`b${i}`, { created_at: "2026-11-01T01:00:00.000Z" }),
|
|
131
|
+
similarity: 0.91
|
|
132
|
+
}));
|
|
133
|
+
const tally = await recordLedgerTriage(service, "run-1", pairs, { now: Date.UTC(2026, 10, 1) });
|
|
134
|
+
assert.equal(tally.recorded, 120, "跨三块全部落账,无截断");
|
|
135
|
+
assert.equal(tally.dup, 120);
|
|
136
|
+
assert.equal(tally.truncatedAt, null);
|
|
137
|
+
assert.equal(saved.length, 120);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("issue#641 记账分块:取消即停、truncatedAt 可见,绝不静默截断观察样本", async () => {
|
|
141
|
+
const service = { saveConflictLedgerEntry: () => {} };
|
|
142
|
+
const pairs = Array.from({ length: 120 }, (_, i) => ({
|
|
143
|
+
a: mem(`a${i}`, { created_at: "2026-11-01T00:00:00.000Z" }),
|
|
144
|
+
b: mem(`b${i}`, { created_at: "2026-11-01T01:00:00.000Z" }),
|
|
145
|
+
similarity: 0.91
|
|
146
|
+
}));
|
|
147
|
+
// 首块前取消:零写入,truncatedAt=0
|
|
148
|
+
const stopAtStart = await recordLedgerTriage(service, "run-1", pairs, { now: Date.UTC(2026, 10, 1), signal: { aborted: true } });
|
|
149
|
+
assert.equal(stopAtStart.recorded, 0, "取消在首块前生效");
|
|
150
|
+
assert.equal(stopAtStart.truncatedAt, 0);
|
|
151
|
+
// 中途取消:getter 在首块(50 条)落完后翻转 → 第二块前停,块内不弃样本
|
|
152
|
+
const saved = [];
|
|
153
|
+
let flipped = false;
|
|
154
|
+
const midService = {
|
|
155
|
+
saveConflictLedgerEntry: (r) => {
|
|
156
|
+
saved.push(r);
|
|
157
|
+
if (saved.length >= 10) flipped = true;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const mid = await recordLedgerTriage(midService, "run-2", pairs, {
|
|
161
|
+
now: Date.UTC(2026, 10, 1),
|
|
162
|
+
signal: { get aborted() { return flipped; } }
|
|
163
|
+
});
|
|
164
|
+
assert.equal(mid.recorded, 50, "第一块完整落账(块内不检查取消)");
|
|
165
|
+
assert.equal(mid.truncatedAt, 50, "取消点对可见,余下的对下一轮重见");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---- conflict_ledger 存储契约 ----
|
|
169
|
+
|
|
170
|
+
test("issue#641 conflict_ledger:键归一化 + 读回 + 人工队列零污染", () => {
|
|
171
|
+
const store = createStore(":memory:");
|
|
172
|
+
store.saveConflictLedgerEntry({
|
|
173
|
+
run_id: "run-1", memory_a: "b-id", memory_b: "a-id", tier: "dup",
|
|
174
|
+
similarity: 0.91, signals: { crossScope: false, annotated: false, gapDays: 0.5, dupMin: 0.9, similarity: 0.91 }
|
|
175
|
+
});
|
|
176
|
+
const rows = store.listConflictLedger();
|
|
177
|
+
assert.equal(rows.length, 1);
|
|
178
|
+
assert.equal(rows[0].memory_a, "a-id", "键按字典序归一化——反着传也是同一行");
|
|
179
|
+
assert.equal(rows[0].tier, "dup");
|
|
180
|
+
assert.equal(rows[0].status, "open");
|
|
181
|
+
assert.deepEqual(JSON.parse(rows[0].signals), { crossScope: false, annotated: false, gapDays: 0.5, dupMin: 0.9, similarity: 0.91 });
|
|
182
|
+
assert.equal(store.countConflictPending(), 0, "分诊账本是测量记录,绝不涨人工队列徽标");
|
|
183
|
+
assert.equal(store.countConflictLedger(), 1);
|
|
184
|
+
store.close();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("issue#641 conflict_ledger:重复观察是刷新不是新行,first_* 保留首次检出", () => {
|
|
188
|
+
const store = createStore(":memory:");
|
|
189
|
+
store.saveConflictLedgerEntry({ run_id: "run-1", memory_a: "a", memory_b: "b", tier: "conflict", similarity: 0.86 });
|
|
190
|
+
const first = store.listConflictLedger()[0];
|
|
191
|
+
// 同一对后来判成 dup(两侧内容变了 tier 就该变——tier 是观察结论,不是身份)
|
|
192
|
+
store.saveConflictLedgerEntry({ run_id: "run-2", memory_a: "b", memory_b: "a", tier: "dup", similarity: 0.92 });
|
|
193
|
+
const rows = store.listConflictLedger();
|
|
194
|
+
assert.equal(rows.length, 1, "同一对只留一行");
|
|
195
|
+
assert.equal(rows[0].tier, "dup");
|
|
196
|
+
assert.equal(rows[0].first_run_id, "run-1", "首见 run 保留——挂账时长锚在首次检出");
|
|
197
|
+
assert.equal(rows[0].last_run_id, "run-2");
|
|
198
|
+
assert.ok(rows[0].first_seen_at < rows[0].last_seen_at, "单调时钟下两次观察严格递增");
|
|
199
|
+
assert.equal(typeof first.first_seen_at, "string");
|
|
200
|
+
store.close();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("issue#641 conflict_ledger:状态迁移合法值校验 + 未知对返回 false", () => {
|
|
204
|
+
const store = createStore(":memory:");
|
|
205
|
+
store.saveConflictLedgerEntry({ run_id: "run-1", memory_a: "a", memory_b: "b", tier: "conflict" });
|
|
206
|
+
assert.equal(store.setConflictLedgerStatus("b", "a", "zombied"), true, "反着传也能命中(归一化)");
|
|
207
|
+
assert.equal(store.listConflictLedger({ status: "zombied" }).length, 1);
|
|
208
|
+
assert.throws(() => store.setConflictLedgerStatus("a", "b", "not-a-status"), /unknown status/, "非法状态是编程错误,抛错不落库");
|
|
209
|
+
assert.equal(store.setConflictLedgerStatus("x", "y", "open"), false, "未知对返回 false 不抛");
|
|
210
|
+
assert.throws(() => store.saveConflictLedgerEntry({ memory_a: "a", memory_b: "b", tier: "weird" }), /unknown tier/);
|
|
211
|
+
store.close();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("issue#641 conflict_ledger:tier/status 过滤口径(shadow 周分析的查询面)", () => {
|
|
215
|
+
const store = createStore(":memory:");
|
|
216
|
+
store.saveConflictLedgerEntry({ memory_a: "a", memory_b: "b", tier: "dup" });
|
|
217
|
+
store.saveConflictLedgerEntry({ memory_a: "c", memory_b: "d", tier: "conflict" });
|
|
218
|
+
store.saveConflictLedgerEntry({ memory_a: "e", memory_b: "f", tier: "conflict" });
|
|
219
|
+
assert.equal(store.countConflictLedger({ tier: "conflict" }), 2);
|
|
220
|
+
assert.equal(store.countConflictLedger({ tier: "dup" }), 1);
|
|
221
|
+
assert.equal(store.listConflictLedger({ tier: "dup" })[0].memory_a, "a");
|
|
222
|
+
store.close();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---- 配置面 ----
|
|
226
|
+
|
|
227
|
+
test("issue#641 conflictLedgerMode 默认 off、枚举拒绝未定义档位(不存在设了没行为的档位)", () => {
|
|
228
|
+
const cfg = Config({});
|
|
229
|
+
assert.equal(cfg.conflictLedgerMode, "off");
|
|
230
|
+
assert.equal(cfg.conflictLedgerDupMinSimilarity, 0.9);
|
|
231
|
+
// enforce 在 beta 第二阶段进枚举(处置接线),但清单之外的值仍然要被拒
|
|
232
|
+
assert.throws(() => Config({ conflictLedgerMode: "yolo" }), /conflictLedgerMode/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("issue#641 LEDGER_TIERS 与 store 侧白名单一致(两处清单不许漂移)", () => {
|
|
236
|
+
const store = createStore(":memory:");
|
|
237
|
+
for (const tier of LEDGER_TIERS) {
|
|
238
|
+
store.saveConflictLedgerEntry({ memory_a: `a-${tier}`, memory_b: `b-${tier}`, tier });
|
|
239
|
+
}
|
|
240
|
+
assert.equal(store.countConflictLedger(), LEDGER_TIERS.length, "triage.js 的每一档都能落账");
|
|
241
|
+
store.close();
|
|
242
|
+
});
|
package/test/contrast.test.js
CHANGED
|
@@ -48,8 +48,15 @@ const HOST_TOKENS = {
|
|
|
48
48
|
"--dsw-alias-interactive-bg-active": { light: "#2631481a", dark: "#ffffff24" },
|
|
49
49
|
"--dsw-alias-scrollbar-bg-l2": { light: "#e5e5e5", dark: "#545557" },
|
|
50
50
|
"--dsw-alias-scrollbar-hover-l2": { light: "#d4d4d4", dark: "#65676b" },
|
|
51
|
+
// static-* 在两个主题下同值(宿主定义在 :root),图表色盘就是靠它们做色相来源,
|
|
52
|
+
// 再由 label 混色让深浅两套色阶自动成立。
|
|
53
|
+
"--dsw-static-amber-500": { light: "#f59e0b", dark: "#f59e0b" },
|
|
51
54
|
"--dsw-static-amber-600": { light: "#dd8629", dark: "#dd8629" },
|
|
55
|
+
"--dsw-static-blue-500": { light: "#3b82f6", dark: "#3b82f6" },
|
|
52
56
|
"--dsw-static-green-500": { light: "#22c55e", dark: "#22c55e" },
|
|
57
|
+
"--dsw-static-neutral-500": { light: "#7f8287", dark: "#7f8287" },
|
|
58
|
+
"--dsw-static-red-500": { light: "#ef4444", dark: "#ef4444" },
|
|
59
|
+
"--dsw-static-violet-400": { light: "#a78bfa", dark: "#a78bfa" },
|
|
53
60
|
// 真实存在但**不是颜色**的宿主 token:只参与上面的「名字必须存在」检查。
|
|
54
61
|
"--dsw-radius-md": { light: null, dark: null },
|
|
55
62
|
"--dsw-focus-ring-width": { light: null, dark: null },
|
|
@@ -139,22 +146,38 @@ function resolveValue(raw, theme) {
|
|
|
139
146
|
}
|
|
140
147
|
return v.includes("\u00acMISSING\u00ac") ? null : v.trim();
|
|
141
148
|
}
|
|
149
|
+
// 按顶层逗号切。color-mix 会嵌套(图表色盘先跟中性灰压彩度、再跟 label 适配主题),
|
|
150
|
+
// 早期用非贪婪正则会从内层那个「62%,」处劈开,两层都解析失败 → 图形锁报「解析不出颜色」,
|
|
151
|
+
// 看着像色值坏了,其实是解析器不会数括号。
|
|
152
|
+
function splitTop(s) {
|
|
153
|
+
const out = [];
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let cur = "";
|
|
156
|
+
for (const ch of s) {
|
|
157
|
+
if (ch === "(") depth++;
|
|
158
|
+
else if (ch === ")") depth--;
|
|
159
|
+
if (ch === "," && depth === 0) { out.push(cur); cur = ""; continue; }
|
|
160
|
+
cur += ch;
|
|
161
|
+
}
|
|
162
|
+
out.push(cur);
|
|
163
|
+
return out.map((x) => x.trim());
|
|
164
|
+
}
|
|
142
165
|
function colorOf(raw, theme) {
|
|
143
166
|
const v = resolveValue(raw, theme);
|
|
144
167
|
if (v === null) return null;
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
const
|
|
168
|
+
const head = /^color-mix\(\s*in\s+srgb\s*,/i.exec(v);
|
|
169
|
+
if (head && v.endsWith(")")) {
|
|
170
|
+
const args = splitTop(v.slice(head[0].length, -1));
|
|
171
|
+
if (args.length !== 2) return null;
|
|
172
|
+
const pct = /^(.*?)\s+([\d.]+)%$/.exec(args[0]);
|
|
173
|
+
if (!pct) return null;
|
|
174
|
+
const base = colorOf(pct[1], theme);
|
|
148
175
|
if (!base) return null;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const b = colorOf(m[3], theme);
|
|
155
|
-
if (!a || !b) return null;
|
|
156
|
-
const p = parseFloat(m[2]) / 100;
|
|
157
|
-
return [...a.slice(0, 3).map((x, i) => x * p + b[i] * (1 - p)), a[3] * p + b[3] * (1 - p)];
|
|
176
|
+
const p = parseFloat(pct[2]) / 100;
|
|
177
|
+
if (/^transparent$/i.test(args[1])) return [...base.slice(0, 3), base[3] * p];
|
|
178
|
+
const other = colorOf(args[1], theme);
|
|
179
|
+
if (!other) return null;
|
|
180
|
+
return [...base.slice(0, 3).map((x, i) => x * p + other[i] * (1 - p)), base[3] * p + other[3] * (1 - p)];
|
|
158
181
|
}
|
|
159
182
|
return parseColor(v);
|
|
160
183
|
}
|
|
@@ -308,6 +331,95 @@ test("图形色(热度/打卡/连线)在浅深两主题下都达到 3:1", ()
|
|
|
308
331
|
assert.deepEqual(problems, [], "图形色对比度不足:\n" + problems.join("\n"));
|
|
309
332
|
});
|
|
310
333
|
|
|
334
|
+
// 色槽的归属是全局的:--mneme-chart-* 只服侍 donut(弧 + 图例点),--mneme-heat-* 只服侍
|
|
335
|
+
// 热力格,--mneme-sink-* 只服侍沉淀图那两根柱。三种回退都在这条锁上:
|
|
336
|
+
// ① 沉淀图借 donut 的 chart-3 / chart-2——同一页里一根琥珀柱在 donut 是「实体抽取」、
|
|
337
|
+
// 在沉淀图是「新增」,用户会把两个无关的量读成同一类;
|
|
338
|
+
// ② 上一版把第 6 个色槽内联成了三份(令牌 + 弧 + 图例点),改一处漏两处,死令牌还
|
|
339
|
+
// 没人发现——所以要求每一格都真的由令牌供给,不许内联字面量;
|
|
340
|
+
// ③ 令牌之间互相继承(`--mneme-sink-added:var(--mneme-chart-3)`)——看着解耦了,实际
|
|
341
|
+
// 一动 donut 的色盘就跟着改,语义层里出现别的图的 var() 一律拦。
|
|
342
|
+
// 它不锁色值:调色是设计决定,该靠截图给人看。与上面 GRAPHICS 的分工是「值 vs 名字」——
|
|
343
|
+
// 那条按值算 3:1、不看名字,所以换语义名也放宽不了对比度,新令牌照样被它咬住。
|
|
344
|
+
test("色槽各有其主:只被自己的图引用,且每格都由令牌供给", () => {
|
|
345
|
+
const FAMILIES = [
|
|
346
|
+
{ name: "chart", re: /var\(\s*--mneme-chart-/, owns: /^\.mneme-(seg|dot)--[1-6]$/ },
|
|
347
|
+
{ name: "heat", re: /var\(\s*--mneme-heat-/, owns: /^\.mneme-heatcell\.l[1-4]$/ },
|
|
348
|
+
{ name: "sink", re: /var\(\s*--mneme-sink-/, owns: /^\.mneme-sinkbar--(added|merged)$/ }
|
|
349
|
+
];
|
|
350
|
+
const problems = [];
|
|
351
|
+
// 语义层自己的声明也要查:`--mneme-sink-added:var(--mneme-chart-3)` 这种「继承式解耦」
|
|
352
|
+
// 看着分了家,实际上 donut 一动色盘它就跟着变。注意语义层那条规则是字符串拼出来的,
|
|
353
|
+
// **不在 rules 里**(rules 只收能整条匹配单个字面量的规则),所以必须单独扫 layer.body
|
|
354
|
+
// ——第一版写成了 `if (r.sel === layer.sel)`,那条分支永远进不去,等于没锁。
|
|
355
|
+
for (const f of FAMILIES) {
|
|
356
|
+
if (f.re.test(layer.body)) problems.push(`语义层里出现 ${f.name} 色槽的 var() 引用:色值要写成独立一份,不能继承别的图`);
|
|
357
|
+
}
|
|
358
|
+
for (const r of rules) {
|
|
359
|
+
for (const f of FAMILIES) {
|
|
360
|
+
if (f.re.test(r.body) && !f.owns.test(r.sel)) problems.push(`${r.sel} 借用了 ${f.name} 的色槽`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const bySel = new Map(rules.map((r) => [r.sel, r]));
|
|
364
|
+
for (let i = 1; i <= 6; i++) {
|
|
365
|
+
for (const kind of ["seg", "dot"]) {
|
|
366
|
+
const sel = `.mneme-${kind}--${i}`;
|
|
367
|
+
const r = bySel.get(sel);
|
|
368
|
+
if (!r) { problems.push(`${sel} 规则没了`); continue; }
|
|
369
|
+
if (!r.body.includes(`var(--mneme-chart-${i})`)) problems.push(`${sel} 没按 --mneme-chart-${i} 取色(内联颜色会变成第二份源)`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
for (const [sel, tok] of [
|
|
373
|
+
...Array.from({ length: 4 }, (_, i) => [`.mneme-heatcell.l${i + 1}`, null]),
|
|
374
|
+
[".mneme-sinkbar--added", "--mneme-sink-added"],
|
|
375
|
+
[".mneme-sinkbar--merged", "--mneme-sink-merged"]
|
|
376
|
+
]) {
|
|
377
|
+
const r = bySel.get(sel);
|
|
378
|
+
if (!r) { problems.push(`${sel} 规则没了`); continue; }
|
|
379
|
+
if (tok ? !r.body.includes(`var(${tok})`) : !/var\(\s*--mneme-heat-/.test(r.body)) {
|
|
380
|
+
problems.push(`${sel} 的填充色不是从语义层的令牌取的`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
assert.deepEqual(problems, [], "色槽归属或取色来源不对:\n" + problems.join("\n"));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// 图表调色盘(2026-11,代号 E):五个色相 + 一套共同配方。上面那条明说了「不锁色值」——
|
|
387
|
+
// 色号该靠截图给人看。这条锁的是**配方**,因为配方一散就会退回旧口径:
|
|
388
|
+
// ① 五个槽各自挑色器与配比:chart-2 与 chart-5 又都变绿(深色下 H 只差 4)、红槽彩度
|
|
389
|
+
// 翻到 0.172(别人的两倍)、amber-600 再压黑读成土褐;
|
|
390
|
+
// ② 少掉「先跟中性灰混」那一步:彩度与明度带一起散,深浅两套色阶就没法自动成立;
|
|
391
|
+
// ③ 拿 --dsw-alias-state-* 当色源:那是宿主状态的语义别名,宿主调一次状态色就顺手
|
|
392
|
+
// 换掉了整张图——图表色盘是设计决定,不该被别人的开关牵走。
|
|
393
|
+
// 五个色相必须互不相同(这就是「两块绿」那条的直接回归锁),灰槽留给 chart-6。
|
|
394
|
+
test("图表五色:五个不同色相、同一套两步混色配方", () => {
|
|
395
|
+
const problems = [];
|
|
396
|
+
const hues = new Set();
|
|
397
|
+
for (let i = 1; i <= 5; i++) {
|
|
398
|
+
const m = new RegExp("--mneme-chart-" + i + ":([^;]+);").exec(layer.body);
|
|
399
|
+
if (!m) { problems.push("--mneme-chart-" + i + " 没了"); continue; }
|
|
400
|
+
const v = m[1].trim();
|
|
401
|
+
if (/--dsw-alias-state-/.test(v)) problems.push("--mneme-chart-" + i + " 用了宿主状态别名当色源:" + v);
|
|
402
|
+
const shape = /^color-mix\(in srgb,color-mix\(in srgb,var\((--dsw-static-[a-z]+-\d{3}),#[0-9a-f]{6}\) \d+%,var\(--dsw-static-neutral-500,#7f8287\)\) \d+%,var\(--dsw-alias-label-primary\)\)$/.exec(v);
|
|
403
|
+
if (!shape) { problems.push("--mneme-chart-" + i + " 不是「静态色 → 中性灰 → label」两步式:" + v); continue; }
|
|
404
|
+
hues.add(shape[1]);
|
|
405
|
+
}
|
|
406
|
+
if (hues.size !== 5) problems.push("五个槽只用了 " + hues.size + " 个色相(" + [...hues].join(" / ") + "):相邻两块又会分不出");
|
|
407
|
+
const g = /--mneme-chart-6:([^;]+);/.exec(layer.body);
|
|
408
|
+
if (!g) problems.push("--mneme-chart-6 没了");
|
|
409
|
+
else if (!/^color-mix\(in srgb,var\(--dsw-alias-label-primary\) \d+%,transparent\)$/.test(g[1].trim())) {
|
|
410
|
+
problems.push("--mneme-chart-6 是「其他」类,该保持半透明灰:" + g[1].trim());
|
|
411
|
+
}
|
|
412
|
+
// 沉淀图两根柱跟着同一套配方走(同页一半柔和一半艳最难解释),但归属仍是自己的
|
|
413
|
+
for (const tok of ["--mneme-sink-added", "--mneme-sink-merged"]) {
|
|
414
|
+
const m = new RegExp(tok + ":([^;]+);").exec(layer.body);
|
|
415
|
+
if (!m) { problems.push(tok + " 没了"); continue; }
|
|
416
|
+
if (!/color-mix\(in srgb,color-mix\(in srgb,var\(--dsw-static-/.test(m[1])) {
|
|
417
|
+
problems.push(tok + " 没跟着图表的配方走(单层混色会和 donut 并排时突兀)");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
assert.deepEqual(problems, [], "图表调色盘的配方不统一:\n" + problems.join("\n"));
|
|
421
|
+
});
|
|
422
|
+
|
|
311
423
|
// 虚线关系边曾经在 .mneme-gedge-dashed 上再叠一层 opacity:.7,把已经只有 50% 的
|
|
312
424
|
// 描边压到浅色 2.28(低于图形 3:1)。上面那条图形锁只读单条规则自己的 stroke,
|
|
313
425
|
// 看不到「同一条边同时命中两条规则、其中一条在降透明度」这种叠加,所以单独锁一层:
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createStore } from "../src/store.js";
|
|
6
|
+
import { createService } from "../src/service.js";
|
|
7
|
+
import { createDreamScheduler } from "../src/dream.js";
|
|
8
|
+
|
|
9
|
+
// 回归(issue #634):阈值基线只在成功轮前进(dream_runs 落库 + 调度器闭包),而
|
|
10
|
+
// 归档/删除/合并把活跃量压下去——活跃量一旦跌破基线,overBase 恒为假,从此再
|
|
11
|
+
// 也不会巩固,且全程静默(报告实测停摆 13 天 / 22 小时零轮:末轮基线 1,475 条,
|
|
12
|
+
// 归档后活跃 798 条,门槛 ≥1,485 永不满足)。修法是判定口夹回:活跃量跌破基线的
|
|
13
|
+
// 轴取 min,阈值从新的低点重新起算,归掉的量不必重新长回来。本文件锁四个面:
|
|
14
|
+
// 1. 活着的调度器亲历收缩:归档压破基线后,重新增长阈值量仍能触发(停摆反例);
|
|
15
|
+
// 2. 重启后旧峰值种子在第一次判定被夹回——不落库也能自愈(收缩不触发
|
|
16
|
+
// notifyWrite,调度器本来就无从在归档当下得知,判定口是唯一可靠的收敛处);
|
|
17
|
+
// 3. 夹回事件走 auditBaselineClamp 记账:每次收缩周期至多一次、不收缩不记;
|
|
18
|
+
// 4. 两轴独立夹回:只缩字符不缩条数时,条数轴的基线不动。
|
|
19
|
+
|
|
20
|
+
/** 与阈值判定同口径的活跃库规模(测试侧独立实现,避免与被测代码共用一份过滤条件)。 */
|
|
21
|
+
function activeSize(store) {
|
|
22
|
+
const rows = store.all().filter((m) => !m.archived && m.type !== "summary" && m.type !== "document");
|
|
23
|
+
return {
|
|
24
|
+
count: rows.length,
|
|
25
|
+
chars: rows.reduce((sum, m) => sum + m.title.length + m.content.length, 0)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function seedMemories(service, n, tag = "a") {
|
|
30
|
+
// 标题必须全局唯一(saveWithDedupe 的去重键是 type+title+scope,同名会合并成
|
|
31
|
+
// 同一条),「累计到阈值」的断言才会真涨。
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
service.saveWithDedupe({ type: "history", title: `${tag}-${i}`, content: "x".repeat(300), importance: 3 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 把「上次成功轮的库规模」落进 dream_runs——调度器重启后读到的就是这份种子。 */
|
|
38
|
+
function persistBaseline(store) {
|
|
39
|
+
const size = activeSize(store);
|
|
40
|
+
store.saveDreamRun({
|
|
41
|
+
status: "ok", snapshot_hash: "h", input_count: 1, receipt: "r",
|
|
42
|
+
created_at: new Date(Date.now() - 10).toISOString(), run_type: "auto",
|
|
43
|
+
store_count: size.count, store_chars: size.chars
|
|
44
|
+
});
|
|
45
|
+
return size;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeOpts(store, overrides = {}) {
|
|
49
|
+
return {
|
|
50
|
+
onRun: async () => ({ ok: false }),
|
|
51
|
+
thresholdCount: 10, thresholdChars: 5000, delayMs: 0,
|
|
52
|
+
baselineSeed: store.lastDreamBaseline("auto"),
|
|
53
|
+
logger: { warn: () => {} },
|
|
54
|
+
...overrides
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 把活跃行里除 excludeTag 外的前 n 条归档(归档不触发 notifyWrite,这正是 #634 的一部分)。 */
|
|
59
|
+
function archiveRows(service, store, n, excludeTag = null) {
|
|
60
|
+
const rows = store.all().filter((m) => !m.archived && (excludeTag === null || !m.title.startsWith(excludeTag)));
|
|
61
|
+
for (const m of rows.slice(0, n)) service.setArchived(m.id, true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test("issue#634: a live scheduler survives an archive below the baseline (the freeze, unlocked)", async () => {
|
|
65
|
+
const store = createStore(":memory:");
|
|
66
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
67
|
+
seedMemories(service, 12);
|
|
68
|
+
const baseline = persistBaseline(store);
|
|
69
|
+
const dream = createDreamScheduler(makeOpts(store));
|
|
70
|
+
assert.equal(dream.maybeSchedule(service), false, "no growth since the last run → no trigger");
|
|
71
|
+
// 收缩发生在两次写入之间:归档 10 条,活跃 2 条 < 基线 12 条。
|
|
72
|
+
archiveRows(service, store, 10);
|
|
73
|
+
assert.ok(activeSize(store).count < baseline.count, "precondition: archive pulled active below the baseline");
|
|
74
|
+
// 收缩后的第一次写入把基线夹回当前水平(2+1=3),此后按新的低点起算。
|
|
75
|
+
seedMemories(service, 1, "g");
|
|
76
|
+
assert.equal(dream.maybeSchedule(service), false, "the first post-shrink write clamps instead of triggering");
|
|
77
|
+
seedMemories(service, 9, "p");
|
|
78
|
+
assert.equal(dream.maybeSchedule(service), false, "+9 from the clamped level is still below the threshold");
|
|
79
|
+
seedMemories(service, 1, "q"); // 夹回后的第 +10 条
|
|
80
|
+
assert.equal(dream.maybeSchedule(service), true, "+10 past the clamp fires — pre-fix this stayed silent forever");
|
|
81
|
+
await dream.dispose();
|
|
82
|
+
store.close();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("issue#634: a stale peak seed from dream_runs is clamped on the first judgement after restart", async () => {
|
|
86
|
+
const store = createStore(":memory:");
|
|
87
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
88
|
+
seedMemories(service, 12);
|
|
89
|
+
persistBaseline(store);
|
|
90
|
+
// 收缩发生在上一进程末轮之后、重启之前(归档不 notifyWrite,调度器无从得知):
|
|
91
|
+
// dream_runs 里的种子还是那个峰值。
|
|
92
|
+
archiveRows(service, store, 10);
|
|
93
|
+
const dream = createDreamScheduler(makeOpts(store));
|
|
94
|
+
// 重启后第一条写入:先把旧峰值种子夹回当前水平,再谈阈值。
|
|
95
|
+
seedMemories(service, 1, "r");
|
|
96
|
+
assert.equal(dream.maybeSchedule(service), false, "the restart write clamps the stale seed instead of bypassing");
|
|
97
|
+
seedMemories(service, 9, "s");
|
|
98
|
+
assert.equal(dream.maybeSchedule(service), false, "+9 from the clamped level is still below the threshold");
|
|
99
|
+
seedMemories(service, 1, "t");
|
|
100
|
+
assert.equal(dream.maybeSchedule(service), true, "the clamp makes the threshold reachable again without re-growing archived rows");
|
|
101
|
+
await dream.dispose();
|
|
102
|
+
store.close();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("issue#634: the clamp is audited exactly once per shrink episode, and never without one", async () => {
|
|
106
|
+
const store = createStore(":memory:");
|
|
107
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
108
|
+
const clamps = [];
|
|
109
|
+
seedMemories(service, 12);
|
|
110
|
+
const baseline = persistBaseline(store);
|
|
111
|
+
const dream = createDreamScheduler(makeOpts(store, {
|
|
112
|
+
auditBaselineClamp: ({ previous, count, chars }) => clamps.push({ previous, count, chars })
|
|
113
|
+
}));
|
|
114
|
+
// 不收缩的写入:正常增长(13 > 12),既不夹回也不记账。
|
|
115
|
+
seedMemories(service, 1, "n");
|
|
116
|
+
assert.equal(dream.maybeSchedule(service), false);
|
|
117
|
+
assert.equal(clamps.length, 0, "no clamp audit without a shrink");
|
|
118
|
+
// 归档压破基线(12→2),收缩后第一次判定夹回并记一次账。
|
|
119
|
+
archiveRows(service, store, 10, "n-");
|
|
120
|
+
seedMemories(service, 1, "p");
|
|
121
|
+
assert.equal(dream.maybeSchedule(service), false);
|
|
122
|
+
assert.equal(clamps.length, 1, "the shrink episode produces exactly one clamp audit");
|
|
123
|
+
assert.deepEqual(clamps[0].previous, baseline, "previous = the stale peak baseline");
|
|
124
|
+
assert.equal(clamps[0].count, activeSize(store).count, "count/chars = the clamped (current active) level");
|
|
125
|
+
assert.equal(clamps[0].chars, activeSize(store).chars, "chars axis is clamped to the same level");
|
|
126
|
+
// 同一收缩周期内活跃只增不减:不再重复记账(夹回后基线 == 活跃量)。
|
|
127
|
+
seedMemories(service, 5, "u");
|
|
128
|
+
assert.equal(dream.maybeSchedule(service), false, "still below the threshold from the clamped level");
|
|
129
|
+
assert.equal(clamps.length, 1, "no repeated clamp audits while active only grows");
|
|
130
|
+
await dream.dispose();
|
|
131
|
+
store.close();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("issue#634: the clamp is per-axis — a chars-only shrink leaves the count baseline alone", async () => {
|
|
135
|
+
const store = createStore(":memory:");
|
|
136
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
137
|
+
seedMemories(service, 12);
|
|
138
|
+
const baseline = persistBaseline(store);
|
|
139
|
+
const clamps = [];
|
|
140
|
+
const dream = createDreamScheduler(makeOpts(store, {
|
|
141
|
+
auditBaselineClamp: (event) => clamps.push(event)
|
|
142
|
+
}));
|
|
143
|
+
// 更新类写入(同 type+title+scope 命中去重键)把一条换成短内容:条数不动、字符缩。
|
|
144
|
+
// 去重命中默认 appendContent(追加),只有显式 _overwrite 才替换——不加的话
|
|
145
|
+
// 字符数反而变多,这条用例的前置就不成立了。
|
|
146
|
+
service.saveWithDedupe({ type: "history", title: "a-0", content: "短", _overwrite: true, importance: 3 });
|
|
147
|
+
assert.equal(activeSize(store).count, baseline.count, "precondition: the count axis did not move");
|
|
148
|
+
assert.ok(activeSize(store).chars < baseline.chars, "precondition: the chars axis shrank");
|
|
149
|
+
assert.equal(dream.maybeSchedule(service), false, "the threshold itself is not met yet");
|
|
150
|
+
assert.equal(clamps.length, 1, "a chars-only shrink still clamps");
|
|
151
|
+
assert.equal(clamps[0].previous.count, baseline.count, "the count baseline was intact before the clamp");
|
|
152
|
+
assert.equal(clamps[0].count, baseline.count, "the count baseline survives — only the shrunken axis moves");
|
|
153
|
+
assert.ok(clamps[0].chars < clamps[0].previous.chars, "the chars baseline is clamped down");
|
|
154
|
+
await dream.dispose();
|
|
155
|
+
store.close();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- CodeRabbit 在 #291 上的同类要求(源码级锁):index.js 的接线是单行注入,仓库
|
|
159
|
+
// 没有插件级挂载 harness(apply 需要完整宿主 ctx),删掉该注入行不会让任何单测变
|
|
160
|
+
// 红——所以用正则锁注入本身。import 用 URL 相对本文件解析,不依赖测试进程 CWD。
|
|
161
|
+
const indexSrc = readFileSync(fileURLToPath(new URL("../src/index.js", import.meta.url)), "utf8");
|
|
162
|
+
|
|
163
|
+
test("issue#634: index.js wires the baseline-clamp audit into llm_audit_logs", () => {
|
|
164
|
+
assert.match(
|
|
165
|
+
indexSrc,
|
|
166
|
+
/auditBaselineClamp:/,
|
|
167
|
+
"the clamp callback must be wired at assembly — otherwise clamp events stay unobservable (the #634 silence)"
|
|
168
|
+
);
|
|
169
|
+
assert.match(
|
|
170
|
+
indexSrc,
|
|
171
|
+
/error_message:\s*"baseline-clamp"/,
|
|
172
|
+
"the audit row must carry the baseline-clamp marker so it can be told apart from peak-hours skips"
|
|
173
|
+
);
|
|
174
|
+
});
|