@a9i5k4/dsh-auto-memory 2.5.3 → 3.0.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.
Files changed (167) hide show
  1. package/README.md +189 -7
  2. package/README.zh-CN.md +189 -7
  3. package/docs/CONTRIBUTORS.html +471 -0
  4. package/docs/FRONTEND-CO-CREATION.md +191 -0
  5. package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
  6. package/docs/HANDOFF-CRITERIA.md +92 -0
  7. package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
  8. package/docs/INTEGRATION-ANALYSIS.md +350 -348
  9. package/docs/PROMO-PROMPT-3.0.md +100 -0
  10. package/docs/USER-GUIDE.en.md +58 -3
  11. package/docs/USER-GUIDE.zh-CN.md +59 -4
  12. package/docs/WHITEPAPER.md +207 -0
  13. package/docs/internal/ACCEPT-35-LIVE.md +143 -0
  14. package/docs/internal/ACCEPTANCE-20260914.md +90 -0
  15. package/docs/internal/ARCH-REVIEW-BRIEF.md +411 -0
  16. package/docs/internal/ARCH-REVIEW-REQUEST.md +201 -0
  17. package/docs/internal/ARCH-REVIEW-ROUND2.md +169 -0
  18. package/docs/internal/ARCH-REVIEW-ROUND3.md +206 -0
  19. package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +397 -0
  20. package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +351 -0
  21. package/docs/internal/ART-DIRECTION-WIREFRAME.md +191 -181
  22. package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +181 -0
  23. package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +314 -0
  24. package/docs/internal/BATTLE-PLAN-20260917.md +871 -0
  25. package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +192 -0
  26. package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +72 -0
  27. package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +131 -0
  28. package/docs/internal/DECISIONS-20260914-SESSION.md +269 -0
  29. package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +219 -0
  30. package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +132 -0
  31. package/docs/internal/FEATURE-INVENTORY.md +531 -0
  32. package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +13 -0
  33. package/docs/internal/G-SERIES-EXECUTION-20260917.md +248 -0
  34. package/docs/internal/G3-DESIGN-20260918.md +82 -0
  35. package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +92 -0
  36. package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +74 -0
  37. package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +352 -0
  38. package/docs/internal/GPT-REVIEW-PROMPT.md +216 -0
  39. package/docs/internal/GROUP-WEBHOOK-SETUP.md +33 -0
  40. package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +309 -0
  41. package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +120 -0
  42. package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +74 -0
  43. package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +175 -0
  44. package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +389 -0
  45. package/docs/internal/ISSUE10-PLAN-20260919.md +254 -0
  46. package/docs/internal/ISSUE10B-FORENSICS-20260919.md +468 -0
  47. package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +150 -0
  48. package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +114 -0
  49. package/docs/internal/KICKOFF-P0.md +254 -0
  50. package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +79 -0
  51. package/docs/internal/MASTER-PLAN-3.0.md +411 -0
  52. package/docs/internal/MEMORY-GOVERNANCE-20260917.md +309 -0
  53. package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +85 -0
  54. package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +222 -0
  55. package/docs/internal/PENDING-FIXES-20260916.md +289 -0
  56. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +705 -0
  57. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +649 -0
  58. package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +225 -0
  59. package/docs/internal/PROGRESS-20260917.md +93 -0
  60. package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +128 -0
  61. package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +163 -0
  62. package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +127 -0
  63. package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +140 -0
  64. package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +138 -0
  65. package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +218 -0
  66. package/docs/internal/RAG-KARPATHY-PROGRAM.md +229 -0
  67. package/docs/internal/REPORT-P0-NIGHTLY.md +212 -0
  68. package/docs/internal/REPORT-P5-ACCEPTANCE.md +31 -0
  69. package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +153 -0
  70. package/docs/internal/RESUME-20260918.md +171 -0
  71. package/docs/internal/RESUME-20260919.md +104 -0
  72. package/docs/internal/REVIEW-WB-GRAPH-SELF.md +81 -0
  73. package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +198 -0
  74. package/docs/internal/ROADMAP-20260917-WEEK.md +439 -0
  75. package/docs/internal/ROADMAP.md +106 -0
  76. package/docs/internal/RUN-P0-NIGHTLY.md +227 -0
  77. package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +185 -0
  78. package/docs/internal/S10-GAP-INVENTORY-20260917.md +239 -0
  79. package/docs/internal/S10-GAPS-PLAIN-20260917.md +125 -0
  80. package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +360 -0
  81. package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +90 -0
  82. package/docs/internal/T6-EXECUTION-20260920.md +130 -0
  83. package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +146 -0
  84. package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +89 -0
  85. package/docs/internal/THESIS-OUTLINE-20260918.md +147 -0
  86. package/docs/internal/THREE-LAYER-CONTRACT.md +219 -0
  87. package/docs/internal/TODO-BACKLOG.md +263 -142
  88. package/docs/internal/TODO-GRAPH.html +715 -0
  89. package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +493 -0
  90. package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +710 -0
  91. package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +710 -0
  92. package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +703 -0
  93. package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +710 -0
  94. package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +715 -0
  95. package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +297 -0
  96. package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +104 -0
  97. package/docs/internal/WB-FORMAT-CONVENTION.md +112 -0
  98. package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +71 -0
  99. package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +56 -0
  100. package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +787 -0
  101. package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +112 -0
  102. package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +230 -0
  103. package/docs/prompts/M8-3-enable-verify.md +49 -49
  104. package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
  105. package/lib/acceptance.js +71 -0
  106. package/lib/activation-host.js +153 -18
  107. package/lib/activation-inbox.js +25 -7
  108. package/lib/board-mode.js +30 -0
  109. package/lib/client.js +1758 -90
  110. package/lib/config-io.js +156 -0
  111. package/lib/context-bridge.js +5 -2
  112. package/lib/context-host.js +86 -15
  113. package/lib/degrade.js +385 -0
  114. package/lib/dsh-home.js +143 -0
  115. package/lib/engine-identity.js +149 -0
  116. package/lib/engine-switch.js +247 -0
  117. package/lib/episodic-store.js +63 -12
  118. package/lib/evidence-store.js +10 -3
  119. package/lib/fact-store.js +22 -3
  120. package/lib/fs-retry.js +46 -0
  121. package/lib/index-sync.js +13 -1
  122. package/lib/index.js +3446 -263
  123. package/lib/intent-clean-safe.js +258 -0
  124. package/lib/intent-clean.js +12 -16
  125. package/lib/l0-extract.js +478 -149
  126. package/lib/l0-index-sync.js +195 -0
  127. package/lib/l0-index.js +349 -239
  128. package/lib/ledger-criteria.js +142 -0
  129. package/lib/m4-corpus.js +8 -2
  130. package/lib/m7-index-sync-host.js +73 -5
  131. package/lib/m7-wire.js +3 -3
  132. package/lib/memory-anchor.js +56 -1
  133. package/lib/memory-envelope.js +257 -0
  134. package/lib/memory-hub.js +138 -13
  135. package/lib/memory-index.js +4 -2
  136. package/lib/memory-mutation.js +246 -0
  137. package/lib/memory-writer.js +204 -24
  138. package/lib/note-status-apply.js +118 -0
  139. package/lib/note-status.js +196 -0
  140. package/lib/procedure-observation.js +48 -0
  141. package/lib/procedure-store.js +118 -20
  142. package/lib/python-setup.js +1 -1
  143. package/lib/python-sidecar-client.js +29 -3
  144. package/lib/recall-fusion.js +83 -12
  145. package/lib/rerank-host.js +160 -0
  146. package/lib/rules-edit.js +159 -0
  147. package/lib/rules-layer.js +261 -0
  148. package/lib/semantic-decide.js +41 -8
  149. package/lib/semantic-js.js +66 -6
  150. package/lib/shadow-host.js +3 -5
  151. package/lib/shadow-retrieval.js +3 -3
  152. package/lib/skill-export-host.js +153 -0
  153. package/lib/skill-export.js +239 -0
  154. package/lib/state-commit.js +245 -0
  155. package/lib/storage-manage.js +6 -0
  156. package/lib/subagent-gc.js +4 -8
  157. package/lib/temporal-parse.js +191 -159
  158. package/lib/tier-layer-inject.js +650 -0
  159. package/lib/tier0-catalog.js +735 -0
  160. package/lib/water-window.js +263 -186
  161. package/lib/wb-contract.js +691 -0
  162. package/lib/wb-sidecar.js +890 -0
  163. package/lib/ws-overview-rank.js +2 -2
  164. package/package.json +1 -1
  165. package/python/m7_embedding_v1.py +5 -5
  166. package/python/worker_semantic_v1.py +17 -6
  167. package/python/worker_v1.py +38 -4
