dsh-engramory-recall 0.12.0 → 0.12.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,38 @@
3
3
  本项目的所有重要变更都记在这里。格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
4
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
5
5
 
6
+ ## [0.12.2] — 2026-10-15
7
+
8
+ ### 新增
9
+
10
+ - **embeddings 接缝的实测尺子**(`npm run bench:embed`):`--fake` 起本机假服务把管线与代价量清楚
11
+ (池子实际大小、每次检索耗时、HTTP 调用数、发出文本条数、缓存省下多少),`--real` 换成真端点量质量。
12
+ 真库那半边要显式 `--store`,因为**它会把笔记片段发给端点**。
13
+ - 提供方模块现在也接受**默认导出对象**(`export default { rerank }`)——第三方提供方最自然的写法,
14
+ 不必改成命名导出。命名导出照旧。
15
+
16
+ ### 说明
17
+
18
+ - 假向量(哈希向量 = 最坏提供方)把融合表的结论变成实测:真库探针 hit@1 从 72.2%(词面)掉到
19
+ 5.6%(`trust`)与 19.4%(`rrf`)—— `trust` 对垃圾提供方是灾难,**第一次接第三方接口建议先 `rrf`**。
20
+ - 缓存实测:26925 条候选只发出 1994 条(省 92.6%),因为同一篇笔记在不同查询里反复出现。
21
+ - 池子实际大小中位 31~39 条(不是 50):词面只回有分的文档,所以 `semanticPool` 是上限不是常态。
22
+
23
+ ## [0.12.1] — 2026-10-15
24
+
25
+ ### 新增
26
+
27
+ - **池宽天花板尺子**(`npm run bench:ceiling`):把「候选池放宽到 N 条时,任何提供方最多能拿到几分」
28
+ 单独量出来,含按题型拆分与「每查询要发给提供方多少东西」(条数 / 片段字符数 → token 估算,离线可算)。
29
+
30
+ ### 说明
31
+
32
+ - 量出一个反直觉的事实并写进 README:**公开数据集上池宽买不到分数** —— LongMemEval-S 每题的 haystack
33
+ 只有 30~50 个 session,池内召回 top-5 就已经 98.2%、top-20 99.6%;池宽的价值在「库比窗口大得多」时
34
+ (合成 704 条 70.3% → 97.3%)。所以默认 50 是给大库留的余量,不是公共基准上的分数。
35
+ - `bench/longmemeval.mjs` 的 session→笔记映射抽到 `bench/sessions.mjs`(与尺子共用一份,避免口径漂移),
36
+ 20 题输出逐字未变。
37
+
6
38
  ## [0.12.0] — 2026-10-15
7
39
 
8
40
  ### 新增
package/README.md CHANGED
@@ -670,6 +670,28 @@ RRF 只能在池内重排,所以 hit@4 一动都不动(67.5%),坏的提
670
670
  2. **`rrf` 是安全档**:提供方不靠谱时,`trust` 会一路砸到底(倒排 1.0%),`rrf` 至少保住词面的一部分(26.0%)。拿不准就用它。
671
671
  3. **「只投一票」两种融合同分**:稀释来自提供方**自己的低名次**,不是来自「它参与了融合」。所以问题不在「要不要融合」,而在「按谁的顺序」。
672
672
 
673
+ #### 池宽在公开数据集上量不出差别(`npm run bench:ceiling`)
674
+
675
+ LongMemEval-S 500 题,每题自带的 haystack 只有 30~50 个 session(数据 `sha256_12 = d6f21ea9d60a`):
676
+
677
+ | 候选池 | 5 | 20 | 50 | 100 |
678
+ |---|---|---|---|---|
679
+ | 池内召回 | 98.2% | 99.6% | 99.8% | 99.8% |
680
+
681
+ 按题型看,唯一在 top-5 有明显缺口的是偏好题(90.0%),它在 top-20 就补满。所以这份公开卷子上
682
+ **top-5 已经够了,池宽放宽买不到分数**——它的意义在库比窗口大得多的时候(合成 704 条 70.3% → 97.3%;
683
+ 真库 66 篇 / 200 chunk 从池 5 起就是 36/36)。一句话:**池宽是给大库留的余量,不是公共基准上的分数。**
684
+
685
+ 代价(离线估算,按池 100 算,即最坏情况):
686
+
687
+ | 项 | 中位 | p90 | 最大 |
688
+ |---|---|---|---|
689
+ | 候选条数 | 27 | 41 | 48 |
690
+ | 片段字符数 | 1916 | 2921 | 3425 |
691
+
692
+ 按英文 ≈4 字符/token 粗估:一条查询约 479 token(中位)、730 token(p90)。提供方按文本缓存,
693
+ 同一篇笔记反复出现只付一次;**真延迟要在有端点时量**(`npm run embed:check` 会报一次往返耗时)。
694
+
673
695
  #### 内置参考提供方:OpenAI 兼容的 embeddings(`providers/openai-embeddings.mjs`)
674
696
 
675
697
  填两个环境变量就能试;密钥只从环境变量或一个 0600 的小文件进,**不进仓库、不进日志**:
@@ -699,6 +721,27 @@ export ENGRAMORY_EMBED_MODEL=text-embedding-3-small
699
721
  **一句实话**:这条路径的代码、测试、失败降级都有,但**还没有在真 key 上量过召回**(要花token,得你先点头)。
