@a9i5k4/dsh-auto-memory 3.1.5 → 3.1.7

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 (93) hide show
  1. package/docs/CONTINUITY-FLOW.md +6 -6
  2. package/docs/HANDBOOK.md +54 -53
  3. package/docs/M-CM-PLAN.md +1 -1
  4. package/docs/M-CM-STATE.md +1 -1
  5. package/docs/M-CM7-HANDOFF-LAYERED-RETRIEVAL.md +1 -1
  6. package/docs/M3B-CONTRACT.md +7 -7
  7. package/docs/M7-AUTONOMOUS-STATE.md +1 -1
  8. package/docs/M7-TASKSET-DISPATCH.md +1 -1
  9. package/docs/PROMPT-PACK-LAYERED-RECALL.md +3 -3
  10. package/docs/PROMPT-SET-STRICT.md +3 -3
  11. package/docs/RELEASE-GO-NOGO.md +1 -1
  12. package/docs/STATUS-BOARD.md +1 -1
  13. package/docs/USER-GUIDE.en.md +21 -21
  14. package/docs/USER-GUIDE.zh-CN.md +21 -21
  15. package/docs/WHITEPAPER.md +1 -1
  16. package/docs/prompts/FIX-AGENT-P12-FULL-REGRESSION.md +1 -1
  17. package/docs/prompts/LIVE-VERIFY-ZCODE.md +2 -2
  18. package/docs/prompts/P2-semantic-recall.md +2 -2
  19. package/docs/prompts/P7-write-fix.md +1 -1
  20. package/docs/prompts/RELEASE-AGENT.md +4 -4
  21. package/docs/prompts/ZCODE-DROPIN.md +1 -1
  22. package/lib/acceptance.js +6 -6
  23. package/lib/activation-host.js +15 -14
  24. package/lib/activation-inbox-state.js +4 -4
  25. package/lib/activation-inbox.js +44 -44
  26. package/lib/board-mode.js +3 -3
  27. package/lib/client.js +225 -41
  28. package/lib/config-io.js +12 -12
  29. package/lib/context-bridge.js +22 -22
  30. package/lib/context-host.js +24 -23
  31. package/lib/datadir.js +95 -0
  32. package/lib/degrade.js +21 -21
  33. package/lib/dsh-home.js +8 -8
  34. package/lib/engine-identity.js +12 -12
  35. package/lib/engine-switch.js +3 -3
  36. package/lib/episodic-store.js +13 -13
  37. package/lib/evidence-agg.js +4 -4
  38. package/lib/evidence-store.js +12 -12
  39. package/lib/fact-store.js +47 -47
  40. package/lib/handoff-anchor.js +4 -4
  41. package/lib/hub-io.js +356 -3
  42. package/lib/index-sync.js +5 -5
  43. package/lib/index.js +426 -242
  44. package/lib/intent-clean-safe.js +3 -3
  45. package/lib/intent-clean.js +1 -1
  46. package/lib/l0-extract.js +15 -15
  47. package/lib/l0-index-sync.js +8 -8
  48. package/lib/l0-index.js +11 -11
  49. package/lib/ledger-criteria.js +8 -8
  50. package/lib/m4-corpus.js +1 -1
  51. package/lib/m7-index-sync-host.js +2 -2
  52. package/lib/m7-wire.js +24 -24
  53. package/lib/memory-anchor.js +1 -1
  54. package/lib/memory-envelope.js +15 -15
  55. package/lib/memory-hub.js +95 -50
  56. package/lib/memory-importance.js +7 -7
  57. package/lib/memory-mutation.js +5 -5
  58. package/lib/note-status-apply.js +2 -2
  59. package/lib/note-status.js +7 -7
  60. package/lib/policies/activation_policy_v2.json +2 -2
  61. package/lib/policies/recall_intent_lr_v1.json +1 -1
  62. package/lib/procedure-observation.js +2 -2
  63. package/lib/procedure-store.js +117 -28
  64. package/lib/python-setup.js +6 -5
  65. package/lib/python-sidecar-client.js +26 -26
  66. package/lib/recall-fusion.js +12 -12
  67. package/lib/recall-stats.js +36 -1
  68. package/lib/rerank-host.js +11 -11
  69. package/lib/rules-edit.js +9 -9
  70. package/lib/rules-layer.js +26 -26
  71. package/lib/semantic-decide.js +7 -7
  72. package/lib/semantic-js.js +13 -13
  73. package/lib/shadow-host.js +7 -6
  74. package/lib/shadow-retrieval.js +40 -40
  75. package/lib/skill-export-host.js +7 -7
  76. package/lib/skill-export.js +6 -6
  77. package/lib/state-commit.js +10 -10
  78. package/lib/storage-manage.js +6 -6
  79. package/lib/tier-layer-inject.js +28 -28
  80. package/lib/tier0-catalog.js +4 -4
  81. package/lib/wb-contract.js +45 -45
  82. package/lib/wb-sidecar.js +11 -11
  83. package/package.json +3 -4
  84. package/python/m7_activation_features_v2.py +7 -7
  85. package/python/m7_embedding_v1.py +12 -12
  86. package/python/policies/activation_policy_v2.json +2 -2
  87. package/python/policies/decision-record-activation-v2-delta-exp-override-20260824.json +1 -1
  88. package/python/policies/decision-record-reasoning-kind-admission-20260826.json +2 -2
  89. package/python/policies/decision-record-stale-gate-per-candidate-20260825.json +2 -2
  90. package/python/policies/recall_intent_lr_v1.json +1 -1
  91. package/python/verify_policy_artifact.py +3 -3
  92. package/python/worker_semantic_v1.py +18 -18
  93. package/python/worker_v1.py +18 -18
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 三层注入装配(tier_layer_inject_v1)—— 契约 C5
2
+ * 三层注入装配(tier_layer_inject_pre_v1)—— 契约 C5
3
3
  * (`docs/internal/THREE-LAYER-CONTRACT.md` §2.1/§5、`SEMANTIC-ARCHITECTURE-SPEC.md` S5/S9)。
4
4
  *
5
5
  * 2026-09-14 建立。职责 = **把三层按闸门装成一段可注入文本**:
@@ -33,10 +33,10 @@ import {
33
33
  // 而本模块的 S9 检查只禁网络/LLM/子进程/await,不禁内部纯模块引用(该模块本身零 import)。
34
34
  import { isCurrentPre } from './l0-extract.js'
35
35
 
36
- export const TIER_INJECT_VERSION = 'tier_layer_inject_v1'
36
+ export const TIER_INJECT_VERSION = 'tier_layer_inject_pre_v1'
37
37
 
38
38
  /** 契约预算(冻结;变更须同步 `THREE-LAYER-CONTRACT.md` §4.3)。 */
39
- export const TIER_BUDGET_V1 = Object.freeze({
39
+ export const TIER_BUDGET_PRE_V1 = Object.freeze({
40
40
  B0: 800,
41
41
  L1: 140,
42
42
  K: 8,
@@ -47,7 +47,7 @@ export const TIER_BUDGET_V1 = Object.freeze({
47
47
  })
48
48
 
49
49
  /** 段落标记(可读、可 grep;不改既有 Reference Tail 的固定边界行)。 */
50
- export const TIER_MARK_V1 = Object.freeze({
50
+ export const TIER_MARK_PRE_V1 = Object.freeze({
51
51
  degrade: '[降级]',
52
52
  gate: '[闸门]',
53
53
  account: '[层账]',
@@ -55,7 +55,7 @@ export const TIER_MARK_V1 = Object.freeze({
55
55
  })
56
56
 
57
57
  /** 五层展示顺序(与契约 §2 优先级一致:project > whiteboard > user > reflection > log)。 */
58
- export const TIER_LAYER_ORDER_V1 = Object.freeze(['project', 'whiteboard', 'user', 'reflection', 'log'])
58
+ export const TIER_LAYER_ORDER_PRE_V1 = Object.freeze(['project', 'whiteboard', 'user', 'reflection', 'log'])
59
59
 
60
60
  /** 深挖语义(决定"要不要下探 Tier-1"):含这些词说明用户在问"怎么/为什么/具体"。 */
61
61
  const DEEP_INTENT_RE = /为什么|怎么|如何|具体|复现|细节|原理|根因|哪个文件|在哪/
@@ -143,7 +143,7 @@ export function filterExcludedSourcesPre(hits, patterns) {
143
143
  }
144
144
  const normPath = (s) => String(s == null ? '' : s).replace(/[\\/]+/g, '/').toLowerCase()
145
145
  const isDirPat = (p) => /[\\/]$/.test(p)
146
- const layers = new Set(TIER_LAYER_ORDER_V1)
146
+ const layers = new Set(TIER_LAYER_ORDER_PRE_V1)
147
147
  const matched = new Set()
148
148
  const kept = []
149
149
  const dropped = []
@@ -189,7 +189,7 @@ export function filterExcludedSourcesPre(hits, patterns) {
189
189
  * 于是「A 快照产生的候选,到 B 快照才准备输出」时会照样被复用(A 的正文进了 B 的注入),
190
190
  * 这正是 T0-2 要挡的场景(契约 I6:三层必须来自同一份快照,混版视为错误)。
191
191
  */
192
- export const TIER_HITS_REUSE_REASONS_V1 = Object.freeze({
192
+ export const TIER_HITS_REUSE_REASONS_PRE_V1 = Object.freeze({
193
193
  ok: '投影仍属当前快照',
194
194
  'no-projection': '本轮无命中投影(尚未发生激活投递)',
195
195
  'stale-time': '投影超出新鲜度窗口(仅时间新鲜不足以保证版本一致)',
@@ -205,7 +205,7 @@ export const TIER_HITS_REUSE_REASONS_V1 = Object.freeze({
205
205
  /** 原因码 → 可读中文(未知码原样返回,便于排障时不吞信息)。 */
206
206
  export function describeReuseReasonPre(code) {
207
207
  const k = clean(code)
208
- return TIER_HITS_REUSE_REASONS_V1[k] || k || '未知原因'
208
+ return TIER_HITS_REUSE_REASONS_PRE_V1[k] || k || '未知原因'
209
209
  }
210
210
 
211
211
  /**
@@ -285,7 +285,7 @@ export function selectReusableTierHitsPre(input = {}) {
285
285
  }
286
286
 
287
287
  /** 跳过原因 → 可读中文(I7:降级行必须人能读懂,不能只写机器枚举)。 */
288
- export const SKIP_REASON_TEXT_V1 = Object.freeze({
288
+ export const SKIP_REASON_TEXT_PRE_V1 = Object.freeze({
289
289
  'missing-path': '路径未提供',
290
290
  'not-found': '文件不存在',
291
291
  'not-a-file': '路径不是文件',
@@ -301,7 +301,7 @@ export const SKIP_REASON_TEXT_V1 = Object.freeze({
301
301
  export function describeReasonPre(reason) {
302
302
  const key = clean(reason)
303
303
  if (!key) return '未知原因'
304
- return SKIP_REASON_TEXT_V1[key] || key
304
+ return SKIP_REASON_TEXT_PRE_V1[key] || key
305
305
  }
306
306
 
307
307
  /**
@@ -310,7 +310,7 @@ export function describeReasonPre(reason) {
310
310
  * @param {string[]} [order]
311
311
  */
312
312
  export function tierLayerAccountLinePre(perLayer, order) {
313
- const layers = Array.isArray(order) && order.length ? order : TIER_LAYER_ORDER_V1
313
+ const layers = Array.isArray(order) && order.length ? order : TIER_LAYER_ORDER_PRE_V1
314
314
  const segs = layers.map((layer) => {
315
315
  const m = (perLayer && perLayer[layer]) || null
316
316
  if (!m) return layer + ' ?'
@@ -318,7 +318,7 @@ export function tierLayerAccountLinePre(perLayer, order) {
318
318
  return layer + ' ' + m.picked + '/' + m.candidates + (m.dropped ? '(裁' + m.dropped + ')' : '')
319
319
  })
320
320
  const dropped = layers.reduce((a, l) => a + (((perLayer && perLayer[l]) || {}).dropped || 0), 0)
321
- return TIER_MARK_V1.account + ' ' + segs.join(' · ') + (dropped ? ' · 合计裁剪 ' + dropped + ' 条' : '')
321
+ return TIER_MARK_PRE_V1.account + ' ' + segs.join(' · ') + (dropped ? ' · 合计裁剪 ' + dropped + ' 条' : '')
322
322
  }
323
323
 
324
324
  /**
@@ -331,7 +331,7 @@ export function collectDegradationsPre(input = {}) {
331
331
  const o = input && typeof input === 'object' ? input : {}
332
332
  const catalog = o.catalog || null
333
333
  const out = []
334
- const mark = TIER_MARK_V1.degrade + ' '
334
+ const mark = TIER_MARK_PRE_V1.degrade + ' '
335
335
 
336
336
  if (!catalog) {
337
337
  out.push({ code: 'catalog-missing', text: mark + 'Tier-0 目录生成器未返回结果(本轮无目录层)' })
@@ -424,8 +424,8 @@ function hitSummarySourcePre(h) {
424
424
  * 超出 K 的部分**显式计数**(不静默截断)。
425
425
  */
426
426
  export function buildTier1SectionPre(hits, opts = {}) {
427
- const K = Math.max(1, Number((opts && opts.maxItems) || TIER_BUDGET_V1.K))
428
- const L1 = Math.max(8, Number((opts && opts.itemChars) || TIER_BUDGET_V1.L1))
427
+ const K = Math.max(1, Number((opts && opts.maxItems) || TIER_BUDGET_PRE_V1.K))
428
+ const L1 = Math.max(8, Number((opts && opts.itemChars) || TIER_BUDGET_PRE_V1.L1))
429
429
  const list = (Array.isArray(hits) ? hits.filter(Boolean) : [])
430
430
  .map((h, i) => ({ h, i, s: Number(h.score) }))
431
431
  .sort((a, b) => (Number.isFinite(b.s) ? b.s : -1) - (Number.isFinite(a.s) ? a.s : -1) || a.i - b.i)
@@ -440,7 +440,7 @@ export function buildTier1SectionPre(hits, opts = {}) {
440
440
  })
441
441
  const overflow = list.length - kept.length
442
442
  const head = '[Tier-1 命中摘要 · ' + kept.length + '/' + list.length + ' 条 · 每条 ≤' + L1 + ' 字 · K=' + K + ']'
443
- const tail = overflow > 0 ? [TIER_MARK_V1.degrade + ' Tier-1 另 ' + overflow + ' 条命中未展开(K=' + K + ' 上限);需要时用 memory_recall 取'] : []
443
+ const tail = overflow > 0 ? [TIER_MARK_PRE_V1.degrade + ' Tier-1 另 ' + overflow + ' 条命中未展开(K=' + K + ' 上限);需要时用 memory_recall 取'] : []
444
444
  return {
445
445
  text: [head].concat(lines, tail).join('\n'),
446
446
  lines,
@@ -454,8 +454,8 @@ export function buildTier1SectionPre(hits, opts = {}) {
454
454
  * Tier-2 渲染:命中块原文,单块 ≤ `B2`(默认 2400)字符;超长块显式标注"已截断,全文 N 字符"。
455
455
  */
456
456
  export function buildTier2SectionPre(hits, opts = {}) {
457
- const B2 = Math.max(80, Number((opts && opts.maxChars) || TIER_BUDGET_V1.B2))
458
- const maxBlocks = Math.max(1, Number((opts && opts.maxBlocks) || TIER_BUDGET_V1.maxTier2Blocks))
457
+ const B2 = Math.max(80, Number((opts && opts.maxChars) || TIER_BUDGET_PRE_V1.B2))
458
+ const maxBlocks = Math.max(1, Number((opts && opts.maxBlocks) || TIER_BUDGET_PRE_V1.maxTier2Blocks))
459
459
  const list = (Array.isArray(hits) ? hits.filter(Boolean) : [])
460
460
  .map((h, i) => ({ h, i, s: Number(h.score) }))
461
461
  .sort((a, b) => (Number.isFinite(b.s) ? b.s : -1) - (Number.isFinite(a.s) ? a.s : -1) || a.i - b.i)
@@ -496,7 +496,7 @@ export function buildTier2SectionPre(hits, opts = {}) {
496
496
  */
497
497
  export function composeTieredInjectionPre(input = {}) {
498
498
  const o = input && typeof input === 'object' ? input : {}
499
- const maxTokens = Math.max(60, Number(o.maxTokens) > 0 ? Math.min(Number(o.maxTokens), TIER_BUDGET_V1.B0) : TIER_BUDGET_V1.B0)
499
+ const maxTokens = Math.max(60, Number(o.maxTokens) > 0 ? Math.min(Number(o.maxTokens), TIER_BUDGET_PRE_V1.B0) : TIER_BUDGET_PRE_V1.B0)
500
500
  // I5 状态过滤(2026-09-14 P0 / C8):**闸门之前**先挡下非 current —— 顺序很关键。
501
501
  // 若放在 Tier-1 渲染里过滤,闸门仍会看到"有命中"而下探、渲染出空段,浪费一轮预算且语义错乱;
502
502
  // 放在闸门前,则"全是 superseded"会自然退化成 tier0(本轮无可下探内容),这才是真实处境。
@@ -516,7 +516,7 @@ export function composeTieredInjectionPre(input = {}) {
516
516
  const catalog = o.catalog || buildTier0CatalogFromTextPre(Array.isArray(o.sources) ? o.sources : [], {
517
517
  maxTokens,
518
518
  estimateMode: 'conservative',
519
- quota: { projectRatio: TIER_BUDGET_V1.projectRatio, floorRatio: TIER_BUDGET_V1.floorRatio },
519
+ quota: { projectRatio: TIER_BUDGET_PRE_V1.projectRatio, floorRatio: TIER_BUDGET_PRE_V1.floorRatio },
520
520
  })
521
521
  const degradations = collectDegradationsPre({
522
522
  catalog,
@@ -528,7 +528,7 @@ export function composeTieredInjectionPre(input = {}) {
528
528
  if (exHits.droppedCount > 0) {
529
529
  degradations.push({
530
530
  code: 'excluded-source',
531
- text: TIER_MARK_V1.degrade + ' 已按用户排除项挡下 ' + exHits.droppedCount + ' 条命中('
531
+ text: TIER_MARK_PRE_V1.degrade + ' 已按用户排除项挡下 ' + exHits.droppedCount + ' 条命中('
532
532
  + exHits.matchedPatterns.slice(0, 3).map((p) => clean(p)).join(' / ') + ')· 不进注入',
533
533
  })
534
534
  }
@@ -538,7 +538,7 @@ export function composeTieredInjectionPre(input = {}) {
538
538
  const states = Array.from(new Set(gateHits.dropped.map((h) => clean(h && h.status) || '(空)')))
539
539
  degradations.push({
540
540
  code: 'status-filtered',
541
- text: TIER_MARK_V1.degrade + ' 已按 I5 挡下 ' + gateHits.droppedCount + ' 条非 current 命中('
541
+ text: TIER_MARK_PRE_V1.degrade + ' 已按 I5 挡下 ' + gateHits.droppedCount + ' 条非 current 命中('
542
542
  + states.join('/') + ')· 审计视图仍可见,但不进注入',
543
543
  })
544
544
  }
@@ -550,7 +550,7 @@ export function composeTieredInjectionPre(input = {}) {
550
550
  ? buildTier2SectionPre(gateHits.kept, { maxChars: o.B2, maxBlocks: o.maxBlocks })
551
551
  : null
552
552
 
553
- const head = '[Tier-0 常驻目录 · 指引层 · ≤B0=' + TIER_BUDGET_V1.B0 + ' token(实计 ' + catalog.tokens + ') · '
553
+ const head = '[Tier-0 常驻目录 · 指引层 · ≤B0=' + TIER_BUDGET_PRE_V1.B0 + ' token(实计 ' + catalog.tokens + ') · '
554
554
  + (Array.isArray(catalog.items) ? catalog.items.length : 0) + ' 条]'
555
555
  const headParts = [head]
556
556
  if (Array.isArray(catalog.items) && catalog.items.length) headParts.push(catalog.text)
@@ -558,12 +558,12 @@ export function composeTieredInjectionPre(input = {}) {
558
558
  for (const d of degradations) headParts.push(d.text)
559
559
  const drillParts = []
560
560
  if (gate.level === 'tier0') {
561
- headParts.push(TIER_MARK_V1.gate + ' 本轮无语义命中(' + gate.reasons.join(',') + ')→ 仅目录层,未下探 Tier-1/Tier-2')
561
+ headParts.push(TIER_MARK_PRE_V1.gate + ' 本轮无语义命中(' + gate.reasons.join(',') + ')→ 仅目录层,未下探 Tier-1/Tier-2')
562
562
  } else {
563
- headParts.push(TIER_MARK_V1.gate + ' 本轮命中 ' + gate.hitCount + ' 条(' + gate.reasons.join(',') + ')→ 下探 Tier-1')
563
+ headParts.push(TIER_MARK_PRE_V1.gate + ' 本轮命中 ' + gate.hitCount + ' 条(' + gate.reasons.join(',') + ')→ 下探 Tier-1')
564
564
  if (tier1) drillParts.push(tier1.text)
565
565
  if (tier2) {
566
- drillParts.push(TIER_MARK_V1.gate + ' 需要证据语义 → 继续下探 Tier-2(单块 ≤' + TIER_BUDGET_V1.B2 + ' 字符)')
566
+ drillParts.push(TIER_MARK_PRE_V1.gate + ' 需要证据语义 → 继续下探 Tier-2(单块 ≤' + TIER_BUDGET_PRE_V1.B2 + ' 字符)')
567
567
  drillParts.push(tier2.text)
568
568
  }
569
569
  }
@@ -582,7 +582,7 @@ export function composeTieredInjectionPre(input = {}) {
582
582
  let claimedTotalChars = 0
583
583
  const maxTotal = Number(o.maxTotalChars) > 0 ? Number(o.maxTotalChars) : 0
584
584
  if (maxTotal) {
585
- const noteFor = (n) => TIER_MARK_V1.degrade + ' 下探段超注入预算,已裁剪 ' + n + ' 行(目录层与降级标注不受影响)'
585
+ const noteFor = (n) => TIER_MARK_PRE_V1.degrade + ' 下探段超注入预算,已裁剪 ' + n + ' 行(目录层与降级标注不受影响)'
586
586
  const lines = drillParts.join('\n').split('\n').filter(Boolean)
587
587
  // 用"预留降级行长度"的保守估计先裁(降级行的字数随 trimmed 变化,先按最坏情况占位)。
588
588
  const reserve = noteFor(999).length
@@ -604,7 +604,7 @@ export function composeTieredInjectionPre(input = {}) {
604
604
  if (claimedTotalChars > maxTotal) {
605
605
  // 走投无路:目录层与降级行按 I7 不可裁 ⇒ **如实标注**,绝不假装达标。
606
606
  headOverBudget = claimedTotalChars - maxTotal
607
- const overNote = TIER_MARK_V1.degrade + ' 目录层与降级标注不可裁,本轮合计仍超注入预算 '
607
+ const overNote = TIER_MARK_PRE_V1.degrade + ' 目录层与降级标注不可裁,本轮合计仍超注入预算 '
608
608
  + headOverBudget + ' 字符(尽力门 + 显式超额,不静默)'
609
609
  headParts.push(overNote)
610
610
  claimedTotalChars = headParts.join('\n').length + (drillLen ? 1 + drillLen : 0)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tier-0 目录生成器(tier0_catalog_v1)—— 三层检索契约(`docs/internal/THREE-LAYER-CONTRACT.md`)C4。
2
+ * Tier-0 目录生成器(tier0_catalog_pre_v1)—— 三层检索契约(`docs/internal/THREE-LAYER-CONTRACT.md`)C4。
3
3
  *
4
4
  * 2026-09-14 建立。作用=**指引层**:把四类来源(用户级记忆 / 项目笔记 / 当日日志 / 白板 PLAN)
5
5
  * 压成"每条 1 行"的短目录,用来回答「要不要用某条记忆」,从而决定是否下探 Tier-1 / Tier-2。
@@ -26,7 +26,7 @@
26
26
  * 约 2-4 倍**(中文接近 1 token/字符),拿它当预算门会静默突破 B0。故本模块两种口径都实现:
27
27
  * - `estimateMode:'repo'` —— 原样复刻仓库既有口径(供对齐 / 复算);
28
28
  * - `estimateMode:'conservative'` —— `ceil(字符/2)`,**默认**用它当预算门(CJK 密集体裁的保守上界)。
29
- * 验收口径:**两种口径下 text 都 ≤ maxTokens**(见 `tests/smoke/smoke-test-tier0-catalog-pre.mjs`)。
29
+ * 验收口径:**两种口径下 text 都 ≤ maxTokens**(见 `tests/smoke/smoke-test-tier0-catalog.mjs`)。
30
30
  *
31
31
  * 单元("一条")切分:① 有 `<!-- memory:mem_<32hex> -->` 锚点 → 一块一条(与 l0-extract-pre 同约定:
32
32
  * 锚点标记其**后**内容);② 无锚点 → 按最浅的 ≥2 级标题切(`#` 文档标题成为独立一条);③ 无标题 →
@@ -44,12 +44,12 @@
44
44
  */
45
45
  import { readFileSync, statSync } from 'node:fs'
46
46
 
47
- export const TIER0_CATALOG_VERSION = 'tier0_catalog_v1'
47
+ export const TIER0_CATALOG_VERSION = 'tier0_catalog_pre_v1'
48
48
 
49
49
  /**
50
50
  * ★M1(2026-09-18):白板锚点 id 的**形态判据**(契约 §2)。
51
51
  *
52
- * 与 `wb-sidecar.js:466 WB_ANCHOR_LINE_RE_V1`、契约 §2 的
52
+ * 与 `wb-sidecar.js:466 WB_ANCHOR_LINE_RE_PRE_V1`、契约 §2 的
53
53
  * `^mem_[0-9a-f]{32}$` **同源同形**。此处**本地定义而非 import**:
54
54
  * - tier0-catalog 是本仓最底层的目录渲染模块,不应反向依赖 wb-sidecar(避免耦合/循环);
55
55
  * - 两处形态由**同一份契约文档**约束,套件断言两者**逐字相同**(防双源漂移)。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 白板格式适配器(wb_contract_v1)—— 白板线拥有「白板格式及其适配器」。
2
+ * 白板格式适配器(wb_contract_pre_v1)—— 白板线拥有「白板格式及其适配器」。
3
3
  *
4
4
  * 2026-09-14 建立(P0 最小适配边界)。**边界(总纲 §0.5 / ROUND3 §3.1 定案)**:
5
5
  * - **3.0 主体**拥有「共同提交与保护入口」= `lib/memory-mutation.js:validateMutationBoundaryPre`
@@ -27,15 +27,15 @@
27
27
  */
28
28
  import { createHash } from 'node:crypto'
29
29
  // 账本四段判据复用既有解析器(`handoff-anchor.js` 是账本段结构的既有权威,不另写一份)
30
- import { parseHandoffLedgerPre, HANDOFF_LEDGER_SECTION_WEIGHTS_V1 } from './handoff-anchor.js'
30
+ import { parseHandoffLedgerPre, HANDOFF_LEDGER_SECTION_WEIGHTS_PRE_V1 } from './handoff-anchor.js'
31
31
 
32
- export const WB_CONTRACT_VERSION = 'wb_contract_v1'
32
+ export const WB_CONTRACT_VERSION = 'wb_contract_pre_v1'
33
33
 
34
34
  /** 锚点正则(与 `l0-extract.js` 的 `MEM_ANCHOR_RE` 完全一致;契约 §2 要求"完全一致")。 */
35
- export const WB_ANCHOR_RE_V1 = /^<!--\s*memory:(mem_[0-9a-f]{32})\s*-->$/
35
+ export const WB_ANCHOR_RE_PRE_V1 = /^<!--\s*memory:(mem_[0-9a-f]{32})\s*-->$/
36
36
 
37
37
  /** 分区标记(契约 §5 逐字)。 */
38
- export const WB_MARKERS_V1 = Object.freeze({
38
+ export const WB_MARKERS_PRE_V1 = Object.freeze({
39
39
  modelOpen: '<!-- model -->',
40
40
  modelClose: '<!-- /model -->',
41
41
  userOpen: '<!-- user -->',
@@ -48,10 +48,10 @@ export const WB_MARKERS_V1 = Object.freeze({
48
48
  * (`current`/`superseded`/`retracted`), 属已发布插件 (@a9i5k4/dsh-auto-memory 3.0.0, 有真实用户)
49
49
  * 的公开导出面; 第三方按契约读取状态时需要它作为**单一真源**, 删掉等于把值域契约降级成散落字面量。
50
50
  * ⇒ 保留 + 显式标注, 不做"沉默留着"。 */
51
- export const WB_STATUSES_V1 = Object.freeze(['current', 'superseded', 'retracted'])
51
+ export const WB_STATUSES_PRE_V1 = Object.freeze(['current', 'superseded', 'retracted'])
52
52
 
53
53
  /** 解析/校验的原因码 → 可读中文。 */
54
- export const WB_REASONS_V1 = Object.freeze({
54
+ export const WB_REASONS_PRE_V1 = Object.freeze({
55
55
  'not-string': '内容不是字符串',
56
56
  'empty': '内容为空',
57
57
  'no-card': '页面里没有任何卡片标题(`### ` 行)',
@@ -66,17 +66,17 @@ export const WB_REASONS_V1 = Object.freeze({
66
66
 
67
67
  /** 原因码 → 可读中文(未知码原样返回,排障不吞信息)。
68
68
  * ⚠️ 2026-09-17 L7 存活标注: 本函数**当前无仓内调用者**(全仓引用计数 = 1, 仅此定义)。
69
- * 判定 = **对外契约保留**, 不删 —— 它是 `WB_REASONS_V1`(上文, **有消费者**) 的唯一
69
+ * 判定 = **对外契约保留**, 不删 —— 它是 `WB_REASONS_PRE_V1`(上文, **有消费者**) 的唯一
70
70
  * 取值入口; 删掉函数会让那份码表的"怎么读"离开公开面, 而码表本身是契约的一部分
71
71
  * (发布版已有真实用户)。⇒ 保留 + 显式标注。 */
72
72
  export function describeWbReasonPre(code) {
73
73
  const k = String(code == null ? '' : code)
74
- return WB_REASONS_V1[k] || k || '未知原因'
74
+ return WB_REASONS_PRE_V1[k] || k || '未知原因'
75
75
  }
76
76
 
77
- const isAnchor = (line) => WB_ANCHOR_RE_V1.test(String(line || '').replace(/\r$/, ''))
77
+ const isAnchor = (line) => WB_ANCHOR_RE_PRE_V1.test(String(line || '').replace(/\r$/, ''))
78
78
  const anchorIdOf = (line) => {
79
- const m = WB_ANCHOR_RE_V1.exec(String(line || '').replace(/\r$/, ''))
79
+ const m = WB_ANCHOR_RE_PRE_V1.exec(String(line || '').replace(/\r$/, ''))
80
80
  return m ? m[1] : ''
81
81
  }
82
82
  const isCardTitle = (line) => /^#{3,}\s+\S/.test(String(line || '').replace(/\r$/, ''))
@@ -153,7 +153,7 @@ export function parseWhiteboardPre(text, opts = {}) {
153
153
  const trimmed = line.replace(/\r$/, '')
154
154
 
155
155
  // ── 分区标记(先于一切判定:标记行本身既不是标题也不是内容) ──
156
- if (trimmed === WB_MARKERS_V1.userOpen) {
156
+ if (trimmed === WB_MARKERS_PRE_V1.userOpen) {
157
157
  if (region === 'user') issues.push({ code: 'user-region-interleaved', detail: '重复的 user 开始标记', line: i + 1 })
158
158
  if (region === 'model') closeRegion()
159
159
  if (!cur) issues.push({ code: 'user-region-unclosed', detail: 'user 段不在任何卡片内(契约 §5 要求每卡分区)', line: i + 1 })
@@ -162,19 +162,19 @@ export function parseWhiteboardPre(text, opts = {}) {
162
162
  regionLines = []
163
163
  continue
164
164
  }
165
- if (trimmed === WB_MARKERS_V1.userClose) {
165
+ if (trimmed === WB_MARKERS_PRE_V1.userClose) {
166
166
  if (region !== 'user') issues.push({ code: 'user-region-unclosed', detail: 'user 结束标记没有对应的开始标记', line: i + 1 })
167
167
  else closeRegion()
168
168
  continue
169
169
  }
170
- if (trimmed === WB_MARKERS_V1.modelOpen) {
170
+ if (trimmed === WB_MARKERS_PRE_V1.modelOpen) {
171
171
  if (region === 'user') issues.push({ code: 'user-region-interleaved', detail: 'user 段里又开了 model 段(契约 §5 禁止)', line: i + 1 })
172
172
  region = 'model'
173
173
  regionStart = i + 1
174
174
  regionLines = []
175
175
  continue
176
176
  }
177
- if (trimmed === WB_MARKERS_V1.modelClose) {
177
+ if (trimmed === WB_MARKERS_PRE_V1.modelClose) {
178
178
  if (region !== 'model') issues.push({ code: 'model-region-unclosed', detail: 'model 结束标记没有对应的开始标记', line: i + 1 })
179
179
  else closeRegion()
180
180
  continue
@@ -281,10 +281,10 @@ function emptyResult(kind, issues) {
281
281
  // ⑤ 矛盾检测 → 需 LLM,**不在本模块**(契约 §6:必须手动触发,不进自动路径)
282
282
  // ─────────────────────────────────────────────────────────────────────────────
283
283
 
284
- export const WB_LINT_VERSION = 'wb_lint_v1'
284
+ export const WB_LINT_VERSION = 'wb_lint_pre_v1'
285
285
 
286
286
  /** lint 问题码(§6 四类;⑤ 矛盾检测需 LLM,不在此列)。 */
287
- export const WB_LINT_CODES_V1 = Object.freeze({
287
+ export const WB_LINT_CODES_PRE_V1 = Object.freeze({
288
288
  ORPHAN: 'orphan', // ① 孤立条目
289
289
  INACTIVE: 'inactive', // ② 陈旧(已标 superseded/retracted)
290
290
  EXPIRED: 'expired', // ② 陈旧(仍标 current 但超阈值)
@@ -293,7 +293,7 @@ export const WB_LINT_CODES_V1 = Object.freeze({
293
293
  })
294
294
 
295
295
  /** 默认口径。 */
296
- export const WB_LINT_DEFAULTS_V1 = Object.freeze({
296
+ export const WB_LINT_DEFAULTS_PRE_V1 = Object.freeze({
297
297
  staleDays: 90, // ② 仍标 current 但日期早于 now-90d ⇒ 报 expired
298
298
  minSharedTags: 2, // ④ 共享 tag 数 ≥ 2 才算"应互链"
299
299
  })
@@ -322,8 +322,8 @@ export function lintWhiteboardPre(entries, opts = {}) {
322
322
  const o = opts && typeof opts === 'object' ? opts : {}
323
323
  const list = Array.isArray(entries) ? entries
324
324
  : (entries && Array.isArray(entries.entries) ? entries.entries : [])
325
- const staleDays = Number.isFinite(Number(o.staleDays)) ? Number(o.staleDays) : WB_LINT_DEFAULTS_V1.staleDays
326
- const minSharedTags = Number.isFinite(Number(o.minSharedTags)) ? Number(o.minSharedTags) : WB_LINT_DEFAULTS_V1.minSharedTags
325
+ const staleDays = Number.isFinite(Number(o.staleDays)) ? Number(o.staleDays) : WB_LINT_DEFAULTS_PRE_V1.staleDays
326
+ const minSharedTags = Number.isFinite(Number(o.minSharedTags)) ? Number(o.minSharedTags) : WB_LINT_DEFAULTS_PRE_V1.minSharedTags
327
327
 
328
328
  const nowMs0 = o.now instanceof Date ? o.now.getTime()
329
329
  : (typeof o.now === 'number' ? o.now : asDateMs(o.now))
@@ -382,19 +382,19 @@ export function lintWhiteboardPre(entries, opts = {}) {
382
382
  if (total - selfN > 0) referenced = true
383
383
  }
384
384
  }
385
- if (!referenced) push(WB_LINT_CODES_V1.ORPHAN, c, '无任何其他卡片或语料引用它的 id/标题')
385
+ if (!referenced) push(WB_LINT_CODES_PRE_V1.ORPHAN, c, '无任何其他卡片或语料引用它的 id/标题')
386
386
  }
387
387
 
388
388
  // ── ② 陈旧 ──
389
389
  const cutoff = nowMs - staleDays * 86400000
390
390
  for (const c of cards) {
391
391
  if (c.status !== 'current') {
392
- push(WB_LINT_CODES_V1.INACTIVE, c, 'status=' + c.status + '(已作废,仍在白板内)')
392
+ push(WB_LINT_CODES_PRE_V1.INACTIVE, c, 'status=' + c.status + '(已作废,仍在白板内)')
393
393
  continue
394
394
  }
395
395
  if (Number.isFinite(c.dateMs) && c.dateMs < cutoff) {
396
396
  const ageDays = Math.floor((nowMs - c.dateMs) / 86400000)
397
- push(WB_LINT_CODES_V1.EXPIRED, c, '仍标 current 但日期为 ' + c.dateRaw + '(' + ageDays + ' 天前,超阈值 ' + staleDays + ' 天)')
397
+ push(WB_LINT_CODES_PRE_V1.EXPIRED, c, '仍标 current 但日期为 ' + c.dateRaw + '(' + ageDays + ' 天前,超阈值 ' + staleDays + ' 天)')
398
398
  }
399
399
  }
400
400
 
@@ -406,7 +406,7 @@ export function lintWhiteboardPre(entries, opts = {}) {
406
406
  for (const id of allIds) {
407
407
  if (!knownIds.has(id)) {
408
408
  findings.push({
409
- code: WB_LINT_CODES_V1.DANGLING_REF, id, title: '',
409
+ code: WB_LINT_CODES_PRE_V1.DANGLING_REF, id, title: '',
410
410
  detail: '正文引用了 ' + id + ',但白板内没有该 id 的卡片(悬空引用)',
411
411
  })
412
412
  }
@@ -425,7 +425,7 @@ export function lintWhiteboardPre(entries, opts = {}) {
425
425
  (a.title && String((list.find((x) => x && x.id === b.id) || {}).body || '').includes(a.title))
426
426
  if (!aRefB && !bRefA) {
427
427
  findings.push({
428
- code: WB_LINT_CODES_V1.NO_CROSS_REF, id: a.id, title: a.title,
428
+ code: WB_LINT_CODES_PRE_V1.NO_CROSS_REF, id: a.id, title: a.title,
429
429
  detail: '与「' + (b.title || b.id) + '」共享 ' + shared.length + ' 个 tag(' + shared.join(', ') + ')但互不引用',
430
430
  })
431
431
  }
@@ -503,25 +503,25 @@ export function toMutationProjectionPre(projection) {
503
503
  // `memory-mutation.js` 里,无条件生效**,任何开关都绕不过。
504
504
  // ═══════════════════════════════════════════════════════════════════════════
505
505
 
506
- export const CRITERIA_GATE_VERSION = 'wb_criteria_v1'
506
+ export const CRITERIA_GATE_VERSION = 'wb_criteria_pre_v1'
507
507
 
508
508
  /** 账本四段标题(逐字匹配;与 `handoff-anchor.js` 的权重表同源,不另写一份)。 */
509
- export const HANDOFF_REQUIRED_SECTIONS_V1 = Object.freeze(
510
- HANDOFF_LEDGER_SECTION_WEIGHTS_V1.map((x) => x.title),
509
+ export const HANDOFF_REQUIRED_SECTIONS_PRE_V1 = Object.freeze(
510
+ HANDOFF_LEDGER_SECTION_WEIGHTS_PRE_V1.map((x) => x.title),
511
511
  )
512
512
 
513
513
  /** 占位符黑名单(H3):段 body 只含这些即视为未填写。 */
514
- export const CRITERIA_PLACEHOLDERS_V1 = Object.freeze(['(待补充)', '(待补充)', 'todo', '同上', '略', 'n/a', '待补充'])
514
+ export const CRITERIA_PLACEHOLDERS_PRE_V1 = Object.freeze(['(待补充)', '(待补充)', 'todo', '同上', '略', 'n/a', '待补充'])
515
515
 
516
516
  /** 账本/白板判据的阈值(与 `sanitizeForWrite` 上限同源,先于它执行以免双重截断语义混乱)。 */
517
- export const CRITERIA_LIMITS_V1 = Object.freeze({
517
+ export const CRITERIA_LIMITS_PRE_V1 = Object.freeze({
518
518
  handoffMaxChars: 8000,
519
519
  planMaxChars: 200000,
520
520
  sectionMinChars: 20,
521
521
  sectionMaxLines: 5,
522
522
  })
523
523
 
524
- const PLACEHOLDER_SET = new Set(CRITERIA_PLACEHOLDERS_V1.map((x) => x.toLowerCase()))
524
+ const PLACEHOLDER_SET = new Set(CRITERIA_PLACEHOLDERS_PRE_V1.map((x) => x.toLowerCase()))
525
525
 
526
526
  /** 段 body 的"实质内容"行(去空行、去纯占位行)。 */
527
527
  function bodyRealLines(body) {
@@ -545,7 +545,7 @@ export function checkHandoffCriteriaPre(text) {
545
545
 
546
546
  // H1 四段标题齐全且逐字匹配
547
547
  const gotTitles = parsed ? parsed.sections.map((s) => String(s.title).replace(/\r$/, '').replace(/^##\s+/, '').trim()) : []
548
- const missing = HANDOFF_REQUIRED_SECTIONS_V1.filter((t) => !gotTitles.includes(t))
548
+ const missing = HANDOFF_REQUIRED_SECTIONS_PRE_V1.filter((t) => !gotTitles.includes(t))
549
549
  hard.push({
550
550
  id: 'H1', pass: missing.length === 0, missing: missing.slice(),
551
551
  detail: missing.length
@@ -558,10 +558,10 @@ export function checkHandoffCriteriaPre(text) {
558
558
  if (parsed) {
559
559
  for (const s of parsed.sections) {
560
560
  const name = String(s.title).replace(/\r$/, '').replace(/^##\s+/, '').trim()
561
- if (!HANDOFF_REQUIRED_SECTIONS_V1.includes(name)) continue
561
+ if (!HANDOFF_REQUIRED_SECTIONS_PRE_V1.includes(name)) continue
562
562
  const real = bodyRealLines(s.body)
563
563
  const chars = real.join('').length
564
- if (!real.length || chars < CRITERIA_LIMITS_V1.sectionMinChars) thinSections.push(name + '(' + chars + ' 字符)')
564
+ if (!real.length || chars < CRITERIA_LIMITS_PRE_V1.sectionMinChars) thinSections.push(name + '(' + chars + ' 字符)')
565
565
  }
566
566
  } else {
567
567
  thinSections.push('(无法解析出任何段)')
@@ -569,7 +569,7 @@ export function checkHandoffCriteriaPre(text) {
569
569
  hard.push({
570
570
  id: 'H2', pass: thinSections.length === 0, missing: thinSections.slice(),
571
571
  detail: thinSections.length
572
- ? '这些段为空或少于 ' + CRITERIA_LIMITS_V1.sectionMinChars + ' 字符:' + thinSections.join('、')
572
+ ? '这些段为空或少于 ' + CRITERIA_LIMITS_PRE_V1.sectionMinChars + ' 字符:' + thinSections.join('、')
573
573
  : '四段 body 均非空且达到最小长度',
574
574
  })
575
575
 
@@ -586,14 +586,14 @@ export function checkHandoffCriteriaPre(text) {
586
586
  hard.push({
587
587
  id: 'H3', pass: placeholderHits.length === 0, missing: placeholderHits.slice(),
588
588
  detail: placeholderHits.length
589
- ? '这些段的正文只有占位符(' + CRITERIA_PLACEHOLDERS_V1.slice(0, 4).join('/') + ' 等):' + placeholderHits.join('、')
589
+ ? '这些段的正文只有占位符(' + CRITERIA_PLACEHOLDERS_PRE_V1.slice(0, 4).join('/') + ' 等):' + placeholderHits.join('、')
590
590
  : '无纯占位符段',
591
591
  })
592
592
 
593
593
  // H4 总长 ≤ 8000
594
594
  hard.push({
595
- id: 'H4', pass: src.length <= CRITERIA_LIMITS_V1.handoffMaxChars, missing: [],
596
- detail: '总长 ' + src.length + ' 字符(上限 ' + CRITERIA_LIMITS_V1.handoffMaxChars + ')',
595
+ id: 'H4', pass: src.length <= CRITERIA_LIMITS_PRE_V1.handoffMaxChars, missing: [],
596
+ detail: '总长 ' + src.length + ' 字符(上限 ' + CRITERIA_LIMITS_PRE_V1.handoffMaxChars + ')',
597
597
  })
598
598
 
599
599
  // S1 每段 ≤5 行(软·警告;prompt 纪律既有约定)
@@ -601,9 +601,9 @@ export function checkHandoffCriteriaPre(text) {
601
601
  if (parsed) {
602
602
  for (const s of parsed.sections) {
603
603
  const name = String(s.title).replace(/\r$/, '').replace(/^##\s+/, '').trim()
604
- if (!HANDOFF_REQUIRED_SECTIONS_V1.includes(name)) continue
604
+ if (!HANDOFF_REQUIRED_SECTIONS_PRE_V1.includes(name)) continue
605
605
  const n = bodyRealLines(s.body).length
606
- if (n > CRITERIA_LIMITS_V1.sectionMaxLines) longSections.push(name + '(' + n + ' 行)')
606
+ if (n > CRITERIA_LIMITS_PRE_V1.sectionMaxLines) longSections.push(name + '(' + n + ' 行)')
607
607
  }
608
608
  }
609
609
  soft.push({ id: 'S1', pass: longSections.length === 0, detail: longSections.length ? '超过 5 行的段:' + longSections.join('、') : '各段均 ≤5 行' })
@@ -631,7 +631,7 @@ export function checkHandoffCriteriaPre(text) {
631
631
  return {
632
632
  ok: hard.every((h) => h.pass), version: CRITERIA_GATE_VERSION, target: 'handoff',
633
633
  hardPass: hard.every((h) => h.pass), hard, soft,
634
- report: { chars: src.length, sections: gotTitles.length, requiredSections: HANDOFF_REQUIRED_SECTIONS_V1.length },
634
+ report: { chars: src.length, sections: gotTitles.length, requiredSections: HANDOFF_REQUIRED_SECTIONS_PRE_V1.length },
635
635
  }
636
636
  }
637
637
 
@@ -652,17 +652,17 @@ export function checkPlanCriteriaPre(text, projection) {
652
652
  const sections = src.split(/(?=^## )/m).filter((s) => /^## /.test(s))
653
653
  const nonEmpty = sections.filter((s) => {
654
654
  const body = s.split('\n').slice(1).join('\n').trim()
655
- return body.length >= CRITERIA_LIMITS_V1.sectionMinChars
655
+ return body.length >= CRITERIA_LIMITS_PRE_V1.sectionMinChars
656
656
  })
657
657
  hard.push({
658
658
  id: 'P-H1', pass: nonEmpty.length >= 1, missing: nonEmpty.length ? [] : ['(无非空 ## 顶层节)'],
659
659
  detail: nonEmpty.length
660
660
  ? '有 ' + nonEmpty.length + ' 个非空顶层节(共 ' + sections.length + ' 个)'
661
- : '没有任何非空 `## ` 顶层节(' + CRITERIA_LIMITS_V1.sectionMinChars + ' 字符以上)—— 这是"白板被整篇覆盖成骨架/空白板"的形态',
661
+ : '没有任何非空 `## ` 顶层节(' + CRITERIA_LIMITS_PRE_V1.sectionMinChars + ' 字符以上)—— 这是"白板被整篇覆盖成骨架/空白板"的形态',
662
662
  })
663
663
  hard.push({
664
- id: 'P-H2', pass: src.length <= CRITERIA_LIMITS_V1.planMaxChars, missing: [],
665
- detail: '总长 ' + src.length + ' 字符(上限 ' + CRITERIA_LIMITS_V1.planMaxChars + ')',
664
+ id: 'P-H2', pass: src.length <= CRITERIA_LIMITS_PRE_V1.planMaxChars, missing: [],
665
+ detail: '总长 ' + src.length + ' 字符(上限 ' + CRITERIA_LIMITS_PRE_V1.planMaxChars + ')',
666
666
  })
667
667
  const forward = /下一步|待办|计划|todo/i.test(src)
668
668
  soft.push({ id: 'P-S1', pass: forward, detail: forward ? '含前瞻内容' : '未见前瞻内容(下一步/待办/计划)—— 软提示,不拦截' })
package/lib/wb-sidecar.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * wb-sidecar-pre —— WB-GRAPH P2 结构化 sidecar(2026-09-16, wb_sidecar_v1)。
2
+ * wb-sidecar-pre —— WB-GRAPH P2 结构化 sidecar(2026-09-16, wb_sidecar_pre_v1)。
3
3
  *
4
4
  * 权威依据: WB-GRAPH-INTEGRATION-PLAN.md §5 P2-1/P2-2 + WB-FORMAT-CONVENTION §3(索引派生)/§4(写入门)
5
5
  * + 拍板 A2: sidecar 放 memoryRoot/<ws>/handoff/ 旁挂。
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { createHash } from 'node:crypto'
20
20
 
21
- export const WB_SIDECAR_VERSION = 'wb_sidecar_v1'
21
+ export const WB_SIDECAR_VERSION = 'wb_sidecar_pre_v1'
22
22
 
23
23
  /** 章节标题 → tag(截断 24 字, 去掉 markdown 井号前缀)。 */
24
24
  function sectionTagPre(sectionTitle) {
@@ -196,7 +196,7 @@ export function splitSectionsPre(text) {
196
196
  // 引言区里的 `# 文档标题` 行不是内容 —— 若不剔除, 它会变成一张只有标题的垃圾卡
197
197
  if (cur.preamble && /^#\s+/.test(line)) continue
198
198
  // 标题行紧随的锚点行进 anchorId(§2 契约形态)
199
- const am = line.trim().match(WB_ANCHOR_LINE_RE_V1)
199
+ const am = line.trim().match(WB_ANCHOR_LINE_RE_PRE_V1)
200
200
  if (am && !cur.bodyLines.some((x) => x.trim())) { cur.anchorId = am[1]; continue }
201
201
  cur.bodyLines.push(line)
202
202
  }
@@ -514,7 +514,7 @@ export function criteriaWeightPre(criteria) {
514
514
  }
515
515
 
516
516
  /** 锚点行正则(与 L0 抽取的 MEM_ANCHOR_RE 同形)。 */
517
- export const WB_ANCHOR_LINE_RE_V1 = /^<!--\s*memory:(mem_[0-9a-f]{32})\s*-->$/
517
+ export const WB_ANCHOR_LINE_RE_PRE_V1 = /^<!--\s*memory:(mem_[0-9a-f]{32})\s*-->$/
518
518
 
519
519
  /**
520
520
  * WB-FORMAT-CONVENTION §2 锚点写入(纯函数, 零 IO)。
@@ -552,7 +552,7 @@ export function applyAnchorsPre(workspaceKey, relPath, text) {
552
552
  const want = wbEntryIdPre(workspaceKey, rel, cardTitle)
553
553
  const anchor = '<!-- memory:' + want + ' -->'
554
554
  const next = lines[i + 1] == null ? '' : lines[i + 1]
555
- const have = next.trim().match(WB_ANCHOR_LINE_RE_V1)
555
+ const have = next.trim().match(WB_ANCHOR_LINE_RE_PRE_V1)
556
556
  if (have) {
557
557
  // 标题未变 ⇒ **原样保留既有锚点行**(不重发、不删除; 否则二次应用会丢失锚点, 破坏幂等)
558
558
  if (have[1] === want) { unchanged++; out.push(next) } else { updated++; out.push(anchor) }
@@ -569,7 +569,7 @@ export function applyAnchorsPre(workspaceKey, relPath, text) {
569
569
  export function collectAnchorIdsPre(text) {
570
570
  const ids = []
571
571
  for (const line of String(text || '').split('\n')) {
572
- const m = line.trim().match(WB_ANCHOR_LINE_RE_V1)
572
+ const m = line.trim().match(WB_ANCHOR_LINE_RE_PRE_V1)
573
573
  if (m) ids.push(m[1])
574
574
  }
575
575
  return ids
@@ -590,7 +590,7 @@ export function collectAnchorIdsPre(text) {
590
590
  /** 泳道定义(顺序即渲染顺序)。match 按 kind → tag → 小节名 → 小节标题正则, 先命中先归。
591
591
  * v2(2026-09-16): 新增 matchTitle 正则 —— 小节级卡片没有 tag(白板正文里几乎没有语义 tag),
592
592
  * 只能靠**小节标题**分列; 实测真实数据的标题形如「任务状态」「已试方案与失败原因」「关键坑(血泪)」。 */
593
- export const WB_KANBAN_LANES_V1 = [
593
+ export const WB_KANBAN_LANES_PRE_V1 = [
594
594
  { key: 'goal', label: '目标', matchTags: ['type:goal'], matchSections: ['目标'], matchTitle: /^(?:总体)?目标|这是什么|定位|背景/ },
595
595
  { key: 'state', label: '进行中', matchTags: ['type:state'], matchSections: ['任务状态'], matchTitle: /任务状态|现状|进行中|在做/ },
596
596
  { key: 'deadend', label: '失败与弯路', matchTags: ['type:dead-end'], matchSections: ['已试方案与失败原因', '失败'], matchTitle: /失败|弯路|坑|教训|血泪|报错|阻塞/ },
@@ -612,7 +612,7 @@ export function laneOfEntryPre(entry) {
612
612
  const tags = Array.isArray(e.tags) ? e.tags : []
613
613
  const sec = String(e.section || e.title || '')
614
614
  const kind = String(e.kind || '')
615
- for (const lane of WB_KANBAN_LANES_V1) {
615
+ for (const lane of WB_KANBAN_LANES_PRE_V1) {
616
616
  if (Array.isArray(lane.matchKinds) && lane.matchKinds.includes(kind)) return lane.key
617
617
  if (Array.isArray(lane.matchTags) && lane.matchTags.some((t) => tags.includes(t))) return lane.key
618
618
  if (Array.isArray(lane.matchSections) && sec) {
@@ -651,7 +651,7 @@ export function buildKanbanPre(index, opts = {}) {
651
651
  const sectionCards = Array.isArray(opts.cards) && opts.cards.length ? opts.cards : null
652
652
  const perLaneCap = Number(opts.perLaneCap) > 0 ? Number(opts.perLaneCap) : 0
653
653
 
654
- const lanes = WB_KANBAN_LANES_V1.map((l) => ({ key: l.key, label: l.label, cards: [] }))
654
+ const lanes = WB_KANBAN_LANES_PRE_V1.map((l) => ({ key: l.key, label: l.label, cards: [] }))
655
655
  const misc = { key: 'misc', label: '其它', cards: [] }
656
656
 
657
657
  if (sectionCards) {
@@ -809,9 +809,9 @@ export function buildKanbanMatrixPre(cards, opts = {}) {
809
809
  const list = Array.isArray(cards) ? cards : []
810
810
  const colKeys = Array.isArray(opts.colKeys) && opts.colKeys.length
811
811
  ? opts.colKeys
812
- : WB_KANBAN_LANES_V1.map((l) => l.key).concat(['misc'])
812
+ : WB_KANBAN_LANES_PRE_V1.map((l) => l.key).concat(['misc'])
813
813
  const columns = colKeys.map((k) => {
814
- const lane = WB_KANBAN_LANES_V1.find((l) => l.key === k)
814
+ const lane = WB_KANBAN_LANES_PRE_V1.find((l) => l.key === k)
815
815
  return { key: k, label: lane ? lane.label : (k === 'misc' ? '其它' : k), count: 0 }
816
816
  })
817
817
  const colOf = {}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-auto-memory",
3
3
  "description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
4
- "version": "3.1.5",
4
+ "version": "3.1.7",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -17,9 +17,8 @@
17
17
  "!python/bench",
18
18
  "!python/__pycache__",
19
19
  "!docs/internal",
20
- "!docs/**/*.bak",
21
- "!docs/**/*.bak-*",
22
- "!**/*.mjs.bak-*"
20
+ "!**/*.bak*",
21
+ "!lib/*.m8b*bak*"
23
22
  ],
24
23
  "dsh": {
25
24
  "bundle": {