@@ -21,6 +21,19 @@ export const FUSION_RRF_K_V1 = 60
21
21
  /** rank 尺度常数:rank/divisor 把秩归到 (0,1] 量级(默认与 k 同值)。 */
22
22
  export const FUSION_RRF_DIVISOR_V1 = 60
23
23
 
24
+ /**
25
+ * ★M2.5b:层次优先级序(与注入侧一致)。
26
+ *
27
+ * 出处:`tier-layer-inject.js:58 TIER_LAYER_ORDER_V1` —— **必须与之逐字相同**。
28
+ *
29
+ * 为什么不直接 import:本模块头注释承诺「**零依赖**、纯函数、零 IO」,
30
+ * 引入跨模块依赖会破坏该契约(且 tier-layer-inject 自身还有依赖链)。
31
+ * 为了既不破契约、又不产生「双源漂移」,此处**本地复刻**并由测试**断言两者相等**
32
+ * (见 smoke-test-m25b-layer-arm-pre.mjs 的「常量一致性」断言)——
33
+ * 任何一侧改动而另一侧未同步,测试立刻红。
34
+ */
35
+ export const FUSION_LAYER_ORDER_V1 = Object.freeze(['project', 'whiteboard', 'user', 'reflection', 'log'])
36
+
24
37
  /** 版本标识。 */