700
722
  接缝本身的上限是量过的(上表 86.6%),提供方的质量只能真调一次才知道。
701
723
 
724
+ #### 真端点怎么量:`npm run bench:embed`
725
+
726
+ 一条命令,两种模式:`--fake`(默认)起一个**本机假 embeddings 服务**,只读「管线与代价」;
727
+ `--real` 用 `providers/openai-embeddings.mjs` 的配置读法找真端点,那时质量列才是真的。
728
+ 真库那半边要显式加 `--store ~/memory/engramory` —— 加了就意味着**你的笔记片段会发给那个端点**,
729
+ 所以默认不开。
730
+
731
+ 假向量(哈希向量 = 最坏提供方)跑出来的两组数字,正好把上面那张融合表变成实测:
732
+
733
+ | 卷子 | 词面 hit@1 / hit@4 | 假向量 + `trust` | 假向量 + `rrf` |
734
+ |---|---|---|---|
735
+ | 合成基准 704 条 | 50.4% / 67.5% | 18.3% / 25.4% | 26.8% / 43.6% |
736
+ | 真库探针 36 条 | 72.2% / 97.2% | 5.6% / 16.7% | 19.4% / 52.8% |
737
+
738
+ `trust` 对垃圾提供方是**灾难**(真库上 72.2% → 5.6%),`rrf` 也一样亏但守住一半。
739
+ 所以:**不确定提供方靠不靠谱就别开 `trust`**(这也是第一次接第三方接口时的默认建议)。
740
+
741
+ 代价侧(同一次运行数的):池子实际大小中位 31~39 条(不是 50,因为词面只回有分的文档)、
742
+ 每次检索 p50 19~20 ms(假服务在本机,真延迟看端点);提供方按文本缓存让
743
+ **26925 条候选只发了 1994 条(省 92.6%)** —— 同一篇笔记在不同查询里反复出现时只付一次钱。
744
+
702
745
  #### 复用 DSH 里已经配好的那条路由(`provider: "host-embed"`)
703
746
 
704
747
  不想再填一遍环境变量:DSH 设置页 → 模型里配好的路由(`baseURL` + 模型 + API 密钥)可以**直接借来用**:
@@ -931,6 +974,8 @@ npm run probe -- --save # 把这次结果存成基线,之后每次跑自动
931
974
  npm run bench:check # 合成基准 + 和 bench/expectations.json 对账(CI 跑的就是它)
932
975
  npm run bench:bridges # 同义表覆盖率尺子:死值、覆盖、使用面、逐键消融(--ablate / --json / --store)
933
976
  npm run bench:seam # 语义接缝的代价与天花板(四种坏提供方 + 作弊上界 + 融合模式对照)
977
+ npm run bench:ceiling # 池宽天花板:任一提供方在公开数据集上的上界 + 每查询要发多少东西
978
+ npm run bench:embed # embeddings 接缝:--fake 量管线与代价 / --real 用真端点量质量
934
979
  npm run embed:check # 参考提供方自检:探一次 embeddings 接口,报维度与耗时(不打印密钥)
935
980
  npm run bench:rewrite # 宿主模型当改写提供方:真调用量代价与收益(要本机有 dsh;--offline 只重算)
936
981
  npm run bench:mcp # MCP 这一层:冷启动、每步往返、协议帧字节数
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-engramory-recall",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Local BM25 auto-recall, fail-closed write guard, static doctor and a memory panel for the Engramory markdown store — no network, no model, no daemon.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -81,6 +81,8 @@
81
81
  "bench": "node bench/run.mjs",
82
82
  "bench:check": "node bench/run.mjs --check",
83
83
  "bench:seam": "node bench/semantic-seam.mjs",
84
+ "bench:ceiling": "node bench/pool-ceiling.mjs",
85
+ "bench:embed": "node bench/embed-live.mjs",
84
86
  "bench:rewrite": "node bench/host-rewrite.mjs",
85
87
  "bench:bridges": "node bench/bridge-coverage.mjs",
86
88
  "bench:mcp": "node bench/mcp-overhead.mjs",
package/semantic.js CHANGED
@@ -291,13 +291,14 @@ export async function loadProvider(config = {}, deps = {}) {
291
291
  }
292
292
 
293
293
  function normalizeModule(module, source) {
294
- const rerank = module?.rerank;
295
- const expand = module?.expand;
296
- if (typeof rerank === 'function') {
297
- return { name: String(module.name ?? source.split('/').pop()), kind: 'rerank', rerank, source };
298
- }
299
- if (typeof expand === 'function') {
300
- return { name: String(module.name ?? source.split('/').pop()), kind: 'expand', expand, source };
294
+ // 两种写法都收:命名导出(`export function rerank`)与默认导出对象(`export default { rerank }`)。
295
+ // 后者是第三方提供方最自然的写法(工厂函数 + 默认导出),不必逼人家改风格。
296
+ const fallback = String(source).split('/').pop();
297
+ for (const target of [module, module?.default]) {
298
+ if (!target || typeof target !== 'object') continue;
299
+ const name = String(target.name ?? module?.name ?? fallback);
300
+ if (typeof target.rerank === 'function') return { name, kind: 'rerank', rerank: target.rerank, source };
301
+ if (typeof target.expand === 'function') return { name, kind: 'expand', expand: target.expand, source };
301
302
  }
302
303
  return null;
303
304
  }