@modusensus/dsh-mneme 0.10.0 → 0.11.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.
Files changed (79) hide show
  1. package/README.md +11 -3
  2. package/bin/dsh-mneme-choro.mjs +254 -0
  3. package/docs/CONFIGURATION.md +8 -1
  4. package/lib/api-standalone.js +17 -21
  5. package/lib/api.js +298 -12
  6. package/lib/bootstrap-http.js +105 -0
  7. package/lib/bootstrap.js +5 -1
  8. package/lib/choro-command.js +104 -0
  9. package/lib/client.js +1153 -106
  10. package/lib/config-keys.js +144 -0
  11. package/lib/config.js +28 -0
  12. package/lib/graph/anchoring.js +7 -2
  13. package/lib/index.js +50 -4
  14. package/lib/inject.js +15 -3
  15. package/lib/lang.js +54 -0
  16. package/lib/quality-filter.js +48 -12
  17. package/lib/service.js +72 -8
  18. package/lib/settings.js +8 -1
  19. package/lib/store.js +657 -0
  20. package/lib/tools.js +127 -1
  21. package/lib/wiki/inject.js +164 -0
  22. package/lib/wiki/partition.js +200 -0
  23. package/lib/wiki/plan.js +214 -0
  24. package/lib/wiki/queries.js +220 -0
  25. package/lib/wiki/render.js +187 -0
  26. package/lib/wiki/scan.js +371 -0
  27. package/lib/wiki/t0.js +463 -0
  28. package/lib/wiki/t1.js +496 -0
  29. package/lib/write-admission.js +5 -3
  30. package/package.json +4 -2
  31. package/src/api-standalone.js +17 -21
  32. package/src/api.js +298 -12
  33. package/src/bootstrap-http.js +105 -0
  34. package/src/bootstrap.js +5 -1
  35. package/src/choro-command.js +104 -0
  36. package/src/config-keys.js +144 -0
  37. package/src/config.js +28 -0
  38. package/src/graph/anchoring.js +7 -2
  39. package/src/index.js +50 -4
  40. package/src/inject.js +15 -3
  41. package/src/lang.js +54 -0
  42. package/src/quality-filter.js +48 -12
  43. package/src/service.js +72 -8
  44. package/src/settings.js +8 -1
  45. package/src/store.js +657 -0
  46. package/src/tools.js +127 -1
  47. package/src/wiki/inject.js +164 -0
  48. package/src/wiki/partition.js +200 -0
  49. package/src/wiki/plan.js +214 -0
  50. package/src/wiki/queries.js +220 -0
  51. package/src/wiki/render.js +187 -0
  52. package/src/wiki/scan.js +371 -0
  53. package/src/wiki/t0.js +463 -0
  54. package/src/wiki/t1.js +496 -0
  55. package/src/write-admission.js +5 -3
  56. package/test/api-routes-gaps.test.js +131 -1
  57. package/test/api.test.js +7 -1
  58. package/test/bootstrap.test.js +8 -0
  59. package/test/choro-command.test.js +218 -0
  60. package/test/client.test.js +820 -19
  61. package/test/config-keys-routes.test.js +233 -0
  62. package/test/contrast.test.js +14 -7
  63. package/test/graph-boosted-relations.test.js +189 -0
  64. package/test/graph-multipath.test.js +269 -0
  65. package/test/inject.test.js +51 -0
  66. package/test/llm-audit.test.js +29 -1
  67. package/test/quality-backfill.test.js +290 -0
  68. package/test/quality-filter.test.js +113 -4
  69. package/test/service.test.js +1 -1
  70. package/test/wiki-api.test.js +153 -0
  71. package/test/wiki-inject.test.js +265 -0
  72. package/test/wiki-partition.test.js +112 -0
  73. package/test/wiki-plan.test.js +80 -0
  74. package/test/wiki-queries.test.js +187 -0
  75. package/test/wiki-scan.test.js +208 -0
  76. package/test/wiki-store.test.js +190 -0
  77. package/test/wiki-t0.test.js +380 -0
  78. package/test/wiki-t1.test.js +235 -0
  79. package/test/wiki-tools.test.js +121 -0