25
38
  export const RECALL_FUSION_VERSION = 'rrf_fusion_v1'
26
39
 
@@ -45,20 +58,63 @@ export function rankFusionRRFPre(pairs, opts = {}) {
45
58
  const list = Array.isArray(pairs) ? pairs.filter((p) => p && typeof p.memoryId === 'string') : []
46
59
 
47
60
  // 每臂独立排名:非空有限值按分数降序(平局 memoryId 升序)取秩(1 起);缺席者无秩。
48
- const rankArm = (key) => {
61
+ // ★M2.5b:新增 `strMode` —— 层次臂的值是**字符串**,默认的「只收有限数」过滤会把它全部滤掉
62
+ // (实测踩坑:layer 臂曾因此静默恒为 0,套件 [3] 抓到)。strMode=true 时接受非空字符串。
63
+ const rankArm = (key, cmp, strMode) => {
49
64
  const entries = []
50
65
  for (const p of list) {
51
66
  const v = p[key]
52
- if (typeof v === 'number' && Number.isFinite(v)) entries.push({ memoryId: p.memoryId, v })
67
+ if (strMode) {
68
+ if (typeof v === 'string' && v) entries.push({ memoryId: p.memoryId, v })
69
+ } else if (typeof v === 'number' && Number.isFinite(v)) {
70
+ entries.push({ memoryId: p.memoryId, v })
71
+ }
53
72
  }
54
- entries.sort((a, b) => (b.v !== a.v ? b.v - a.v : (a.memoryId < b.memoryId ? -1 : a.memoryId > b.memoryId ? 1 : 0)))
73
+ const tie = (a, b) => (a.memoryId < b.memoryId ? -1 : a.memoryId > b.memoryId ? 1 : 0)
74
+ entries.sort(cmp || ((a, b) => (b.v !== a.v ? b.v - a.v : tie(a, b))))
55
75
  const ranks = new Map()
56
76
  entries.forEach((e, i) => ranks.set(e.memoryId, i + 1))
57
77
  return ranks
58
78
  }
79
+
80
+ /**
81
+ * ★M2.5b:层次臂的**序** = 注入侧的层次优先级序(二者必须一致)。
82
+ * 出处:`tier-layer-inject.js:58 TIER_LAYER_ORDER_V1`
83
+ * = ['project','whiteboard','user','reflection','log']。
84
+ * 语义:**rank 1 = 最高优先级层**。未列出的层(含新增层)排在已知层之后,按名称升序(确定性)。
85
+ * 注意:这里只定义**偏好顺序**,不定义权重;权重问题走 rank-space(见下),
86
+ * 需要调权重时应改分档/序,而不是给分数乘系数(Hindsight #3956)。
87
+ */
88
+ const layerRankOf = (name) => {
89
+ const i = FUSION_LAYER_ORDER_V1.indexOf(name)
90
+ return i >= 0 ? i : FUSION_LAYER_ORDER_V1.length
91
+ }
92
+ const cmpLayer = (a, b) => {
93
+ const la = layerRankOf(String(a.v))
94
+ const lb = layerRankOf(String(b.v))
95
+ if (la !== lb) return la - lb
96
+ const sa = String(a.v), sb = String(b.v)
97
+ if (sa !== sb) return sa < sb ? -1 : 1
98
+ return a.memoryId < b.memoryId ? -1 : a.memoryId > b.memoryId ? 1 : 0
99
+ }
100
+
59
101
  const denseRanks = rankArm('dense')
60
102
  const lexRanks = rankArm('lex')
61
103
 
104
+ // ★M2.5b 层次臂(2026-09-18):可选第四臂。layer 为层次名('project'/'user'/'log'/
105
+ // 'reflection'/'whiteboard')或缺失。与时间臂同一条**退化安全**纪律:
106
+ // 所有 pair 均缺 layer **或 layer 全相等** → 不构建 layer 秩,行为与三臂版**逐字节一致**
107
+ // (fused 不加 0 项、输出对象不含新字段)。
108
+ // 为什么需要它(实测不一致,2026-09-18):Tier-0 **注入**侧有层次优先级
109
+ // (tier-layer-inject.js:58 TIER_LAYER_ORDER_V1 = ['project','whiteboard','user','reflection','log']
110
+ // + tier0-catalog.js:419-455 的 caps/floors),而 **排序**侧对层次完全无感
111
+ // ⇒ 结论层(project)与流水层(log)在融合里**平等竞争**,笔记层常被日志挤掉。
112
+ // rank-space(Hindsight #3956 禁止 score-space 加权):层次只贡献**秩**,不乘分。
113
+ const layers = []
114
+ for (const p of list) { if (typeof p.layer === 'string' && p.layer) layers.push(p.layer) }
115
+ const hasLayer = layers.length > 0 && layers.some((v) => v !== layers[0])
116
+ const layerRanks = hasLayer ? rankArm('layer', cmpLayer, true) : null
117
+
62
118
  // 时间臂(P12 后新增,2026-09-09):可选第三臂。temp 为 1(命中时间范围)/0(未命中)/
63
119
  // 缺失(无时间臂或非日期来源)。所有 pair 均缺 temp **或 temp 全相等**(全 0/全 1)→
64
120
  // 不构建 temp 秩,行为与两臂版逐字节一致(fused 不加 0 项、输出对象不含新字段)——
@@ -84,16 +140,31 @@ export function rankFusionRRFPre(pairs, opts = {}) {
84
140
  rankDense: rd === undefined ? null : rd,
85
141
  rankLex: rl === undefined ? null : rl,
86
142
  }
87
- if (!hasTemp) return base
88
- const rt = tempRanks.get(p.memoryId)
89
- const rrfTemp = rt === undefined ? 0 : 1 / (k + rt / divisor)
90
- return {
91
- ...base,
92
- fused: rrfDense + rrfLex + rrfTemp,
93
- rrfTemp,
94
- tempRaw: typeof p.temp === 'number' && Number.isFinite(p.temp) ? p.temp : null,
95
- rankTemp: rt === undefined ? null : rt,
143
+ // 时间臂与层次臂**互不依赖**:各自独立判 has*,可单独生效、可同时生效。
144
+ let out = base
145
+ if (hasTemp) {
146
+ const rt = tempRanks.get(p.memoryId)
147
+ const rrfTemp = rt === undefined ? 0 : 1 / (k + rt / divisor)
148
+ out = {
149
+ ...out,
150
+ fused: out.fused + rrfTemp,
151
+ rrfTemp,
152
+ tempRaw: typeof p.temp === 'number' && Number.isFinite(p.temp) ? p.temp : null,
153
+ rankTemp: rt === undefined ? null : rt,
154
+ }
155
+ }
156
+ if (hasLayer) {
157
+ const rlay = layerRanks.get(p.memoryId)
158
+ const rrfLayer = rlay === undefined ? 0 : 1 / (k + rlay / divisor)
159
+ out = {
160
+ ...out,
161
+ fused: out.fused + rrfLayer,
162
+ rrfLayer,
163
+ layerRaw: typeof p.layer === 'string' && p.layer ? p.layer : null,
164
+ rankLayer: rlay === undefined ? null : rlay,
165
+ }
96
166
  }
167
+ return out
97
168
  })
98
169
  .sort((x, y) => (y.fused !== x.fused ? y.fused - x.fused : (x.memoryId < y.memoryId ? -1 : x.memoryId > y.memoryId ? 1 : 0)))
99
170
  }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * rerank-host-pre —— P4 精排多级档位 + 有界异步窗口(2026-09-16, rerank_host_v1)。
