@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.
- package/docs/CONTINUITY-FLOW.md +6 -6
- package/docs/HANDBOOK.md +54 -53
- package/docs/M-CM-PLAN.md +1 -1
- package/docs/M-CM-STATE.md +1 -1
- package/docs/M-CM7-HANDOFF-LAYERED-RETRIEVAL.md +1 -1
- package/docs/M3B-CONTRACT.md +7 -7
- package/docs/M7-AUTONOMOUS-STATE.md +1 -1
- package/docs/M7-TASKSET-DISPATCH.md +1 -1
- package/docs/PROMPT-PACK-LAYERED-RECALL.md +3 -3
- package/docs/PROMPT-SET-STRICT.md +3 -3
- package/docs/RELEASE-GO-NOGO.md +1 -1
- package/docs/STATUS-BOARD.md +1 -1
- package/docs/USER-GUIDE.en.md +21 -21
- package/docs/USER-GUIDE.zh-CN.md +21 -21
- package/docs/WHITEPAPER.md +1 -1
- package/docs/prompts/FIX-AGENT-P12-FULL-REGRESSION.md +1 -1
- package/docs/prompts/LIVE-VERIFY-ZCODE.md +2 -2
- package/docs/prompts/P2-semantic-recall.md +2 -2
- package/docs/prompts/P7-write-fix.md +1 -1
- package/docs/prompts/RELEASE-AGENT.md +4 -4
- package/docs/prompts/ZCODE-DROPIN.md +1 -1
- package/lib/acceptance.js +6 -6
- package/lib/activation-host.js +15 -14
- package/lib/activation-inbox-state.js +4 -4
- package/lib/activation-inbox.js +44 -44
- package/lib/board-mode.js +3 -3
- package/lib/client.js +225 -41
- package/lib/config-io.js +12 -12
- package/lib/context-bridge.js +22 -22
- package/lib/context-host.js +24 -23
- package/lib/datadir.js +95 -0
- package/lib/degrade.js +21 -21
- package/lib/dsh-home.js +8 -8
- package/lib/engine-identity.js +12 -12
- package/lib/engine-switch.js +3 -3
- package/lib/episodic-store.js +13 -13
- package/lib/evidence-agg.js +4 -4
- package/lib/evidence-store.js +12 -12
- package/lib/fact-store.js +47 -47
- package/lib/handoff-anchor.js +4 -4
- package/lib/hub-io.js +356 -3
- package/lib/index-sync.js +5 -5
- package/lib/index.js +426 -242
- package/lib/intent-clean-safe.js +3 -3
- package/lib/intent-clean.js +1 -1
- package/lib/l0-extract.js +15 -15
- package/lib/l0-index-sync.js +8 -8
- package/lib/l0-index.js +11 -11
- package/lib/ledger-criteria.js +8 -8
- package/lib/m4-corpus.js +1 -1
- package/lib/m7-index-sync-host.js +2 -2
- package/lib/m7-wire.js +24 -24
- package/lib/memory-anchor.js +1 -1
- package/lib/memory-envelope.js +15 -15
- package/lib/memory-hub.js +95 -50
- package/lib/memory-importance.js +7 -7
- package/lib/memory-mutation.js +5 -5
- package/lib/note-status-apply.js +2 -2
- package/lib/note-status.js +7 -7
- package/lib/policies/activation_policy_v2.json +2 -2
- package/lib/policies/recall_intent_lr_v1.json +1 -1
- package/lib/procedure-observation.js +2 -2
- package/lib/procedure-store.js +117 -28
- package/lib/python-setup.js +6 -5
- package/lib/python-sidecar-client.js +26 -26
- package/lib/recall-fusion.js +12 -12
- package/lib/recall-stats.js +36 -1
- package/lib/rerank-host.js +11 -11
- package/lib/rules-edit.js +9 -9
- package/lib/rules-layer.js +26 -26
- package/lib/semantic-decide.js +7 -7
- package/lib/semantic-js.js +13 -13
- package/lib/shadow-host.js +7 -6
- package/lib/shadow-retrieval.js +40 -40
- package/lib/skill-export-host.js +7 -7
- package/lib/skill-export.js +6 -6
- package/lib/state-commit.js +10 -10
- package/lib/storage-manage.js +6 -6
- package/lib/tier-layer-inject.js +28 -28
- package/lib/tier0-catalog.js +4 -4
- package/lib/wb-contract.js +45 -45
- package/lib/wb-sidecar.js +11 -11
- package/package.json +3 -4
- package/python/m7_activation_features_v2.py +7 -7
- package/python/m7_embedding_v1.py +12 -12
- package/python/policies/activation_policy_v2.json +2 -2
- package/python/policies/decision-record-activation-v2-delta-exp-override-20260824.json +1 -1
- package/python/policies/decision-record-reasoning-kind-admission-20260826.json +2 -2
- package/python/policies/decision-record-stale-gate-per-candidate-20260825.json +2 -2
- package/python/policies/recall_intent_lr_v1.json +1 -1
- package/python/verify_policy_artifact.py +3 -3
- package/python/worker_semantic_v1.py +18 -18
- package/python/worker_v1.py +18 -18
package/lib/tier-layer-inject.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 三层注入装配(
|
|
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 = '
|
|
36
|
+
export const TIER_INJECT_VERSION = 'tier_layer_inject_pre_v1'
|
|
37
37
|
|
|
38
38
|
/** 契约预算(冻结;变更须同步 `THREE-LAYER-CONTRACT.md` §4.3)。 */
|
|
39
|
-
export const
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
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
|
|
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 =
|
|
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) ||
|
|
428
|
-
const L1 = Math.max(8, Number((opts && opts.itemChars) ||
|
|
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 ? [
|
|
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) ||
|
|
458
|
-
const maxBlocks = Math.max(1, Number((opts && opts.maxBlocks) ||
|
|
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),
|
|
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:
|
|
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:
|
|
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:
|
|
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=' +
|
|
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(
|
|
561
|
+
headParts.push(TIER_MARK_PRE_V1.gate + ' 本轮无语义命中(' + gate.reasons.join(',') + ')→ 仅目录层,未下探 Tier-1/Tier-2')
|
|
562
562
|
} else {
|
|
563
|
-
headParts.push(
|
|
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(
|
|
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) =>
|
|
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 =
|
|
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)
|
package/lib/tier0-catalog.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tier-0 目录生成器(
|
|
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
|
|
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 = '
|
|
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
|
|
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
|
* - 两处形态由**同一份契约文档**约束,套件断言两者**逐字相同**(防双源漂移)。
|
package/lib/wb-contract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 白板格式适配器(
|
|
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,
|
|
30
|
+
import { parseHandoffLedgerPre, HANDOFF_LEDGER_SECTION_WEIGHTS_PRE_V1 } from './handoff-anchor.js'
|
|
31
31
|
|
|
32
|
-
export const WB_CONTRACT_VERSION = '
|
|
32
|
+
export const WB_CONTRACT_VERSION = 'wb_contract_pre_v1'
|
|
33
33
|
|
|
34
34
|
/** 锚点正则(与 `l0-extract.js` 的 `MEM_ANCHOR_RE` 完全一致;契约 §2 要求"完全一致")。 */
|
|
35
|
-
export const
|
|
35
|
+
export const WB_ANCHOR_RE_PRE_V1 = /^<!--\s*memory:(mem_[0-9a-f]{32})\s*-->$/
|
|
36
36
|
|
|
37
37
|
/** 分区标记(契约 §5 逐字)。 */
|
|
38
|
-
export const
|
|
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
|
|
51
|
+
export const WB_STATUSES_PRE_V1 = Object.freeze(['current', 'superseded', 'retracted'])
|
|
52
52
|
|
|
53
53
|
/** 解析/校验的原因码 → 可读中文。 */
|
|
54
|
-
export const
|
|
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
|
-
* 判定 = **对外契约保留**, 不删 —— 它是 `
|
|
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
|
|
74
|
+
return WB_REASONS_PRE_V1[k] || k || '未知原因'
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const isAnchor = (line) =>
|
|
77
|
+
const isAnchor = (line) => WB_ANCHOR_RE_PRE_V1.test(String(line || '').replace(/\r$/, ''))
|
|
78
78
|
const anchorIdOf = (line) => {
|
|
79
|
-
const m =
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 = '
|
|
284
|
+
export const WB_LINT_VERSION = 'wb_lint_pre_v1'
|
|
285
285
|
|
|
286
286
|
/** lint 问题码(§6 四类;⑤ 矛盾检测需 LLM,不在此列)。 */
|
|
287
|
-
export const
|
|
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
|
|
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) :
|
|
326
|
-
const minSharedTags = Number.isFinite(Number(o.minSharedTags)) ? Number(o.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(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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 = '
|
|
506
|
+
export const CRITERIA_GATE_VERSION = 'wb_criteria_pre_v1'
|
|
507
507
|
|
|
508
508
|
/** 账本四段标题(逐字匹配;与 `handoff-anchor.js` 的权重表同源,不另写一份)。 */
|
|
509
|
-
export const
|
|
510
|
-
|
|
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
|
|
514
|
+
export const CRITERIA_PLACEHOLDERS_PRE_V1 = Object.freeze(['(待补充)', '(待补充)', 'todo', '同上', '略', 'n/a', '待补充'])
|
|
515
515
|
|
|
516
516
|
/** 账本/白板判据的阈值(与 `sanitizeForWrite` 上限同源,先于它执行以免双重截断语义混乱)。 */
|
|
517
|
-
export const
|
|
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(
|
|
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 =
|
|
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 (!
|
|
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 <
|
|
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
|
-
? '这些段为空或少于 ' +
|
|
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
|
-
? '这些段的正文只有占位符(' +
|
|
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 <=
|
|
596
|
-
detail: '总长 ' + src.length + ' 字符(上限 ' +
|
|
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 (!
|
|
604
|
+
if (!HANDOFF_REQUIRED_SECTIONS_PRE_V1.includes(name)) continue
|
|
605
605
|
const n = bodyRealLines(s.body).length
|
|
606
|
-
if (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:
|
|
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 >=
|
|
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
|
-
: '没有任何非空 `## ` 顶层节(' +
|
|
661
|
+
: '没有任何非空 `## ` 顶层节(' + CRITERIA_LIMITS_PRE_V1.sectionMinChars + ' 字符以上)—— 这是"白板被整篇覆盖成骨架/空白板"的形态',
|
|
662
662
|
})
|
|
663
663
|
hard.push({
|
|
664
|
-
id: 'P-H2', pass: src.length <=
|
|
665
|
-
detail: '总长 ' + src.length + ' 字符(上限 ' +
|
|
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,
|
|
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 = '
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
:
|
|
812
|
+
: WB_KANBAN_LANES_PRE_V1.map((l) => l.key).concat(['misc'])
|
|
813
813
|
const columns = colKeys.map((k) => {
|
|
814
|
-
const lane =
|
|
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.
|
|
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
|
-
"
|
|
21
|
-
"!
|
|
22
|
-
"!**/*.mjs.bak-*"
|
|
20
|
+
"!**/*.bak*",
|
|
21
|
+
"!lib/*.m8b*bak*"
|
|
23
22
|
],
|
|
24
23
|
"dsh": {
|
|
25
24
|
"bundle": {
|