package/lib/service.js CHANGED
@@ -560,16 +560,36 @@ export function createService({ store, mirror, config, onWrite, logger, document
560
560
  * 用出来的」延伸成检索信号:查询命中的不只有直接挂联,还有跳跃可达的活跃子图。
561
561
  * 邻居权重 = HOP_QUOTA[hop] × linked tier(attr 1.0 / relation 0.9),仍低于
562
562
  * 直达种子的满权,绝不喧宾夺主。
563
+ *
564
+ * #24 块1 补充(三路起点接线):前半段只喂了 exact 一路,Tag 桥接与向量两路
565
+ * 能力在 anchoring.js 里却从未被调用。现补齐——
566
+ * · exact:查询里出现实体名(布尔 1.0);
567
+ * · tag :查询词命中某条记忆的 tag,该记忆挂联的实体即种子(计数级别 0.4);
568
+ * · vector:查询向量近邻的记忆挂联的实体(cos 走 min-max,连续通路)。
569
+ * 三路在累加前各按自己的尺度归一化(离散级别 vs min-max),混加不会让高频
570
+ * Tag 淹没强向量命中。两路都是弱信号入口:任一路为空就少一路,全空则
571
+ * anchorSeeds 返回空、整个图轴让位(与加这两路之前的行为一致)。
572
+ * 三者共用 graphAnchoringEnabled 一个闸:默认关时仍是逐字节的 #219 单跳轴。
563
573
  */
564
- function entityRecall(q, limit) {
574
+ function entityRecall(q, limit, { queryVector = null } = {}) {
565
575
  if (config?.entityRecallEnabled !== true) return [];
566
576
  try {
567
577
  const entities = store.findEntitiesMentionedIn(q);
568
- if (!entities.length) return [];
578
+ // Tag 桥接与向量两路只服务锚定层:关锚定时不查(少两次库读,也守住
579
+ // 「关档行为逐字节不变」的承诺——见下方 if 分支)。
580
+ const anchoring = config?.graphAnchoringEnabled === true;
581
+ const tagEntities = anchoring && typeof store.findEntitiesByTags === "function"
582
+ ? (() => { try { return store.findEntitiesByTags(q); } catch { return []; } })()
583
+ : [];
584
+ // 早退只在**三路全无来源**时才成立。向量路的存在感恰恰在查询里没有实体名
585
+ // 的时候最大(「数据库迁移」说不出任何实体),若仍以「名字没命中」早退,
586
+ // 第二、三路就永远等不到自己被调用——接上了也等于没接。
587
+ const canVector = anchoring && Array.isArray(queryVector) && queryVector.length > 0 && !!vectorIndex;
588
+ if (!entities.length && !tagEntities.length && !canVector) return [];
569
589
  // 关闭锚定级联=逐字节复用 #219 单跳轴:种子裁剪/重排/截断全是 #24 新增的
570
590
  // 行为,默认关时一条都不许漏进旧路径(graphSeedCap 也只在开启后生效——
571
591
  // 无论 cap 调多小,关档时命中实体仍全量挂联)。
572
- if (config?.graphAnchoringEnabled !== true) {
592
+ if (!anchoring) {
573
593
  const linked = store.getLinkedMemoryIds(entities.map((e) => e.id));
574
594
  if (!linked.size) return [];
575
595
  const hits = [];
@@ -581,9 +601,43 @@ export function createService({ store, mirror, config, onWrite, logger, document
581
601
  }
582
602
  return hits;
583
603
  }
584
- // 锚定种子(精确名布尔通路——命名实体逻辑上是离散匹配,不参与 min-max)。
585
- const exact = normalizePath(entities.map((e) => e.id), { path: "exact" });
586
- const seeds = anchorSeeds({ paths: [exact], cap: config?.graphSeedCap ?? 12 });
604
+ // 三路激活信号,各自先归一化再累加(尺度不同,混加会让高频通路淹没强相关)。
605
+ const paths = [];
606
+ // 第一路:精确名(布尔通路——命名实体逻辑上是离散匹配,不参与 min-max)。
607
+ if (entities.length) {
608
+ paths.push(normalizePath(entities.map((e) => e.id), { path: "exact" }));
609
+ }
610
+ // 第二路:Tag 桥接(tag 共现,离散级别 0.4——比精确名弱、比子串强)。
611
+ if (tagEntities.length) {
612
+ paths.push(normalizePath(tagEntities.map((e) => e.id), { path: "tag" }));
613
+ }
614
+ // 第三路:向量(连续通路,cos 相似度走 min-max)。记忆 → 挂联实体反查,
615
+ // 同一实体被多条近邻记忆挂联时取最高分(一条更近的记忆就够证明相关)。
616
+ if (Array.isArray(queryVector) && queryVector.length && vectorIndex) {
617
+ try {
618
+ const near = vectorIndex.search(queryVector, { limit: limit * 2, threshold: 0 });
619
+ const bestOf = new Map();
620
+ for (const m of near) {
621
+ const score = m?.score ?? 0;
622
+ if (!(score > 0)) continue;
623
+ for (const eid of store.entityIdsForMemories([m.id])) {
624
+ if (!(bestOf.get(eid) >= score)) bestOf.set(eid, score);
625
+ }
626
+ }
627
+ if (bestOf.size) {
628
+ paths.push(normalizePath(
629
+ [...bestOf.entries()].map(([id, score]) => ({ id, score })),
630
+ { path: "vector", continuous: true }
631
+ ));
632
+ }
633
+ } catch { /* 向量路失败只是少一路,不拖垮整条图轴 */ }
634
+ }
635
+ if (!paths.length) return [];
636
+ // 激活分为 0 的种子不算种子:min-max 会把**连续通路里最弱的那条**压成 0,
637
+ // 可下游给种子挂联记忆的是固定满权 tier(1.0/0.9),不看种子自己的分数。
638
+ // 留着 0 分种子,等于让「这一路里最不像的那条」以最强的方式把记忆拉进池。
639
+ // 零分项在 anchorSeeds 里排最后,所以先取 cap 再滤掉不会浪费名额。
640
+ const seeds = anchorSeeds({ paths, cap: config?.graphSeedCap ?? 12 }).filter((s) => s.score > 0);
587
641
  if (!seeds.length) return [];
588
642
  // 0-hop 直达:挂联记忆按 tier 定分(attr 1.0 / relation 0.9)——与 #219
589
643
  // 单跳轴同分。MAX 语义:同一条记忆被多实体挂联时保留最高分。
@@ -939,6 +993,9 @@ export function createService({ store, mirror, config, onWrite, logger, document
939
993
  const keyword = rawKeyword.map((m) => ({ ...m, score: scoreKeyword(m, q), source: "keyword" }));
940
994
  const wantVector = mode === "vector" || mode === "hybrid" || (mode === "auto" && !!embedder);
941
995
  let vector = [];
996
+ // #24 块1:查询向量在这里算出来,图轴的向量起点要用同一份——再嵌一次既
997
+ // 多一次模型调用,也可能拿到不同的向量(本地模型带随机性时)。
998
+ let vectorQuery = null;
942
999
  if (wantVector && embedder) {
943
1000
  try {
944
1001
  // Legacy embedders expose embed(query); local ones expose embedSingle.
@@ -947,6 +1004,7 @@ export function createService({ store, mirror, config, onWrite, logger, document
947
1004
  : embedder.embed.bind(embedder);
948
1005
  const qv = await embedSingle(q);
949
1006
  if (qv?.length) {
1007
+ vectorQuery = qv;
950
1008
  // Adaptive threshold (v0.5.0 1.2): the fetch runs at the loosest
951
1009
  // branch floor so the head-gap rule can still re-admit the tail;
952
1010
  // the final cutoff is computed against the fetched score
@@ -980,7 +1038,7 @@ export function createService({ store, mirror, config, onWrite, logger, document
980
1038
  // mentioned entities pull their linked memories into the pool. Keyword
981
1039
  // mode stays text-only per the mode contract, so the axis is skipped
982
1040
  // there even when enabled.
983
- const entity = mode === "keyword" ? [] : entityRecall(q, lim);
1041
+ const entity = mode === "keyword" ? [] : entityRecall(q, lim, { queryVector: vectorQuery });
984
1042
  // Entity axis shares BM25's backfill weight (we = wb) until #217 recall
985
1043
  // data justifies a distinct entity weight.
986
1044
 
@@ -1819,7 +1877,7 @@ export function createService({ store, mirror, config, onWrite, logger, document
1819
1877
  // 为空时兜底检索,避免注入路径双倍候选成本。
1820
1878
  if (config?.entityRecallEnabled === true && q) {
1821
1879
  try {
1822
- const graphHits = entityRecall(q, maxItems * 2);
1880
+ const graphHits = entityRecall(q, maxItems * 2, { queryVector });
1823
1881
  if (graphHits.length) {
1824
1882
  const graphIds = new Set(graphHits.map((m) => m.id));
1825
1883
  candidates = [
@@ -2864,6 +2922,8 @@ export function createService({ store, mirror, config, onWrite, logger, document
2864
2922
  listLlmAudits: (opts) => store.listLlmAudits(opts),
2865
2923
  countLlmAudits: (opts) => store.countLlmAudits(opts),
2866
2924
  getLlmAuditStats: (opts) => store.getLlmAuditStats(opts),
2925
+ // 面板「8 周沉淀」图(只读聚合):进来的 / 真沉淀的 / 归档的按天三条线。
2926
+ getConsolidationStats: (opts) => store.getConsolidationStats(opts),
2867
2927
  deleteOldLlmAudits: (before) => store.deleteOldLlmAudits(before),
2868
2928
  // Entity gene (v0.3.0) passthroughs for the autoDream apply path
2869
2929
  // (applyDecisions): records supersedes relations after an update and
@@ -2871,7 +2931,11 @@ export function createService({ store, mirror, config, onWrite, logger, document
2871
2931
  // passthroughs above — never write-hook-triggering memory mutations.
2872
2932
  saveRelation: (r) => store.saveRelation(r),
2873
2933
  listEntities: (o) => store.listEntities(o),
2934
+ countEntities: () => store.countEntities(),
2874
2935
  getRelations: (id) => store.getRelations(id),
2936
+ // #24 块4 被动确认的复核出口:列出权重被 bump 抬过的边(gain = 现权 −
2937
+ // 该 source 的初值)。纯读,不动权重——「看哪些边被用出来过」是人的动作。
2938
+ listBoostedRelations: (o) => store.listBoostedRelations(o),
2875
2939
  // v0.7.0 实体热投影:实体热 = 关联记忆 heat 聚合(取 max)。无关联记忆
2876
2940
  // 或 heatEnabled=false 时返回 null;前端据此决定图谱节点大小/明暗。
2877
2941
  entityHeat: (entityId) => {
package/lib/settings.js CHANGED
@@ -192,6 +192,10 @@ const FEATURE_FLAG_BOOLEANS = [
192
192
  // 反向——那两个藏默认在场的工具,本键开默认不在场的能力(默认关=工具根本不存在,
193
193
  // 行为逐字节不变)。面板可启停=线上回滚开关。
194
194
  "organizeEnabled",
195
+ // chorography(仓库地志)的暴露闸:开默认不在场的三个只读工具 + 面板浏览页
196
+ // (默认关=模型工具列表里根本没有它们,行为逐字节不变)。同 organizeEnabled
197
+ // 的反向语义——见 config.js 的键注释。
198
+ "chorographyEnabled",
195
199
  // 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
196
200
  // 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
197
201
  // 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
@@ -356,7 +360,10 @@ const FEATURE_FLAG_ENUMS = {
356
360
  // Issue #125:dream 候选集构造方式(window 默认 = 现状;hybrid 并入向量组)。
357
361
  dreamCandidateMode: ["window", "hybrid"],
358
362
  // 本地嵌入池化:auto = 按模型族判定(BGE → cls),可显式覆盖为 cls / mean。
359
- localEmbedPooling: ["auto", "cls", "mean"]
363
+ localEmbedPooling: ["auto", "cls", "mean"],
364
+ // M3 开局注入档位(spec §7):轻量档在消费侧压成 off,见 wiki/inject.js 的
365
+ // resolveInjectTier——LIGHT_MODE_OFF 对平铺键一律赋 false,枚举值进去会静默失效。
366
+ chorographyInject: ["off", "light", "full"]
360
367
  };
361
368
  const FEATURE_FLAG_STRING_MAX = 200;
362
369