diff --git a/README.md b/README.md index f9624880..5f1c9d85 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ license CI node - tests + tests coverage Awesome

@@ -207,7 +207,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790 ```bash cd dsh-mneme && npm install -npm test # 1609 个测试 +npm test # 1613 个测试 npm run stress # 三轴线压测 npm run sync # src → lib 同步 ``` @@ -414,7 +414,7 @@ Community projects that wire mneme into more platforms — not part of this repo ```bash cd dsh-mneme && npm install -npm test # 1609 tests +npm test # 1613 tests npm run stress # three-axis stress test npm run sync # src → lib sync ``` diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index 5edf195a..c32a783b 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) -[![tests](https://img.shields.io/badge/tests-1609%20passed-success)](https://github.com/slow-stack/mneme) +[![tests](https://img.shields.io/badge/tests-1613%20passed-success)](https://github.com/slow-stack/mneme) [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions) [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org) [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme) @@ -613,7 +613,7 @@ src/ ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列) └── index.js # 插件接线 lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖) -test/ # 1609 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) +test/ # 1613 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单 ``` @@ -622,7 +622,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压 ```bash cd dsh-mneme npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试) -npm test # 运行 1609 个测试 +npm test # 运行 1613 个测试 npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM) npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行) ``` diff --git a/dsh-mneme/lib/service.js b/dsh-mneme/lib/service.js index c2ba694c..17526a95 100644 --- a/dsh-mneme/lib/service.js +++ b/dsh-mneme/lib/service.js @@ -13,7 +13,15 @@ import { createBM25Index } from "./search/bm25.js"; import { adaptiveThreshold } from "./search/adaptive.js"; import { anchorSeeds, normalizePath, cascadeDepths, HOP_QUOTA } from "./graph/anchoring.js"; -const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint"]); +// Issue #369:蒸馏白名单(src/summarize.js 的 VALID)产得出 history,注入白名单 +// 原先不收——本机活跃库实测 378/1820(20.8%)因此永不可自动注入,而它们在同一库 +// 的 recall_runs `auto` 模式里被按需检索过 328 次:不是垃圾数据,只是进不了注入池。 +// 收编后候选池 1362 → 1713(+25.8%),按 top_k=5 粗算约多占 1 席;重要度闸门对 +// history 一视同仁(见 injectCandidates 的 importance >= threshold),低于闸门照旧出局。 +// pattern 一并收编:它只由手工写入(实测 40 条全 source=manual),却已被 dreamMergeGuard +// 列为长保留受保护类型(docs/CONFIGURATION.md)——巩固层当资产护住、注入面一次不给看, +// 是同一条口径的两头不一致。 +const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint", "history", "pattern"]); // document 摘要行(#230):仅 documentMemoryEnabled 开启时进注入候选。独立 // 集合而非常改共享 Set——INJECT_TYPES 本身是「类型可注入」的静态事实,flag @@ -1634,9 +1642,14 @@ export function createService({ store, mirror, config, onWrite, logger, document } catch { /* vector unavailable: fall through to the recall cache */ } } if (!semanticItems.length && lastSemanticRecall?.query === q && lastSemanticRecall.items?.length) { + // CodeRabbit on #404:这条回填分支此前独漏重要度门槛——向量路(上方)与 + // BM25 路(下方 :1655 注释「门控与向量路径一致(含 importance 阈值)」) + // 都带着它。#369 收编 history 后缺口会被放大:库里低于闸门的行(实测 + // history 378 条中有 27 条 i<=2)走一次检索进缓存就能绕开门槛占注入位。 for (const m of lastSemanticRecall.items) { if (m && !m.archived && injectTypes.has(m.type) && !m.forgotten && - (m.source !== "narrative" || allowNarrative) && codingGate(m)) semanticItems.push(m); + (m.source !== "narrative" || allowNarrative) && codingGate(m) && + (m.type === "summary" || m.type === "preference" || m.importance >= threshold)) semanticItems.push(m); } } // Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。 diff --git a/dsh-mneme/src/service.js b/dsh-mneme/src/service.js index c2ba694c..17526a95 100644 --- a/dsh-mneme/src/service.js +++ b/dsh-mneme/src/service.js @@ -13,7 +13,15 @@ import { createBM25Index } from "./search/bm25.js"; import { adaptiveThreshold } from "./search/adaptive.js"; import { anchorSeeds, normalizePath, cascadeDepths, HOP_QUOTA } from "./graph/anchoring.js"; -const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint"]); +// Issue #369:蒸馏白名单(src/summarize.js 的 VALID)产得出 history,注入白名单 +// 原先不收——本机活跃库实测 378/1820(20.8%)因此永不可自动注入,而它们在同一库 +// 的 recall_runs `auto` 模式里被按需检索过 328 次:不是垃圾数据,只是进不了注入池。 +// 收编后候选池 1362 → 1713(+25.8%),按 top_k=5 粗算约多占 1 席;重要度闸门对 +// history 一视同仁(见 injectCandidates 的 importance >= threshold),低于闸门照旧出局。 +// pattern 一并收编:它只由手工写入(实测 40 条全 source=manual),却已被 dreamMergeGuard +// 列为长保留受保护类型(docs/CONFIGURATION.md)——巩固层当资产护住、注入面一次不给看, +// 是同一条口径的两头不一致。 +const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint", "history", "pattern"]); // document 摘要行(#230):仅 documentMemoryEnabled 开启时进注入候选。独立 // 集合而非常改共享 Set——INJECT_TYPES 本身是「类型可注入」的静态事实,flag @@ -1634,9 +1642,14 @@ export function createService({ store, mirror, config, onWrite, logger, document } catch { /* vector unavailable: fall through to the recall cache */ } } if (!semanticItems.length && lastSemanticRecall?.query === q && lastSemanticRecall.items?.length) { + // CodeRabbit on #404:这条回填分支此前独漏重要度门槛——向量路(上方)与 + // BM25 路(下方 :1655 注释「门控与向量路径一致(含 importance 阈值)」) + // 都带着它。#369 收编 history 后缺口会被放大:库里低于闸门的行(实测 + // history 378 条中有 27 条 i<=2)走一次检索进缓存就能绕开门槛占注入位。 for (const m of lastSemanticRecall.items) { if (m && !m.archived && injectTypes.has(m.type) && !m.forgotten && - (m.source !== "narrative" || allowNarrative) && codingGate(m)) semanticItems.push(m); + (m.source !== "narrative" || allowNarrative) && codingGate(m) && + (m.type === "summary" || m.type === "preference" || m.importance >= threshold)) semanticItems.push(m); } } // Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。 diff --git a/dsh-mneme/test/inject-recall.test.js b/dsh-mneme/test/inject-recall.test.js index 7d3ca45d..cb36d319 100644 --- a/dsh-mneme/test/inject-recall.test.js +++ b/dsh-mneme/test/inject-recall.test.js @@ -103,3 +103,42 @@ test("slotFillRate = 注入条数 / (轮数 × 槽位数)", () => { assert.equal(stats.injection.slotFillRate, 3 / 4); store.close(); }); + +// #369:history 曾被注入白名单挡在门外(蒸馏产得出、注入收不进)。这两条断言分 +// 别锁「收编生效」与「重要度闸门对 history 不豁免」——后者一旦失效,短流水账会 +// 直接挤掉决策类条目,那才是这个改动真正的风险面。 +test("#369: history 进注入候选池", () => { + const { service } = setup(); + service.saveWithDedupe({ type: "history", title: "切到暗色主题", content: "用户本次会话把界面切成暗色主题", importance: 4 }); + const titles = service.injectCandidates({ maxItems: 5 }).map((m) => m.title); + assert.ok(titles.includes("切到暗色主题"), "importance>=3 的 history 应进注入候选"); +}); + +test("#369: 低于重要度闸门的 history 不得例外放行", () => { + const { service } = setup(); + service.saveWithDedupe({ type: "history", title: "打开过一次终端", content: "用户中途打开过一次终端", importance: 2 }); + const titles = service.injectCandidates({ maxItems: 5 }).map((m) => m.title); + assert.ok(!titles.includes("打开过一次终端"), "history 不是 summary/preference,闸门照样生效"); +}); + +// pattern 只由手工写入,却已被 dreamMergeGuard 列为长保留受保护类型——巩固层当资产 +// 护住、注入面一次不给看是同一条口径的两头不一致。这条锁它进池。 +test("#369: 手写的 pattern 进注入候选池", () => { + const { service } = setup(); + service.saveWithDedupe({ type: "pattern", title: "重复字符串先抽共用模块", content: "多处重复的截断逻辑先收拢成共用模块,再改调用方", importance: 5 }); + const titles = service.injectCandidates({ maxItems: 5 }).map((m) => m.title); + assert.ok(titles.includes("重复字符串先抽共用模块"), "importance>=3 的 pattern 应进注入候选"); +}); + +// CodeRabbit on PR #404:语义缓存回填分支曾独漏重要度门槛(向量路与 BM25 路都带)。 +// 一正一反:反向锁「低于闸门的行走缓存也不许进池」,正向锁「达标的照常进池」, +// 免得断言退化成常量真;缓存没被灌进来时正向那条会先红,用例不会空压。 +test("#369: 语义缓存回填同样受重要度闸门约束", async () => { + const { service } = setup(); + const low = service.saveWithDedupe({ type: "history", title: "终端开了一次", content: "用户中途打开过一次终端做检查", importance: 2 }); + const high = service.saveWithDedupe({ type: "history", title: "终端方案定案", content: "用户终端接入方案已定案并验证", importance: 4 }); + await service.searchMemories("终端", { mode: "keyword", useRerank: false, recordRecall: false }); + const ids = service.injectCandidates({ query: "终端", maxItems: 5 }).map((m) => m.id); + assert.ok(ids.includes(high.memory.id), "i4 的 history 应经缓存回填进池(正向对照)"); + assert.equal(ids.includes(low.memory.id), false, "低于闸门的行不得借语义缓存绕开门槛"); +}); diff --git a/dsh-mneme/test/service.test.js b/dsh-mneme/test/service.test.js index 440ffb3d..6809e177 100644 --- a/dsh-mneme/test/service.test.js +++ b/dsh-mneme/test/service.test.js @@ -53,7 +53,10 @@ test("injectCandidates returns preferences + high-importance items within limit" const types = candidates.map((c) => c.type); assert.ok(types.includes("preference"), "preferences included"); assert.ok(types.includes("project"), "high-importance project included"); - assert.ok(!types.includes("history"), "history excluded by default"); + // #369:history 曾被注入白名单挡在门外,现按「注入侧收编」进池(理由与实测数字 + // 见 src/service.js 的 INJECT_TYPES 注释)。这里从「锁排除」改成「锁收编」—— + // 白名单日后若被收窄,这条会红,而不是静默把两成条目关回不可注入。 + assert.ok(types.includes("history"), "history included since #369"); assert.ok(candidates.length <= 5, "respects maxItems"); }); @@ -220,7 +223,9 @@ test("Bug4: injectCandidates with an empty query keeps the legacy rule-based sel assert.deepEqual(noQuery.map((c) => c.id), legacy.map((c) => c.id), "empty query behaves identically to the old logic"); assert.deepEqual(blankQuery.map((c) => c.id), legacy.map((c) => c.id), "whitespace-only query degrades too"); assert.equal(legacy[0].type, "summary", "summary still leads"); - assert.ok(!legacy.some((c) => c.type === "history"), "history still excluded"); + // #369 收编后 history 可进池;本用例的重心是「空查询与旧规则路逐字节同序」, + // 这条只确认收编没有撼动优先级分层——summary 仍打头。 + assert.ok(legacy.some((c) => c.type === "history"), "history included since #369"); }); test("Bug4: injectCandidates ignores the query when hybridInject is disabled", () => {