3
+ *
4
+ * 权威依据: MASTER-PLAN-3.0.md §Phase 4 (H2 按实测重写) + TODO-GRAPH V2-P4 卡。
5
+ * 实测推翻方案假设(留痕): bge-reranker-v2-m3 P95 37.4 秒(50 对/题, RSS 3.84GB);
6
+ * qwen3-reranker-0.6b P95 8.8 秒(小样本探针 10 对/题, RSS 4.95GB, 不可与 bge 直接比);
7
+ * 收益真实: recall@1 0.739 → 0.898(bge); cross-encoder 模型已下载; 真实缺口 = GPU 版 torch
8
+ * (torch 2.13.0+cpu)与快档量化产物。⇒ 精排不得同步等待, 只能走**有界异步窗口**。
9
+ *
10
+ * 设计(卡内 v2 修正, 逐条对应):
11
+ * - 多级档位(用户裁定): 'off' | 'fast'(int8+CPU, 未就绪=不可用) | 'enthusiast'(完整模型+GPU)。
12
+ * 档位只决定"是否允许排精排任务", 一期本地无 GPU torch / 无量化产物 ⇒ fast/enthusiast 均
13
+ * fail closed 降级为粗排(绝不阻塞前台、绝不发出模型请求)。
14
+ * - 一分钟 = 有界后台窗口: 从**入队时**起算含排队与计算, **到期不续命**; 本轮立即用现有排序;
15
+ * 后台完成只进有界结果缓存, 不改本轮 envelope、不再次 emit; 下一请求重新生成 requestKey,
16
+ * 仅 inputKey 完全匹配才复用, 且重跑当轮检查; 没有下一轮就过期, 不主动制造模型请求。
17
+ * - 运行隔离: 精排必须走独立懒加载 worker 角色(注入 opts.rerankFn), 不得占用稠密/索引 worker;
18
+ * 全机最多一个精排任务在跑(busy 门), 忙时继续粗排。
19
+ * - 资源门 T5-5R: 任务超清理宽限未被取消/完成 ⇒ 调 opts.killFn 终止并释放; 期间稠密查询照常。
20
+ * - T5-6R: 本模块不产出任何"异步注入收益"口径 — 结果只进缓存; 零复用就是零复用。
21
+ *
22
+ * 纯主机侧状态机, 模型调用经 opts.rerankFn 注入(测试可控), 零 fs/零网络。
23
+ */
24
+
25
+ export const RERANK_HOST_VERSION = 'rerank_host_v1'
26
+
27
+ /** 有界异步窗口: 从入队起算(毫秒), 含排队与计算; 到期不续命。 */
28
+ export const RERANK_WINDOW_MS_V1 = 60000
29
+ /** 登记的清理宽限: 超窗后仍允许 killFn 在此宽限内终止进程。 */
30
+ export const RERANK_KILL_GRACE_MS_V1 = 5000
31
+ /** 缓存上限(条): 有界结果缓存, LRU 逐出。 */
32
+ export const RERANK_CACHE_MAX_V1 = 16
33
+
34
+ /** 档位解析: 'off'/'fast'/'enthusiast' 之外一律按 off(fail closed)。 */
35
+ export function resolveRerankTierPre(raw) {
36
+ const s = String(raw || '').toLowerCase()
37
+ if (s === 'fast' || s === 'enthusiast') return { tier: s, active: false, reason: 'tier-not-provisioned' }
38
+ if (s === 'off') return { tier: 'off', active: false, reason: 'tier-off' }
39
+ return { tier: 'off', active: false, reason: 'invalid-tier' }
40
+ }
41
+
42
+ /**
43
+ * inputKey: 精排复用的完全匹配键 — 查询 + 排序输入 + miv 的规范化哈希。
44
+ * 任一身份字段变化 ⇒ 键不同 ⇒ 不复用(T5-3R)。
45
+ */
46
+ export function computeRerankInputKeyPre({ query, orderedIds, memoryIndexVersion }) {
47
+ return sha256Hex(JSON.stringify([String(query || ''), JSON.stringify(Array.isArray(orderedIds) ? orderedIds.map(String) : []), String(memoryIndexVersion || '')]))
48
+ }
49
+
50
+ function sha256Hex(s) {
51
+ // 纯 JS sha256 避免引入 node:crypto 依赖面 — 复用 djb2 式双哈希足够做键(非安全用途),
52
+ // 但为确定性可审计, 用 FNV-1a 64 位两次盐化。
53
+ let h1 = 0x811c9dc5, h2 = 0x811c9dc5
54
+ for (let i = 0; i < s.length; i++) {
55
+ const c = s.charCodeAt(i)
56
+ h1 = ((h1 ^ c) * 0x01000193) >>> 0
57
+ h2 = ((h2 ^ (c + i)) * 0x01000193) >>> 0
58
+ }
59
+ return h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')
60
+ }
61
+
62
+ /**
63
+ * 创建精排主机(每引擎一份)。
64
+ * opts: {
65
+ * tier: 'off'|'fast'|'enthusiast',
66
+ * now?: () => ms(测试可注入),
67
+ * rerankFn?: async ({query, ordered}) => number[] // 独立 worker 角色; 懒加载由调用方注入
68
+ * killFn?: () => void // T5-5R: 终止精排进程并释放
69
+ * }
70
+ */
71
+ export function createRerankHostPre(opts = {}) {
72
+ const now = typeof opts.now === 'function' ? opts.now : () => Date.now()
73
+ const resolved = resolveRerankTierPre(opts.tier)
74
+ const stats = { enqueued: 0, reused: 0, expiredDrops: 0, killed: 0, completed: 0, busyRejects: 0, tierRejects: 0 }
75
+ /** Map<inputKey, {at, scores}> — 有界结果缓存(只缓存, 不推送)。 */
76
+ const cache = new Map()
77
+ let running = null // {inputKey, startedAt, done}
78
+
79
+ function pruneExpired() {
80
+ const t = now()
81
+ for (const [k, v] of cache) {
82
+ if (t - v.at > RERANK_WINDOW_MS_V1) { cache.delete(k); stats.expiredDrops++ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 请求一次精排机会(每轮 recall 调用一次)。
88
+ * 返回 {mode, scores?, reason?}:
89
+ * mode='reuse' — 缓存命中(窗口内 + inputKey 完全匹配); 调用方把 scores 并进本轮展示。
90
+ * mode='skip' — 档位关/未就绪/无 worker/忙/无下一轮价值 ⇒ 本轮粗排, 不发模型请求。
91
+ * mode='accepted' — 已入队(有界异步窗口起算); 本轮立即返回(粗排序不变)。
92
+ * **本轮同步路径绝不等待精排**(T5-3R: 前台先返回, 后台完成只进缓存)。
93
+ */
94
+ function offerRerankPre({ query, orderedIds, memoryIndexVersion, requestKey }) {
95
+ pruneExpired()
96
+ const tier = resolveRerankTierPre(opts.tier)
97
+ if (tier.tier === 'off') { stats.tierRejects++; return { mode: 'skip', reason: 'tier-off' } }
98
+ // fast/enthusiast 的可用性由"是否注入了独立 worker"决定(运行隔离: rerankFn 即懒加载 worker 角色)。
99
+ // 环境未供给(GPU torch 缺/量化产物缺) ⇒ 调用方不注入 rerankFn ⇒ 这里 fail closed 走粗排。
100
+ if (typeof opts.rerankFn !== 'function') { stats.tierRejects++; return { mode: 'skip', reason: 'no-worker' } }
101
+ const inputKey = computeRerankInputKeyPre({ query, orderedIds, memoryIndexVersion })
102
+ const hit = cache.get(inputKey)
103
+ if (hit && now() - hit.at <= RERANK_WINDOW_MS_V1) {
104
+ // T5-4R: 复用前调用方必须重跑当轮状态/fv2/冷却/预算检查 — 本模块只担保 inputKey 完全匹配。
105
+ stats.reused++
106
+ return { mode: 'reuse', scores: hit.scores, inputKey }
107
+ }
108
+ if (running) { stats.busyRejects++; return { mode: 'skip', reason: 'busy', inputKey } }
109
+ // 入队: 一期全机最多一个精排任务; 有界窗口从入队起算, 到期不续命。
110
+ stats.enqueued++
111
+ const startedAt = now()
112
+ const job = { inputKey, startedAt, requestKey: String(requestKey || '') }
113
+ running = job
114
+ Promise.resolve()
115
+ .then(() => opts.rerankFn({ query, ordered: Array.isArray(orderedIds) ? orderedIds.map(String) : [] }))
116
+ .then((scores) => {
117
+ // 到期不续命: 完成时已超窗 ⇒ 丢弃(没有下一轮就过期, 不制造请求)。
118
+ if (now() - startedAt > RERANK_WINDOW_MS_V1) { stats.expiredDrops++; return }
119
+ // 缓存条目记入队时刻(卡内口径: 窗口从入队起算, 不是完成时刻 — 否则慢计算等于续命)。
120
+ cache.set(inputKey, { at: startedAt, scores: Array.isArray(scores) ? scores : [] })
121
+ if (cache.size > RERANK_CACHE_MAX_V1) {
122
+ const oldest = [...cache.entries()].sort((a, b) => a[1].at - b[1].at)[0]
123
+ if (oldest) cache.delete(oldest[0])
124
+ }
125
+ stats.completed++
126
+ })
127
+ .catch(() => { /* fail soft: 精排失败不影响粗排 */ })
128
+ .finally(() => { if (running === job) running = null })
129
+ return { mode: 'accepted', inputKey }
130
+ }
131
+
132
+ /**
133
+ * T5-5R: 精排忽略取消/卡死 ⇒ 调用方在登记的清理宽限内调用; 终止进程并释放占用。
134
+ * 返回 {killed:boolean}。稠密查询与本主机其余路径不受影响。
135
+ */
136
+ function killRunningRerankPre() {
137
+ if (!running) return { killed: false }
138
+ running = null
139
+ stats.killed++
140
+ try { if (typeof opts.killFn === 'function') opts.killFn() } catch (_) {}
141
+ return { killed: true }
142
+ }
143
+
144
+ /** 是否存在超窗未完成的运行任务(调用方据此在宽限内 kill)。 */
145
+ function isRunningBeyondWindowPre() {
146
+ return !!(running && now() - running.startedAt > RERANK_WINDOW_MS_V1 + RERANK_KILL_GRACE_MS_V1)
147
+ }
148
+
149
+ function debugView() {
150
+ pruneExpired()
151
+ return {
152
+ version: RERANK_HOST_VERSION, tier: resolved.tier, active: resolved.active,
153
+ running: running ? { inputKey: running.inputKey, elapsedMs: now() - running.startedAt, beyondWindow: isRunningBeyondWindowPre() } : null,
154
+ cacheSize: cache.size, stats,
155
+ windowMs: RERANK_WINDOW_MS_V1, killGraceMs: RERANK_KILL_GRACE_MS_V1,
156
+ }
157
+ }
158
+
159
+ return { offerRerankPre, killRunningRerankPre, isRunningBeyondWindowPre, debugView }
160
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * rules-edit.js —— 「用户级硬性约束」的**条目级**解析与增删改(R7 需求)。
3
+ *
4
+ * ## 背景(用户 2026-09-20 明确要求「这个功能必须要落在前端」)
5
+ *
6
+ * `[规则 — 用户级硬性约束 · 必须遵守]` 这一段是**每轮无条件注入**的硬约束,
7
+ * 真源 = `~/.dsh/memory/MEMORY.md`(见 `rules-layer.js:12` 的真源纪律)。
8
+ * 它**不走语义层** —— 即没有检索/打分/淘汰环节 ⇒ **过时条目不会被自动清理**;
9
+ * 而 AI(`memory_user`)也会往里写错东西。
10
+ * ⇒ 用户必须能**自己增/删/改**,否则错一条就每轮都被误导。
11
+ *
12
+ * ## 与既有渲染层的关系(重要:本模块不改变注入语义)
13
+ *
14
+ * `rules-layer.js` 的 `extractRulesLayerPre()` 负责**读**(把 MEMORY.md 解析成规则段),
15
+ * `renderRulesSectionPre()` 负责**渲染**。本模块只做**编辑定位**:
16
+ * 给定原文,找出「哪一段是规则段、每条规则在第几行」,以及产出改后的新全文。
17
+ * 它**不碰**注入、不碰检索、不碰 store。
18
+ *
19
+ * ## 条目模型
20
+ *
21
+ * 用户级 MEMORY.md 由若干 `## YYYY-MM-DD` 日期段 + `- ` 条目行组成。
22
+ * `rules-layer.js` 的渲染器只取 `- ` 开头的行(且跳过日期段)。
23
+ * ⇒ **一条规则 = 一个 `- ` 行**。本模块据此提供:
24
+ * - `listRuleItemsPre(text)` → [{ line, indent, text, source }]
25
+ * - `updateRuleItemPre(text, i, next)` / `removeRuleItemPre(text, i)` / `appendRuleItemPre(text, body)`
26
+ *
27
+ * `source` 的判定:条目若落在 `## YYYY-MM-DD` 段内 ⇒ 归该日期(多为 AI 写入);
28
+ * 落在段外(文件头部)⇒ 记为 `user`(多为用户手写)。这是 R7-3「标注来源」的依据。
29
+ *
30
+ * ## 纪律
31
+ * - 纯函数:输入文本、输出文本,**不做任何 IO**(IO 由调用方走既有 writeFull 事务)。
32
+ * - **删除 = 真删**(见 R7-4):本层渲染器不认任何状态标记,软删标记会被当正文注入模型。
33
+ * - 零运行时依赖。CRLF、无 BOM。
34
+ */
35
+
36
+ /** 日期段标题(与 rules-layer.js 的 DATE_SECTION_RE 同口径)。 */
37
+ export const RULE_DATE_SECTION_RE_V1 = /^##\s*\d{4}-\d{2}-\d{2}\s*$/
38
+
39
+ /** 条目行:允许前导缩进 + `- `(渲染器同样只看 `- ` 行)。 */
40
+ export const RULE_ITEM_RE_V1 = /^(\s*)-\s+(.*)$/
41
+
42
+ /** 单条规则长度上限(与写闸门同量级,防止一次塞进整篇文档)。 */
43
+ export const RULE_ITEM_MAX_CHARS_V1 = 2000
44
+
45
+ /**
46
+ * 列出所有规则条目(**行号从 0 开始**,供后续增删改定位)。
47
+ *
48
+ * @param {string} text MEMORY.md 全文
49
+ * @returns {Array<{line:number, indent:string, text:string, source:string, dateSection:string|null}>}
50
+ */
51
+ export function listRuleItemsPre(text) {
52
+ const lines = String(text == null ? '' : text).split(/\r?\n/)
53
+ const out = []
54
+ let curDate = null
55
+ for (let i = 0; i < lines.length; i++) {
56
+ const l = lines[i]
57
+ if (RULE_DATE_SECTION_RE_V1.test(l)) { curDate = l.replace(/^##\s*/, '').trim(); continue }
58
+ const m = RULE_ITEM_RE_V1.exec(l)
59
+ if (!m) continue
60
+ const body = m[2].trim()
61
+ if (!body) continue
62
+ out.push({
63
+ line: i,
64
+ indent: m[1] || '',
65
+ text: body,
66
+ // 落在日期段内 ⇒ 多为 AI(memory_user 会带日期标题)写入;否则视为用户手写
67
+ source: curDate ? 'ai' : 'user',
68
+ dateSection: curDate,
69
+ })
70
+ }
71
+ return out
72
+ }
73
+
74
+ /** 内部:切行(保留原始行尾风格判定所需的信息交给 join 处理)。 */
75
+ function splitLinesPre(text) { return String(text == null ? '' : text).split(/\r?\n/) }
76
+
77
+ function joinLinesPre(lines) { return lines.join('\n') }
78
+
79
+ /**
80
+ * **替换**第 idx 条规则(按 listRuleItemsPre 的顺序)。
81
+ *
82
+ * @returns {{ok:boolean, text?:string, error?:string}}
83
+ */
84
+ export function updateRuleItemPre(text, idx, nextBody) {
85
+ const items = listRuleItemsPre(text)
86
+ const body = String(nextBody == null ? '' : nextBody).trim()
87
+ if (!Number.isInteger(idx) || idx < 0 || idx >= items.length) return { ok: false, error: 'index-out-of-range' }
88
+ if (!body) return { ok: false, error: 'empty-body' }
89
+ if (body.length > RULE_ITEM_MAX_CHARS_V1) return { ok: false, error: 'too-long' }
90
+ if (/\r|\n/.test(body)) return { ok: false, error: 'multiline-not-allowed' }
91
+ const lines = splitLinesPre(text)
92
+ const it = items[idx]
93
+ lines[it.line] = it.indent + '- ' + body
94
+ return { ok: true, text: joinLinesPre(lines) }
95
+ }
96
+
97
+ /**
98
+ * **删除**第 idx 条规则(真删,见 R7-4)。
99
+ *
100
+ * ⚠️ 本层渲染器(`renderRulesSectionPre`)不认任何状态标记 ⇒ 软删标记会被当正文注入模型;
101
+ * 故这里**直接移除该行**。不可撤销 —— 调用方必须做二次确认。
102
+ */
103
+ export function removeRuleItemPre(text, idx) {
104
+ const items = listRuleItemsPre(text)
105
+ if (!Number.isInteger(idx) || idx < 0 || idx >= items.length) return { ok: false, error: 'index-out-of-range' }
106
+ const lines = splitLinesPre(text)
107
+ lines.splice(items[idx].line, 1)
108
+ return { ok: true, text: joinLinesPre(lines) }
109
+ }
110
+
111
+ /**
112
+ * **追加**一条规则。
113
+ *
114
+ * 落点规则(与用户手写习惯一致,且保证渲染器一定能读到):
115
+ * - `dateSection` 为空 ⇒ 追加到**文件头部**(用户手写区,`source` 会是 `user`);
116
+ * - 给了 `dateSection` ⇒ 追加到该日期段的**末尾**(AI 写入区)。
117
+ */
118
+ export function appendRuleItemPre(text, body, opts = {}) {
119
+ const b = String(body == null ? '' : body).trim()
120
+ if (!b) return { ok: false, error: 'empty-body' }
121
+ if (b.length > RULE_ITEM_MAX_CHARS_V1) return { ok: false, error: 'too-long' }
122
+ if (/\r|\n/.test(b)) return { ok: false, error: 'multiline-not-allowed' }
123
+ const lines = splitLinesPre(text)
124
+ const want = opts && opts.dateSection ? String(opts.dateSection).trim() : ''
125
+ const newLine = '- ' + b
126
+ if (!want) {
127
+ // 头部插入:跳过开头的空行,避免把条目顶到文件最上面的空行之前
128
+ let at = 0
129
+ while (at < lines.length && lines[at].trim() === '') at++
130
+ lines.splice(at, 0, newLine)
131
+ return { ok: true, text: joinLinesPre(lines) }
132
+ }
133
+ // 找目标日期段,插到该段内最后一个非空行之后
134
+ let secAt = -1
135
+ for (let i = 0; i < lines.length; i++) {
136
+ if (RULE_DATE_SECTION_RE_V1.test(lines[i]) && lines[i].replace(/^##\s*/, '').trim() === want) { secAt = i; break }
137
+ }
138
+ if (secAt < 0) return { ok: false, error: 'date-section-not-found' }
139
+ let end = secAt + 1
140
+ while (end < lines.length && !RULE_DATE_SECTION_RE_V1.test(lines[end])) end++
141
+ let ins = end
142
+ while (ins > secAt + 1 && lines[ins - 1].trim() === '') ins--
143
+ lines.splice(ins, 0, newLine)
144
+ return { ok: true, text: joinLinesPre(lines) }
145
+ }
146
+
147
+ /**
148
+ * **预览**:把规则条目渲染成注入时会长的样子(R7-6)。
149
+ *
150
+ * 只做**只读**格式化,不引入 rules-layer.js 的其它依赖(避免循环 import 风险)。
151
+ */
152
+ export function previewRulesPre(text) {
153
+ const items = listRuleItemsPre(text)
154
+ return items.map((x) => '- ' + x.text).join('\n')
155
+ }
156
+
157
+ /** 空规则段判定(给 UI 显示空态用)。 */
158
+ export function isEmptyRulesPre(text) { return listRuleItemsPre(text).length === 0 }
159
+