diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index af7ddae3..55046827 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- **状态页「8 周沉淀」图看得懂了:图例补颜色色标,柱高的共同基准也写明白了(用户反馈)**。图上每周两根柱(琥珀=进来的、绿=稳住的),图例却只有四段纯文字「新增 / 巩固 / 归档 / 巩固·新增」,读者只能猜哪根柱对应哪个数字;「柱高按最近 8 周单周最高新增折算」这件事也一个字没提。现在图例每一项前面挂同色圆点(直接引用 `--mneme-sink-added` / `--mneme-sink-merged` 两个语义色,不另立颜色,避免与 donut 的组件色撞成两义),归档压根不给色块、只在数值后注明「未画入柱」——它本来就不进曲线、只报总量,给它一个色标(哪怕是空心点)就会长得像第三根柱的别名;图下补一行「柱高同基准:按最近 8 周单周最高新增折算」。`lib/client.js` 是独立手写的面板 bundle(`scripts/sync-lib.js` 只做 src→lib 拷贝、不碰它),所以改动只落在它自身与测试/CHANGELOG。回归锁加在 `test/client.test.js` 的沉淀图用例:两个色标必须引用语义色、归档项必须标明未画入柱。 + ## [0.15.0] - 2026-11-13 - **第三处「默认开」的开关翻过来了:`memoryQualityFilter.enabled` 的读取从 `=== true` 改成 `!== false`,裸配置调用方不再静默拿到「关」(#692 尾巴)**。上一批(同一条 issue 的另两处 `dreamMergeGuard` / `resilientModelDownload`)把这一处刻意留下,理由是它挂在**自身带 `.default({})` 的对象**里、经 Config 的调用方一直是开的;现在按拍板把它翻过来,契约变成一句话:**默认开,只有显式 `false` 才关**。翻的过程中浮出第二层问题,比读法本身更隐蔽——评分块的阈值读取是 `qf.minContentLength ?? 10`,**`qf` 自身缺席时(裸配置不传 `memoryQualityFilter`)这行直接抛 TypeError**,被 `saveWithDedupe` 的 best-effort catch 吞掉,于是「没归档、也没分数」,看起来就像默认关:只改读法、不改这里,翻完等于什么都没发生(第一版实测 3499 条只红 1 条,且那 1 条是 `src/`↔`lib/` 发布漂移)。所以 `minContentLength` / `archiveThreshold` / `exemptImportance` 三处读取全部走可选链,回落到 10 / 30 / 4。**修好之后才是真实代价**:全量 430 条红,全部来自 fixture 用两三字短正文(「入门」「喜欢猫」),靠过滤器默认关才留在库里被检索到——那些测试考的从来不是过滤规则。落法不是逐条改正文,而是把前提**写一次**:新增 `test/helpers/test-config.js` 的 `testConfig(extra)`,返回 `{ memoryQualityFilter: { enabled: false }, ...extra }`;118 个测试文件的 295 处 `config: {}` / `config: {…}` 包成 `testConfig({…})`,另有 15 个自己拼 `config` 变量的 setup 在 `createService` 处包 `testConfig(config)`(调用方传的对象仍优先覆盖,显式 `enabled: true` 照旧生效)。**需要过滤行为的测试自己显式传 `{ memoryQualityFilter: { enabled: true } }`**——`test/quality-filter.test.js` 的 setup 就是这样,它的文件头注释也从「raw configs 保持旧行为」改成「默认开、只有显式 false 才关」。**行为变化**:绕过 `Config` 的程序化 `createService` 调用方(第三方、直连单元的装配)现在默认开着质量过滤,短写入会被归档;要旧行为须显式 `memoryQualityFilter: { enabled: false }`。`docs/CONFIGURATION.md` 与 README 的两处表格本来就写「默认 `true`」,这次是代码终于追上文档,不再改文档。**daemon 那一格是评审照出来的**:`src/serve.js` 自己装配的最小 cfg 原本一个 `memoryQualityFilter` 键都不带 —— 旧契约下缺席等于关,翻成默认开之后缺席就等于开,而 daemon 只从持久化 flags 展开过 `distillRecords.enabled` 一组、结构上看不到宿主的 bundle config。后果不是「多归档几条」,而是**面板上的开关对 daemon 无效**:用户明明关掉了质量过滤,独立服务这边照样把短写入归档(实测 `content:"中文"` / `importance:3` → `archived:true, score:20, tags:["short_content","low_quality"]`)。修法与蒸馏那一格同款:把 kv 里平铺的 `memoryQualityFilter.enabled` 展开进 cfg;**没设过就不写**,默认值只留在 `src/config.js` 的 schema 里,本处不自造第二份默认口径。`docs/DAEMON.md` 的「第一期不吃宿主配置」补上这两个例外,以及那条残留不对称(只靠 bundle 配置打开、从没在面板点过的用户,宿主认、daemon 不认)。**测试** 3,499 → **3,503**(+4):裸配置下短正文被归档且 `quality_score` 确实落库(这条同时是「阈值读取不许抛」的回归锁——抛了就没有分数)、显式 `enabled: false` 时不评分不归档不打标(分数是 NULL 不是 0)、面板关掉后 daemon 不评分也不归档、从没设过时 daemon 按默认开处理。变异自证:读法回退成 `=== true` → 1 红;`minContentLength` 去掉可选链 → 1 红;撤掉 `serve.js` 的 flags 展开 → daemon 那条 1 红;还原 → 全绿。 diff --git a/dsh-mneme/lib/client.js b/dsh-mneme/lib/client.js index 9e94da75..d29f7510 100644 --- a/dsh-mneme/lib/client.js +++ b/dsh-mneme/lib/client.js @@ -1489,6 +1489,8 @@ window.__ModuleLoader__.load({ "memory.status.token.sinkArchived": "归档", "memory.status.token.sinkGap": "巩固/新增", "memory.status.token.sinkEmpty": "最近还没有记录", + "memory.status.token.sinkAxis": "柱高同基准:按最近 8 周单周最高新增折算", + "memory.status.token.sinkArchivedOff": "未画入柱", "memory.status.token.sinkFoot": "巩固按去重记录数计,同一条被多轮合并只算一次", "memory.status.token.last7": "近 7 天", "memory.status.token.meterOnly": "(只计量)", @@ -2631,6 +2633,8 @@ window.__ModuleLoader__.load({ "memory.status.token.sinkArchived": "archived", "memory.status.token.sinkGap": "consolidated/added", "memory.status.token.sinkEmpty": "nothing recorded yet", + "memory.status.token.sinkAxis": "same scale for both bars: height is relative to the busiest week by added", + "memory.status.token.sinkArchivedOff": "not plotted", "memory.status.token.sinkFoot": "consolidated counts distinct records; re-merging the same record counts once", "memory.status.token.last7": "last 7 days", "memory.status.token.meterOnly": "(metered only)", @@ -3437,6 +3441,15 @@ window.__ModuleLoader__.load({ ".mneme-sinklabels span{flex:1;min-width:0;text-align:center;white-space:nowrap}", ".mneme-sinklegend{display:flex;flex-wrap:wrap;align-items:center;gap:var(--mneme-space-4) var(--mneme-space-10);margin-top:var(--mneme-space-8);font-size:10.5px;color:var(--mneme-fg-dim)}", ".mneme-sinklegend b{font-weight:600;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}", + // 图例色标:两根柱的颜色必须有一份能对照的说明,否则读者只能靠猜哪根是 + // 「进来的」哪根是「稳住的」(#用户反馈:没图标标注、看不太懂)。尺寸走刻度 + // (状态页不许新增裸 px),归档不画进曲线,所以干脆不给色块、只注明未画入—— + // 给它一个空心点反而像第三根柱的别名。 + ".mneme-sinklegend>span{display:inline-flex;align-items:center;gap:var(--mneme-space-4)}", + ".mneme-sinkdot{flex:none;width:var(--mneme-space-8);height:var(--mneme-space-8);border-radius:50%}", + ".mneme-sinkdot--added{background:var(--mneme-sink-added)}", + ".mneme-sinkdot--merged{background:var(--mneme-sink-merged)}", + ".mneme-sinkoff{font-style:normal;opacity:.75}", // 原 9 卡降级折叠区:默认收起,展开才挂载子卡(见 TokenFlowSection 注释)。 ".mneme-legacy{max-width:1000px;margin:0 auto var(--mneme-space-12);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--mneme-radius-12);padding:var(--mneme-space-12) var(--mneme-space-16)}", ".mneme-legacy summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}", @@ -7602,13 +7615,20 @@ window.__ModuleLoader__.load({ // 图例只在有数据时给:加载中把三个 0 画出来等于谎报「最近没东西进库」。 sinkDays && !sinkError ? h("div", { className: "mneme-sinklegend" }, - h("span", null, `${t("memory.status.token.sinkAdded")} `, h("b", null, sinkAddedTotal.toLocaleString())), - h("span", null, `${t("memory.status.token.sinkMerged")} `, h("b", null, sinkMergedTotal.toLocaleString())), - h("span", null, `${t("memory.status.token.sinkArchived")} `, h("b", null, sinkArchivedTotal.toLocaleString())), + h("span", null, + h("i", { className: "mneme-sinkdot mneme-sinkdot--added", "aria-hidden": "true" }), + t("memory.status.token.sinkAdded"), h("b", null, sinkAddedTotal.toLocaleString())), + h("span", null, + h("i", { className: "mneme-sinkdot mneme-sinkdot--merged", "aria-hidden": "true" }), + t("memory.status.token.sinkMerged"), h("b", null, sinkMergedTotal.toLocaleString())), + h("span", null, + t("memory.status.token.sinkArchived"), h("b", null, sinkArchivedTotal.toLocaleString()), + h("em", { className: "mneme-sinkoff" }, t("memory.status.token.sinkArchivedOff"))), h("span", null, `${t("memory.status.token.sinkGap")} `, h("b", null, sinkGap)) ) : null, - h("div", { className: "mneme-statuscap", style: { marginTop: "6px", fontSize: "11px" } }, t("memory.status.token.sinkFoot")) + h("div", { className: "mneme-statuscap", style: { marginTop: "6px", fontSize: "11px" } }, t("memory.status.token.sinkAxis")), + h("div", { className: "mneme-statuscap", style: { marginTop: "2px", fontSize: "11px" } }, t("memory.status.token.sinkFoot")) ) ) ) diff --git a/dsh-mneme/test/client.test.js b/dsh-mneme/test/client.test.js index 664295c1..2c66fd48 100644 --- a/dsh-mneme/test/client.test.js +++ b/dsh-mneme/test/client.test.js @@ -1324,6 +1324,17 @@ test("consolidation chart: own 56-day window, null-vs-empty split, and a narrow- // 两根柱的语义色必须走语义层,不能直接拿宿主 label token 当填充色(#178)。 assert.ok(clientSource.includes(".mneme-sinkbar--added{background:var(--mneme-sink-added)}")); assert.ok(clientSource.includes(".mneme-sinkbar--merged{background:var(--mneme-sink-merged)}")); + // ⑤ 图例必须带色标:图上两根柱是两种颜色,图例却只有四段文字,读者只能猜 + // 哪根是「进来的」哪根是「稳住的」。归档不画进柱,也要标明这点。 + assert.ok( + clientSource.includes(".mneme-sinkdot--added{background:var(--mneme-sink-added)}") && + clientSource.includes(".mneme-sinkdot--merged{background:var(--mneme-sink-merged)}"), + "the legend needs colour swatches matching the two bar colours" + ); + assert.ok( + /sinkArchivedOff/.test(clientSource) && !/mneme-sinkdot--archived/.test(clientSource), + "the archived entry must say it is not plotted and must not get a swatch of its own (a hollow dot reads as a third bar)" + ); }); // 巩固/睡眠模型路由 UI:下拉数据来自宿主侧已注册适配器(/llm-providers, diff --git a/dsh-mneme/test/contrast.test.js b/dsh-mneme/test/contrast.test.js index 8b9e11d7..bda5fea4 100644 --- a/dsh-mneme/test/contrast.test.js +++ b/dsh-mneme/test/contrast.test.js @@ -345,7 +345,7 @@ test("色槽各有其主:只被自己的图引用,且每格都由令牌供 const FAMILIES = [ { name: "chart", re: /var\(\s*--mneme-chart-/, owns: /^\.mneme-(seg|dot)--[1-6]$/ }, { name: "heat", re: /var\(\s*--mneme-heat-/, owns: /^\.mneme-heatcell\.l[1-4]$/ }, - { name: "sink", re: /var\(\s*--mneme-sink-/, owns: /^\.mneme-sinkbar--(added|merged)$/ } + { name: "sink", re: /var\(\s*--mneme-sink-/, owns: /^\.mneme-sink(bar|dot)--(added|merged)$/ } ]; const problems = []; // 语义层自己的声明也要查:`--mneme-sink-added:var(--mneme-chart-3)` 这种「继承式解耦」 @@ -372,7 +372,11 @@ test("色槽各有其主:只被自己的图引用,且每格都由令牌供 for (const [sel, tok] of [ ...Array.from({ length: 4 }, (_, i) => [`.mneme-heatcell.l${i + 1}`, null]), [".mneme-sinkbar--added", "--mneme-sink-added"], - [".mneme-sinkbar--merged", "--mneme-sink-merged"] + [".mneme-sinkbar--merged", "--mneme-sink-merged"], + // 图例色标与柱子必须同源:柱子换了语义色而点没跟上,图例就会指着错的颜色 + // 解释图——比没有图例更糟。 + [".mneme-sinkdot--added", "--mneme-sink-added"], + [".mneme-sinkdot--merged", "--mneme-sink-merged"] ]) { const r = bySel.get(sel); if (!r) { problems.push(`${sel} 规则没了`); continue; }