@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/engine-identity.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 引擎身份(
|
|
2
|
+
* 引擎身份(engine_identity_pre_v1)—— P2「引擎隔离」的地基(V2-P2 卡 / 评审 §3.5)。
|
|
3
3
|
*
|
|
4
4
|
* 为什么需要它(权威依据):
|
|
5
5
|
* V2-P2 卡明确指出:单个 `PROVIDER_ID_INT8`(`python-setup.js:55`)**不足以**标识
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 模型与权重摘要 · tokenizer 版本 · 精度格式 · 维度 · 池化 · 归一化 · 输入处理版本。
|
|
10
10
|
*
|
|
11
11
|
* 本模块只做三件事(纯函数、零 IO、零依赖,便于宿主与测试共用):
|
|
12
|
-
* 1) `computeEngineIdentityPre(desc)` —— 宽身份描述 → 稳定身份串 `
|
|
12
|
+
* 1) `computeEngineIdentityPre(desc)` —— 宽身份描述 → 稳定身份串 `engid_pre_<32hex>`
|
|
13
13
|
* (canonical 排序 JSON + sha256,同输入逐字节同输出;任一字段变化 → 身份必变)。
|
|
14
14
|
* 2) `aliasKeyPre(engineIdentity, chunkId)` —— 两级引用的**第一级**:
|
|
15
15
|
* `engineIdentity + chunkId → alias`。记录变化导致未改块重新编号(chunkId 变)时,
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { createHash } from 'node:crypto'
|
|
27
27
|
|
|
28
|
-
export const ENGINE_IDENTITY_VERSION = '
|
|
28
|
+
export const ENGINE_IDENTITY_VERSION = 'engine_identity_pre_v1'
|
|
29
29
|
|
|
30
|
-
/** 身份串前缀:有意与 corpus 的 `
|
|
31
|
-
export const ENGINE_IDENTITY_PREFIX = '
|
|
30
|
+
/** 身份串前缀:有意与 corpus 的 `idx_pre_` / L0 的 `l0idx_pre_` 区分,避免三套版本语义混淆。 */
|
|
31
|
+
export const ENGINE_IDENTITY_PREFIX = 'engid_pre_'
|
|
32
32
|
|
|
33
33
|
/** 身份必须覆盖的字段(缺一即身份不完整;缺失字段按空串参与哈希,不给"省略=通过"的口子)。 */
|
|
34
34
|
export const ENGINE_IDENTITY_FIELDS = Object.freeze([
|
|
35
|
-
'provider', // 通道/提供方标识(如 bge-m3-onnx-int8-v1 / js-e5-small-q8-v1)
|
|
35
|
+
'provider', // 通道/提供方标识(如 bge-m3-onnx-int8-pre-v1 / js-e5-small-q8-pre-v1)
|
|
36
36
|
'model', // 模型名(如 bge-m3 / multilingual-e5-small)
|
|
37
37
|
'weightsDigest', // 权重摘要(sha256 或廉价指纹;见 weightsDigestOfFilePre 的口径说明)
|
|
38
38
|
'tokenizer', // tokenizer 版本/来源(如 xenova-bge-m3-fast / e5-small-tokenizer-v1)
|
|
@@ -63,7 +63,7 @@ export function canonicalEngineDescPre(desc) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* 宽身份计算:canonical 描述 → `
|
|
66
|
+
* 宽身份计算:canonical 描述 → `engid_pre_<first32hex(sha256)>`。
|
|
67
67
|
* 任一字段(含维度、精度、tokenizer、输入版本)变化 → 身份必变;同输入确定性。
|
|
68
68
|
*/
|
|
69
69
|
export function computeEngineIdentityPre(desc) {
|
|
@@ -73,7 +73,7 @@ export function computeEngineIdentityPre(desc) {
|
|
|
73
73
|
|
|
74
74
|
/** 身份合法性:必须是本模块产出的前缀形态(防把任意字符串当身份塞进缓存)。 */
|
|
75
75
|
export function isEngineIdentityPre(x) {
|
|
76
|
-
return typeof x === 'string' && /^
|
|
76
|
+
return typeof x === 'string' && /^engid_pre_[0-9a-f]{32}$/.test(x)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** 两个身份是否同一引擎。非法的身份一律视为不匹配(fail closed:宁可重建,不冒串用风险)。 */
|
|
@@ -115,10 +115,10 @@ export function weightsDigestOfFilePre(stat, name) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/** 端侧 JS 引擎(e5-small q8,384 维已归一化)的规范身份描述。 */
|
|
118
|
-
export const
|
|
119
|
-
provider: 'js-e5-small-q8-v1',
|
|
118
|
+
export const JS_E5_IDENTITY_DESC_PRE_V1 = Object.freeze({
|
|
119
|
+
provider: 'js-e5-small-q8-pre-v1',
|
|
120
120
|
model: 'multilingual-e5-small',
|
|
121
|
-
weightsDigest: 'manifest-e5-small-q8-v1', // 端侧资产清单版本;模型文件摘要由下载清单锁定
|
|
121
|
+
weightsDigest: 'manifest-e5-small-q8-pre-v1', // 端侧资产清单版本;模型文件摘要由下载清单锁定
|
|
122
122
|
tokenizer: 'e5-small-tokenizer-v1',
|
|
123
123
|
precision: 'q8',
|
|
124
124
|
dim: 384,
|
|
@@ -130,7 +130,7 @@ export const JS_E5_IDENTITY_DESC_V1 = Object.freeze({
|
|
|
130
130
|
/** Python 引擎(bge-m3 onnx int8,1024 维)的身份描述构造器(weightsDigest 由调用方给指纹)。 */
|
|
131
131
|
export function pyBgeM3IdentityDescPre(weightsDigest) {
|
|
132
132
|
return {
|
|
133
|
-
provider: 'bge-m3-onnx-int8-v1',
|
|
133
|
+
provider: 'bge-m3-onnx-int8-pre-v1',
|
|
134
134
|
model: 'bge-m3',
|
|
135
135
|
weightsDigest: weightsDigest || '',
|
|
136
136
|
tokenizer: 'xenova-bge-m3-fast',
|
package/lib/engine-switch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 引擎切换状态机(
|
|
2
|
+
* 引擎切换状态机(engine_switch_pre_v1)—— P2 / T2-9 切档隔离与进度条(V2-P2 卡 points 4/5)。
|
|
3
3
|
*
|
|
4
4
|
* 权威依据(评审 §3.5 采纳条目 + V2-P2 卡):
|
|
5
5
|
* · **切档 = 强制全量重建 + 进度条**(用户裁定):每次**实际切换**创建**新 rebuild generation**,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* 零第三方依赖;IO 全注入(不传则纯内存);UTF-8 无 BOM。
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
export const ENGINE_SWITCH_VERSION = '
|
|
25
|
+
export const ENGINE_SWITCH_VERSION = 'engine_switch_pre_v1'
|
|
26
26
|
|
|
27
27
|
/** 终态:到达后不再变化(除 begin 开新代)。 */
|
|
28
28
|
const TERMINAL = new Set(['ready', 'failed', 'cancelled'])
|
|
@@ -117,7 +117,7 @@ export function createEngineSwitchPre(opts = {}) {
|
|
|
117
117
|
const fromIdentity = String(input.fromIdentity || state.toIdentity || '').trim()
|
|
118
118
|
const total = Number(input.total) > 0 ? Math.floor(Number(input.total)) : 0
|
|
119
119
|
if (!switchId) return { ok: false, switchId: '', generation, reason: 'no-switch-id' }
|
|
120
|
-
if (!/^
|
|
120
|
+
if (!/^engid_pre_[0-9a-f]{32}$/.test(toIdentity)) return { ok: false, switchId, generation, reason: 'bad-identity' }
|
|
121
121
|
// 幂等:同 id 已完成 → 不重跑
|
|
122
122
|
if (state.switchId === switchId && state.phase === 'ready' && state.toIdentity === toIdentity) {
|
|
123
123
|
return { ok: true, switchId, generation: state.generation, reason: 'already-ready' }
|
package/lib/episodic-store.js
CHANGED
|
@@ -26,21 +26,21 @@ import { createHash } from 'node:crypto'
|
|
|
26
26
|
|
|
27
27
|
// ========== 冻结常量 ==========
|
|
28
28
|
|
|
29
|
-
export const EPISODIC_POLICY_VERSION = '
|
|
30
|
-
export const EPISODE_ID_PREFIX = '
|
|
31
|
-
export const EPISODE_ID_RE = /^
|
|
29
|
+
export const EPISODIC_POLICY_VERSION = 'episodic_store_pre_v1'
|
|
30
|
+
export const EPISODE_ID_PREFIX = 'epi_pre_'
|
|
31
|
+
export const EPISODE_ID_RE = /^epi_pre_[0-9a-f]{32}$/
|
|
32
32
|
|
|
33
33
|
/** episode 累积上限(防无限增长;超过后最旧段被丢弃)。 */
|
|
34
|
-
export const
|
|
34
|
+
export const EPISODE_SEGMENT_CAP_PRE_V1 = 64
|
|
35
35
|
|
|
36
36
|
/** 巩固后保留的最多 episode 数(超出按时间淘汰,保留最近)。 */
|
|
37
|
-
export const
|
|
37
|
+
export const EPISODE_RETENTION_PRE_V1 = 256
|
|
38
38
|
|
|
39
39
|
/** 巩固阈值:至少多少段才算一个可巩固 episode(少于=丢弃,噪声太多)。 */
|
|
40
|
-
export const
|
|
40
|
+
export const EPISODE_MIN_SEGMENTS_PRE_V1 = 2
|
|
41
41
|
|
|
42
42
|
/** outcome 枚举。 */
|
|
43
|
-
export const
|
|
43
|
+
export const EPISODE_OUTCOMES_PRE_V1 = Object.freeze(['unknown', 'success', 'failure', 'partial'])
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* EpisodeSegment 校验(append 输入)。
|
|
@@ -69,7 +69,7 @@ export function validateEpisodePre(ep) {
|
|
|
69
69
|
if (!Array.isArray(ep.actions)) p.push('actions')
|
|
70
70
|
if (!Array.isArray(ep.entities)) p.push('entities')
|
|
71
71
|
if (!Array.isArray(ep.unresolved)) p.push('unresolved')
|
|
72
|
-
if (!
|
|
72
|
+
if (!EPISODE_OUTCOMES_PRE_V1.includes(ep.outcome)) p.push('outcome')
|
|
73
73
|
if (!Array.isArray(ep.provenance)) p.push('provenance')
|
|
74
74
|
if (typeof ep.startedAt !== 'number' || !Number.isFinite(ep.startedAt)) p.push('startedAt')
|
|
75
75
|
if (ep.consolidatedAt !== undefined && (typeof ep.consolidatedAt !== 'number' || !Number.isFinite(ep.consolidatedAt))) p.push('consolidatedAt')
|
|
@@ -89,9 +89,9 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
89
89
|
const io = opts.io || { save() {}, load() { return null }, clear() {} }
|
|
90
90
|
const nowFn = typeof opts.now === 'function' ? opts.now : () => Date.now()
|
|
91
91
|
const cfg = Object.assign({
|
|
92
|
-
segmentCap:
|
|
93
|
-
retention:
|
|
94
|
-
minSegments:
|
|
92
|
+
segmentCap: EPISODE_SEGMENT_CAP_PRE_V1,
|
|
93
|
+
retention: EPISODE_RETENTION_PRE_V1,
|
|
94
|
+
minSegments: EPISODE_MIN_SEGMENTS_PRE_V1,
|
|
95
95
|
}, opts.config || {})
|
|
96
96
|
|
|
97
97
|
let episodes = [] // 已巩固 episode(含 candidate 状态)
|
|
@@ -100,7 +100,7 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
100
100
|
const stats = { segmentsAppended: 0, consolidated: 0, droppedTooShort: 0, retained: 0 }
|
|
101
101
|
|
|
102
102
|
function defaultEpisodeId(sessionRef, startedAt) {
|
|
103
|
-
const h = createHash('sha256').update(['episode-v1', String(sessionRef), String(startedAt)].join('\u0000')).digest('hex')
|
|
103
|
+
const h = createHash('sha256').update(['episode-pre-v1', String(sessionRef), String(startedAt)].join('\u0000')).digest('hex')
|
|
104
104
|
return EPISODE_ID_PREFIX + h.slice(0, 32)
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -335,7 +335,7 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
335
335
|
// ---- 持久化 ----
|
|
336
336
|
function snapshot() {
|
|
337
337
|
return {
|
|
338
|
-
schemaVersion: 1, namespace: 'dsh-auto-memory', policyVersion: EPISODIC_POLICY_VERSION,
|
|
338
|
+
schemaVersion: 1, namespace: 'dsh-auto-memory-pre', policyVersion: EPISODIC_POLICY_VERSION,
|
|
339
339
|
savedAt: nowFn(),
|
|
340
340
|
// ★ P2-2 修复:快照数组字段同样做副本(调用方改快照不得改脏库内 episode)
|
|
341
341
|
episodes: episodes.map(cloneEpisode),
|
package/lib/evidence-agg.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* evidence-agg-pre —— evidence 事件 → 聚合 → importance 输入契约(M8-2b, 2026-09-09)。
|
|
3
3
|
*
|
|
4
|
-
* 管道:evidence/events/*.jsonl(写入侧 context-bridge-pre,只读)→ 有界扫描 →
|
|
4
|
+
* 管道:evidence-pre/events/*.jsonl(写入侧 context-bridge-pre,只读)→ 有界扫描 →
|
|
5
5
|
* 按 memoryId 聚合六类计数 + distinctSessions → 交给 memory-importance-pre.computeImportancePre
|
|
6
6
|
* → 作为 recall() L0 融合的加权因子之一(P8 融合入口)。
|
|
7
7
|
*
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* - fail-soft:行损坏跳过;io 抛错由调用方处理(模块内不吞 IO 异常——io 是注入的,调用方决定降级)。
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
export const EVIDENCE_AGG_VERSION = '
|
|
17
|
+
export const EVIDENCE_AGG_VERSION = 'evidence_agg_pre_v1'
|
|
18
18
|
|
|
19
19
|
/** 有界读取默认值。 */
|
|
20
|
-
export const
|
|
20
|
+
export const EVIDENCE_AGG_DEFAULTS_PRE_V1 = Object.freeze({
|
|
21
21
|
maxAgeDays: 7, // 文件名日期距 now 的最大天数
|
|
22
22
|
maxLinesPerFile: 400, // 每文件末 N 行(沿用 index.js 证据读取范式 slice(-400))
|
|
23
23
|
})
|
|
@@ -31,7 +31,7 @@ const KINDS = ['seen', 'read', 'cite', 'reuse', 'success', 'correction']
|
|
|
31
31
|
* @returns {Array<object>} 解析后的事件对象(损坏行跳过;任何字段缺失由聚合层兜底)
|
|
32
32
|
*/
|
|
33
33
|
export function scanEvidenceEventsPre(io, opts = {}) {
|
|
34
|
-
const d = Object.assign({},
|
|
34
|
+
const d = Object.assign({}, EVIDENCE_AGG_DEFAULTS_PRE_V1, opts)
|
|
35
35
|
const now = d.now || Date.now
|
|
36
36
|
const files = (io.listFiles() || []).slice().sort()
|
|
37
37
|
const cutoff = now() - d.maxAgeDays * 86400000
|
package/lib/evidence-store.js
CHANGED
|
@@ -16,9 +16,9 @@ const sha256Str = (s) => createHash('sha256').update(Buffer.from(String(s), 'utf
|
|
|
16
16
|
const first32 = (h) => h.slice(0, 32)
|
|
17
17
|
|
|
18
18
|
/** §10 持久化策略(冻结)。 */
|
|
19
|
-
export const
|
|
19
|
+
export const EVIDENCE_STORE_POLICY_PRE_V1 = Object.freeze({
|
|
20
20
|
schemaVersion: 1,
|
|
21
|
-
storePolicyVersion: '
|
|
21
|
+
storePolicyVersion: 'evidence_store_pre_v1',
|
|
22
22
|
keepDays: 30,
|
|
23
23
|
maxTotalBytes: 32 * 1024 * 1024,
|
|
24
24
|
eventMaxBytes: 16 * 1024,
|
|
@@ -32,12 +32,12 @@ export function canonicalWorkspaceKey(key) {
|
|
|
32
32
|
|
|
33
33
|
/** §10 隐私投影:sessionId → plugin-local sessionRef(不可逆哈希;跨文件稳定)。 */
|
|
34
34
|
export function sessionRefOf(sessionId) {
|
|
35
|
-
return 'sesr_' + first32(sha256Str('evidence-sesref-v1\u0000' + String(sessionId || '')))
|
|
35
|
+
return 'sesr_' + first32(sha256Str('evidence-sesref-pre-v1\u0000' + String(sessionId || '')))
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** §10 隐私投影:workspace 绝对键 → 稳定 workspaceRef(不落盘任何绝对路径)。 */
|
|
39
39
|
export function workspaceRefOf(workspaceKey) {
|
|
40
|
-
return 'wsr_' + first32(sha256Str('evidence-wsref-v1\u0000' + canonicalWorkspaceKey(workspaceKey)))
|
|
40
|
+
return 'wsr_' + first32(sha256Str('evidence-wsref-pre-v1\u0000' + canonicalWorkspaceKey(workspaceKey)))
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -48,11 +48,11 @@ export function workspaceRefOf(workspaceKey) {
|
|
|
48
48
|
export function projectEvidenceForDurable(ev, opts = {}) {
|
|
49
49
|
const v = validateAccessEvidencePre(ev)
|
|
50
50
|
if (!v.ok) return { ok: false, reason: 'invalid-evidence:' + v.reason }
|
|
51
|
-
const maxBytes = Number(opts.eventMaxBytes) ||
|
|
51
|
+
const maxBytes = Number(opts.eventMaxBytes) || EVIDENCE_STORE_POLICY_PRE_V1.eventMaxBytes
|
|
52
52
|
const projected = {
|
|
53
53
|
schemaVersion: 1,
|
|
54
54
|
namespace: NAMESPACE,
|
|
55
|
-
storePolicyVersion:
|
|
55
|
+
storePolicyVersion: EVIDENCE_STORE_POLICY_PRE_V1.storePolicyVersion,
|
|
56
56
|
evidenceId: ev.evidenceId,
|
|
57
57
|
kind: ev.kind,
|
|
58
58
|
memoryId: ev.memoryId,
|
|
@@ -101,12 +101,12 @@ export function parseDurableEventLine(line) {
|
|
|
101
101
|
* 表放**模块级**而非实例级:同进程多实例共享 ⇒ 不会各自持一份而双写。
|
|
102
102
|
* 语义同 index.js 的 diagThrottled:首条立即透出,窗口内重复压掉。
|
|
103
103
|
*/
|
|
104
|
-
const
|
|
104
|
+
const DIAG_THROTTLE_MS_PRE_V1 = 300000
|
|
105
105
|
const _diagAt = new Map()
|
|
106
106
|
function shouldReportDiag(key) {
|
|
107
107
|
try {
|
|
108
108
|
const now = Date.now()
|
|
109
|
-
if (now - (Number(_diagAt.get(key)) || 0) <
|
|
109
|
+
if (now - (Number(_diagAt.get(key)) || 0) < DIAG_THROTTLE_MS_PRE_V1) return false
|
|
110
110
|
_diagAt.set(key, now)
|
|
111
111
|
} catch (_) { /* 计时失败不阻断上报 */ }
|
|
112
112
|
return true
|
|
@@ -117,7 +117,7 @@ export class EvidenceEventStore {
|
|
|
117
117
|
this.root = opts.root || null
|
|
118
118
|
this.eventsDir = opts.eventsDir || (this.root ? path.join(this.root, 'events') : null)
|
|
119
119
|
this._chain = Promise.resolve()
|
|
120
|
-
this._appended = new BoundedIdSet(
|
|
120
|
+
this._appended = new BoundedIdSet(EVIDENCE_STORE_POLICY_PRE_V1.appendedIdCache)
|
|
121
121
|
this._swept = false
|
|
122
122
|
this.stats = { appended: 0, duplicates: 0, oversize: 0, invalid: 0, writeFailed: 0, sweptFiles: 0, lastWriteError: null }
|
|
123
123
|
// ★P3-9(组2):可注入诊断通道(宿主不注入时 reportDiag 回落 console.error,行为向后兼容)。
|
|
@@ -189,7 +189,7 @@ export class EvidenceEventStore {
|
|
|
189
189
|
files.sort((a, b) => a.mtimeMs - b.mtimeMs)
|
|
190
190
|
for (const f of files) {
|
|
191
191
|
const ageDays = (now - f.mtimeMs) / 86400000
|
|
192
|
-
if (ageDays >
|
|
192
|
+
if (ageDays > EVIDENCE_STORE_POLICY_PRE_V1.keepDays || total > EVIDENCE_STORE_POLICY_PRE_V1.maxTotalBytes) {
|
|
193
193
|
try { rmSync(f.fp, { force: true }); this.stats.sweptFiles++; total -= f.size } catch (_) {}
|
|
194
194
|
} else break
|
|
195
195
|
}
|
|
@@ -295,11 +295,11 @@ export function rebuildAggregates(durableEvents, corpusRecords) {
|
|
|
295
295
|
return { aggregates, byWorkspaceRef, duplicates, sessionScoped }
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
/** aggregate 快照持久化(evidence/aggregates/index.json;可重建派生物,仅调试/导出用)。 */
|
|
298
|
+
/** aggregate 快照持久化(evidence-pre/aggregates/index.json;可重建派生物,仅调试/导出用)。 */
|
|
299
299
|
export function persistAggregatesSnapshot(root, aggregates) {
|
|
300
300
|
const dir = path.join(root, 'aggregates')
|
|
301
301
|
mkdirSync(dir, { recursive: true })
|
|
302
|
-
const payload = { schemaVersion: 1, namespace: NAMESPACE, storePolicyVersion:
|
|
302
|
+
const payload = { schemaVersion: 1, namespace: NAMESPACE, storePolicyVersion: EVIDENCE_STORE_POLICY_PRE_V1.storePolicyVersion, rebuiltAt: Date.now(), aggregates }
|
|
303
303
|
writeFileSync(path.join(dir, 'index.json'), JSON.stringify(payload, null, 2) + '\n', 'utf8')
|
|
304
304
|
return path.join(dir, 'index.json')
|
|
305
305
|
}
|
package/lib/fact-store.js
CHANGED
|
@@ -39,38 +39,38 @@ import { createHash } from 'node:crypto'
|
|
|
39
39
|
// ========== 冻结常量(变更必须升级 FACT_POLICY_VERSION) ==========
|
|
40
40
|
|
|
41
41
|
/** Fact 策略版本。 */
|
|
42
|
-
export const FACT_POLICY_VERSION = '
|
|
42
|
+
export const FACT_POLICY_VERSION = 'fact_store_pre_v1'
|
|
43
43
|
|
|
44
44
|
/** factId 前缀(确定性身份)。 */
|
|
45
|
-
export const FACT_ID_PREFIX = '
|
|
45
|
+
export const FACT_ID_PREFIX = 'fact_pre_'
|
|
46
46
|
|
|
47
47
|
/** factId 长度前缀断言(与 memoryId/anchorId 同风格)。 */
|
|
48
|
-
export const FACT_ID_RE = /^
|
|
48
|
+
export const FACT_ID_RE = /^fact_pre_[0-9a-f]{32}$/
|
|
49
49
|
|
|
50
50
|
/** scope 枚举: 与 M5/M6 的 scope 对齐(User=跨工作区, Workspace=当前工作区)。 */
|
|
51
|
-
export const
|
|
51
|
+
export const FACT_SCOPES_PRE_V1 = Object.freeze(['User', 'Workspace'])
|
|
52
52
|
|
|
53
53
|
/** M8-1 认识论状态枚举: fact=证据/不可推导, observation=推断/可重算, directive=行为指令。 */
|
|
54
|
-
export const
|
|
54
|
+
export const FACT_EPISTEMIC_STATUSES_PRE_V1 = Object.freeze(['fact', 'observation', 'directive'])
|
|
55
55
|
|
|
56
56
|
/** M8-1 趋势枚举: 相对上次确认的生命周期走向。 */
|
|
57
|
-
export const
|
|
57
|
+
export const FACT_TRENDS_PRE_V1 = Object.freeze(['new', 'strengthening', 'stable', 'weakening', 'stale'])
|
|
58
58
|
|
|
59
59
|
/** sourceClass 枚举: 与 m4-corpus-pre 的 sourceClass 对齐。 */
|
|
60
|
-
export const
|
|
60
|
+
export const FACT_SOURCE_CLASSES_PRE_V1 = Object.freeze([
|
|
61
61
|
'user-memory', 'workspace-notes', 'workspace-log', 'semantic-candidate', 'profile-candidate',
|
|
62
62
|
])
|
|
63
63
|
|
|
64
64
|
/** 来源类型: 用户明确声明(硬规则) vs 模型推断(candidate)。 */
|
|
65
|
-
export const
|
|
65
|
+
export const FACT_SOURCE_KINDS_PRE_V1 = Object.freeze(['explicit', 'inference'])
|
|
66
66
|
|
|
67
67
|
/** 冲突处理结果枚举。 */
|
|
68
|
-
export const
|
|
68
|
+
export const UPSERT_OUTCOMES_PRE_V1 = Object.freeze([
|
|
69
69
|
'created', 'merged', 'superseded', 'conflict-added', 'inference-blocked', 'expired-ignored',
|
|
70
70
|
])
|
|
71
71
|
|
|
72
72
|
/** TTL 语义: 过期事实视为不存在(读取时过滤),但保留记录供审计。 */
|
|
73
|
-
export const
|
|
73
|
+
export const FACT_TTL_DEFAULT_PRE_V1 = 0 // 0 = 永不过期
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* FactCandidate 校验(fail closed)。
|
|
@@ -79,12 +79,12 @@ export const FACT_TTL_DEFAULT_V1 = 0 // 0 = 永不过期
|
|
|
79
79
|
export function validateFactCandidatePre(cand) {
|
|
80
80
|
const p = []
|
|
81
81
|
if (!cand || typeof cand !== 'object' || Array.isArray(cand)) return { ok: false, reason: 'not-object' }
|
|
82
|
-
if (!
|
|
82
|
+
if (!FACT_SCOPES_PRE_V1.includes(cand.scope)) p.push('scope')
|
|
83
83
|
if (typeof cand.subject !== 'string' || !cand.subject.trim()) p.push('subject')
|
|
84
84
|
if (typeof cand.predicate !== 'string' || !cand.predicate.trim()) p.push('predicate')
|
|
85
85
|
if (cand.object !== undefined && cand.object !== null && typeof cand.object !== 'string') p.push('object')
|
|
86
|
-
if (!
|
|
87
|
-
if (cand.sourceClass !== undefined && !
|
|
86
|
+
if (!FACT_SOURCE_KINDS_PRE_V1.includes(cand.sourceKind)) p.push('sourceKind')
|
|
87
|
+
if (cand.sourceClass !== undefined && !FACT_SOURCE_CLASSES_PRE_V1.includes(cand.sourceClass)) p.push('sourceClass')
|
|
88
88
|
if (cand.provenance !== undefined && (!Array.isArray(cand.provenance) || cand.provenance.some((s) => typeof s !== 'string'))) p.push('provenance')
|
|
89
89
|
if (cand.confidence !== undefined && (typeof cand.confidence !== 'number' || !Number.isFinite(cand.confidence) || cand.confidence < 0 || cand.confidence > 1)) p.push('confidence')
|
|
90
90
|
if (cand.ttl !== undefined && (typeof cand.ttl !== 'number' || !Number.isFinite(cand.ttl) || cand.ttl < 0)) p.push('ttl')
|
|
@@ -92,8 +92,8 @@ export function validateFactCandidatePre(cand) {
|
|
|
92
92
|
for (const k of ['occurredAt', 'mentionedAt', 'ingestedAt']) {
|
|
93
93
|
if (cand[k] !== undefined && (typeof cand[k] !== 'number' || !Number.isFinite(cand[k]) || cand[k] < 0)) p.push(k)
|
|
94
94
|
}
|
|
95
|
-
if (cand.epistemicStatus !== undefined && !
|
|
96
|
-
if (cand.trend !== undefined && !
|
|
95
|
+
if (cand.epistemicStatus !== undefined && !FACT_EPISTEMIC_STATUSES_PRE_V1.includes(cand.epistemicStatus)) p.push('epistemicStatus')
|
|
96
|
+
if (cand.trend !== undefined && !FACT_TRENDS_PRE_V1.includes(cand.trend)) p.push('trend')
|
|
97
97
|
if (p.length) return { ok: false, reason: 'invalid:' + p.join(',') }
|
|
98
98
|
return { ok: true, candidate: cand }
|
|
99
99
|
}
|
|
@@ -106,7 +106,7 @@ export function validateFactPre(fact) {
|
|
|
106
106
|
const p = []
|
|
107
107
|
if (!fact || typeof fact !== 'object' || Array.isArray(fact)) return { ok: false, reason: 'not-object' }
|
|
108
108
|
if (typeof fact.factId !== 'string' || !FACT_ID_RE.test(fact.factId)) p.push('factId')
|
|
109
|
-
if (!
|
|
109
|
+
if (!FACT_SCOPES_PRE_V1.includes(fact.scope)) p.push('scope')
|
|
110
110
|
if (typeof fact.subject !== 'string' || !fact.subject.trim()) p.push('subject')
|
|
111
111
|
if (typeof fact.predicate !== 'string' || !fact.predicate.trim()) p.push('predicate')
|
|
112
112
|
if (fact.object !== undefined && fact.object !== null && typeof fact.object !== 'string') p.push('object')
|
|
@@ -118,8 +118,8 @@ export function validateFactPre(fact) {
|
|
|
118
118
|
for (const k of ['occurredAt', 'mentionedAt', 'ingestedAt']) {
|
|
119
119
|
if (fact[k] !== undefined && (typeof fact[k] !== 'number' || !Number.isFinite(fact[k]))) p.push(k)
|
|
120
120
|
}
|
|
121
|
-
if (fact.epistemicStatus !== undefined && !
|
|
122
|
-
if (fact.trend !== undefined && !
|
|
121
|
+
if (fact.epistemicStatus !== undefined && !FACT_EPISTEMIC_STATUSES_PRE_V1.includes(fact.epistemicStatus)) p.push('epistemicStatus')
|
|
122
|
+
if (fact.trend !== undefined && !FACT_TRENDS_PRE_V1.includes(fact.trend)) p.push('trend')
|
|
123
123
|
if (p.length) return { ok: false, reason: 'invalid:' + p.join(',') }
|
|
124
124
|
return { ok: true, fact }
|
|
125
125
|
}
|
|
@@ -139,15 +139,15 @@ export function isFactConflict(a, b) {
|
|
|
139
139
|
// ========== B-3 事实性入口判据(保守版:只拒「明显不是事实陈述」的形态) ==========
|
|
140
140
|
|
|
141
141
|
/** 陈述文本最小长度(去空白后);低于此值且呈散文形态才判为碎片。 */
|
|
142
|
-
export const
|
|
142
|
+
export const FACT_MIN_STATEMENT_CHARS_PRE_V1 = 6
|
|
143
143
|
|
|
144
144
|
/** 判据子类码(冻结;前端/工具按它给中文说明,不要按 code 反推语义)。 */
|
|
145
|
-
export const
|
|
145
|
+
export const FACTNESS_CODES_PRE_V1 = Object.freeze([
|
|
146
146
|
'factness-empty', 'factness-symbol-only', 'factness-question', 'factness-imperative', 'factness-too-short',
|
|
147
147
|
])
|
|
148
148
|
|
|
149
149
|
/** 子类码 → 中文人话说明:既让模型知道「怎么改写成合法陈述」,也让人不看代码就看得懂。 */
|
|
150
|
-
export const
|
|
150
|
+
export const FACTNESS_MESSAGES_PRE_V1 = Object.freeze({
|
|
151
151
|
'factness-empty': '内容为空白(没有任何字符),不是一条事实陈述。请写成「主体 + 谓词 (+ 宾语)」再入库,例如 主体=「项目」、谓词=「构建工具」、宾语=「esbuild」。',
|
|
152
152
|
'factness-symbol-only': '内容只有标点、符号或表情,没有可读的文字或数字,不是一条事实陈述。请补上文字描述再入库,例如 主体=「项目」、谓词=「状态」、宾语=「进行中」。',
|
|
153
153
|
'factness-question': '内容是一个疑问句(以「?」或「?」结尾,且整句没有陈述成分),不是一条事实陈述。请先把答案写成陈述句再入库,例如写「端口 默认值 3080」,而不是「端口是多少?」。',
|
|
@@ -156,7 +156,7 @@ export const FACTNESS_MESSAGES_V1 = Object.freeze({
|
|
|
156
156
|
})
|
|
157
157
|
|
|
158
158
|
/** 判据子类码 → stats 计数键(诊断面可分别读数)。 */
|
|
159
|
-
const
|
|
159
|
+
const FACTNESS_STAT_KEYS_PRE_V1 = Object.freeze({
|
|
160
160
|
'factness-empty': 'factnessEmpty',
|
|
161
161
|
'factness-symbol-only': 'factnessSymbolOnly',
|
|
162
162
|
'factness-question': 'factnessQuestion',
|
|
@@ -165,35 +165,35 @@ const FACTNESS_STAT_KEYS_V1 = Object.freeze({
|
|
|
165
165
|
})
|
|
166
166
|
|
|
167
167
|
/** 认识论状态强弱序:推断(observation) < 明确陈述(fact/directive)。 */
|
|
168
|
-
const
|
|
168
|
+
const FACT_EPISTEMIC_RANK_PRE_V1 = Object.freeze({ observation: 1, fact: 2, directive: 2 })
|
|
169
169
|
|
|
170
170
|
/** 有词形字符(拉丁/数字/CJK/假名/谚文/西里尔/希腊/全角字母数字)才算「有内容」。 */
|
|
171
|
-
const
|
|
171
|
+
const FACT_WORD_CHAR_RE_PRE_V1 = /[0-9A-Za-z\u00C0-\u024F\u0370-\u03FF\u0400-\u04FF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A]/
|
|
172
172
|
|
|
173
173
|
/** 句读/符号:字段里出现即视为「散文残片」,而不是结构化词条(空白 + ASCII 标点 + 中西标点/表情区)。 */
|
|
174
|
-
const
|
|
174
|
+
const FACT_PROSE_PUNCT_RE_PRE_V1 = /[\s!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\u2000-\u206F\u2190-\u2BFF\u3000-\u303F\uD800-\uDFFF\uFE10-\uFE1F\uFE30-\uFE6F\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65]/
|
|
175
175
|
|
|
176
176
|
/** 只含中日韩文字(用于「英文祈使开头只在纯英文文本上生效」这条护栏)。 */
|
|
177
|
-
const
|
|
177
|
+
const FACT_CJK_CHAR_RE_PRE_V1 = /[\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]/
|
|
178
178
|
|
|
179
179
|
/** 疑问尾:「?」或「?」结尾(且整句无陈述成分时判为疑问)。 */
|
|
180
|
-
const
|
|
180
|
+
const FACT_QUESTION_TAIL_RE_PRE_V1 = /[??]\s*$/
|
|
181
181
|
|
|
182
182
|
/** 陈述成分标记:出现任一即认为「有主谓/已成立」⇒ 一律放行(宁可放过不可误杀)。 */
|
|
183
|
-
const
|
|
183
|
+
const FACT_CLAUSE_MARKER_RE_PRE_V1 = /(是|为|在|有|了|被|过|会|能|需|由|得|着|将|已|后|前|时|与|和|及|即|属|含|指|表示|代表|完成|支持|启用|关闭|默认|等于|位于|\b(is|are|was|were|be|been|has|have|had|will|would|can|could|must|should|equals?|means|contains|supports|defaults?|uses?)\b)/i
|
|
184
184
|
|
|
185
185
|
/** 疑问/系词复合:先摘掉,免得「为什么」里的「为」被误当成陈述标记。 */
|
|
186
|
-
const
|
|
186
|
+
const FACT_CLAUSE_STRIP_RE_PRE_V1 = /(为什么|为何|什么样|什么时候|因为|认为|以为|视为|作为|属于|分为)/g
|
|
187
187
|
|
|
188
188
|
/** 祈使开头:这些形态**且整句无陈述成分**时才判为指令(请求式英文词始终生效)。 */
|
|
189
|
-
const
|
|
189
|
+
const FACT_IMPERATIVE_OPENERS_PRE_V1 = Object.freeze([
|
|
190
190
|
/^(帮我|帮忙|帮个忙|麻烦|拜托|烦请|给我|替我|请帮|请把|请将|请给|请写|请记|请查|请修|请改|请加|请添|请看|请做)/,
|
|
191
191
|
/^把[^,,。.;;??!!]{0,12}(改成|改为|换成|换为|调成|调为|设置为|设为|调整|删除|删掉|去掉|加上|添加|补上|写上|写成)/,
|
|
192
192
|
/^(please|pls|plz|help me|can you|could you|would you|i need you to|i want you to|go ahead and|make sure|remember to|do not|don't)\b/i,
|
|
193
193
|
])
|
|
194
194
|
|
|
195
195
|
/** 裸动词祈使开头:**仅纯英文文本**才生效(中式事实句「Run 命令:…」不得被误杀)。 */
|
|
196
|
-
const
|
|
196
|
+
const FACT_IMP_VERB_OPENERS_EN_PRE_V1 = /^(add|change|update|fix|remove|delete|rename|move|run|write|create|implement|refactor|ensure|revert|bump|install)\b/i
|
|
197
197
|
|
|
198
198
|
/** 去掉全部空白(长度与词形判定用)。 */
|
|
199
199
|
function factFlatPre(text) {
|
|
@@ -202,14 +202,14 @@ function factFlatPre(text) {
|
|
|
202
202
|
|
|
203
203
|
/** 整句是否带陈述成分(带 ⇒ 放行信号)。 */
|
|
204
204
|
function factHasClausePre(text) {
|
|
205
|
-
const s = String(text == null ? '' : text).replace(
|
|
206
|
-
return
|
|
205
|
+
const s = String(text == null ? '' : text).replace(FACT_CLAUSE_STRIP_RE_PRE_V1, ' ')
|
|
206
|
+
return FACT_CLAUSE_MARKER_RE_PRE_V1.test(s)
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/** 整句是否「只有疑问」:以 ?/? 结尾且没有陈述成分。 */
|
|
210
210
|
function factQuestionOnlyPre(text) {
|
|
211
211
|
const s = String(text == null ? '' : text).trim()
|
|
212
|
-
if (!
|
|
212
|
+
if (!FACT_QUESTION_TAIL_RE_PRE_V1.test(s)) return false
|
|
213
213
|
return !factHasClausePre(s)
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -217,8 +217,8 @@ function factQuestionOnlyPre(text) {
|
|
|
217
217
|
function factImperativeOnlyPre(text) {
|
|
218
218
|
const s = String(text == null ? '' : text).trim()
|
|
219
219
|
if (!s) return false
|
|
220
|
-
if (
|
|
221
|
-
if (!
|
|
220
|
+
if (FACT_IMPERATIVE_OPENERS_PRE_V1.some((re) => re.test(s))) return !factHasClausePre(s)
|
|
221
|
+
if (!FACT_CJK_CHAR_RE_PRE_V1.test(s) && FACT_IMP_VERB_OPENERS_EN_PRE_V1.test(s)) return !factHasClausePre(s)
|
|
222
222
|
return false
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -227,7 +227,7 @@ function factTermLikeFieldPre(v) {
|
|
|
227
227
|
if (typeof v !== 'string') return true
|
|
228
228
|
const s = v.trim()
|
|
229
229
|
if (!s) return true
|
|
230
|
-
return !
|
|
230
|
+
return !FACT_PROSE_PUNCT_RE_PRE_V1.test(s)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/** 三元组是否「结构化短词条」(每个字段都是词条形态)。 */
|
|
@@ -238,7 +238,7 @@ function factStructuredTuplePre(cand) {
|
|
|
238
238
|
|
|
239
239
|
/** 组装一个拒绝判定(code + 中文人话 message)。 */
|
|
240
240
|
function factnessVerdictPre(code) {
|
|
241
|
-
return { ok: false, code, message:
|
|
241
|
+
return { ok: false, code, message: FACTNESS_MESSAGES_PRE_V1[code] || code }
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
@@ -252,10 +252,10 @@ export function looksFactStatementPre(text) {
|
|
|
252
252
|
const raw = String(text == null ? '' : text).trim()
|
|
253
253
|
const flat = raw.replace(/\s+/g, '')
|
|
254
254
|
if (!flat) return factnessVerdictPre('factness-empty')
|
|
255
|
-
if (!
|
|
255
|
+
if (!FACT_WORD_CHAR_RE_PRE_V1.test(flat)) return factnessVerdictPre('factness-symbol-only')
|
|
256
256
|
if (factQuestionOnlyPre(raw)) return factnessVerdictPre('factness-question')
|
|
257
257
|
if (factImperativeOnlyPre(raw)) return factnessVerdictPre('factness-imperative')
|
|
258
|
-
if (flat.length <
|
|
258
|
+
if (flat.length < FACT_MIN_STATEMENT_CHARS_PRE_V1) return factnessVerdictPre('factness-too-short')
|
|
259
259
|
return { ok: true }
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -302,7 +302,7 @@ export function looksFactCandidatePre(cand) {
|
|
|
302
302
|
* 调用方显式给了合法值就尊重(本函数只在缺省时用);
|
|
303
303
|
* 来源 = 用户明确陈述(sourceKind='explicit' / sourceClass='user-memory' / origin='user')→ 'fact'(明说);
|
|
304
304
|
* 其余(模型推断、自动沉淀、judgement 候选)→ 'observation'(推断)。
|
|
305
|
-
* ⚠️ 取值必须落在冻结枚举
|
|
305
|
+
* ⚠️ 取值必须落在冻结枚举 FACT_EPISTEMIC_STATUSES_PRE_V1 内 —— 写 'inference'/'explicit'
|
|
306
306
|
* 这类枚举外的字面量会被 validateFactPre 判非法并**整条拒收**,不能用。
|
|
307
307
|
*/
|
|
308
308
|
export function defaultEpistemicStatusPre(cand) {
|
|
@@ -341,7 +341,7 @@ export function createFactStorePre(opts = {}) {
|
|
|
341
341
|
// ---- 确定性 factId(与 memoryId/anchorId 同风格) ----
|
|
342
342
|
function defaultFactId(subject, predicate, object, scope) {
|
|
343
343
|
const h = createHash('sha256').update(
|
|
344
|
-
['fact-v1', String(scope), String(subject), String(predicate), String(object == null ? '' : object)].join('\u0000')
|
|
344
|
+
['fact-pre-v1', String(scope), String(subject), String(predicate), String(object == null ? '' : object)].join('\u0000')
|
|
345
345
|
).digest('hex')
|
|
346
346
|
return FACT_ID_PREFIX + h.slice(0, 32)
|
|
347
347
|
}
|
|
@@ -406,8 +406,8 @@ export function createFactStorePre(opts = {}) {
|
|
|
406
406
|
if (c.epistemicStatus !== undefined) {
|
|
407
407
|
// ★ B-4(2026-09-22):认识论状态跟随候选(入口 :~250 已补默认值),但**降级**要挡住 ——
|
|
408
408
|
// 用户明确陈述(fact/directive)不得被后续模型推断(observation)覆盖(与上面 sourceKind 的提升同向)。
|
|
409
|
-
const prevRank =
|
|
410
|
-
const candRank =
|
|
409
|
+
const prevRank = FACT_EPISTEMIC_RANK_PRE_V1[prev.epistemicStatus] || 0
|
|
410
|
+
const candRank = FACT_EPISTEMIC_RANK_PRE_V1[c.epistemicStatus] || 0
|
|
411
411
|
if (prev.epistemicStatus === undefined || c.sourceKind === 'explicit' || candRank > prevRank) {
|
|
412
412
|
prev.epistemicStatus = c.epistemicStatus
|
|
413
413
|
}
|
|
@@ -442,7 +442,7 @@ export function createFactStorePre(opts = {}) {
|
|
|
442
442
|
const factness = looksFactCandidatePre(c)
|
|
443
443
|
if (!factness.ok) {
|
|
444
444
|
stats.factnessRejected++
|
|
445
|
-
const statKey =
|
|
445
|
+
const statKey = FACTNESS_STAT_KEYS_PRE_V1[factness.code]
|
|
446
446
|
if (statKey) stats[statKey]++
|
|
447
447
|
lastFactnessReject = { code: factness.code, message: factness.message, text: factStatementTextPre(c), at: now }
|
|
448
448
|
return {
|
|
@@ -510,7 +510,7 @@ export function createFactStorePre(opts = {}) {
|
|
|
510
510
|
sourceClass: c.sourceClass || (c.sourceKind === 'explicit' ? 'user-memory' : 'semantic-candidate'),
|
|
511
511
|
provenance: c.provenance || [],
|
|
512
512
|
confidence: c.confidence !== undefined ? c.confidence : null,
|
|
513
|
-
confirmedAt: now, ttl: c.ttl !== undefined ? c.ttl :
|
|
513
|
+
confirmedAt: now, ttl: c.ttl !== undefined ? c.ttl : FACT_TTL_DEFAULT_PRE_V1,
|
|
514
514
|
revoked: false,
|
|
515
515
|
// M8-1 时间三价:入库时间必填(=本次确认);发生/陈述时间候选提供则透传,缺省 undefined(落盘时省略)
|
|
516
516
|
ingestedAt: now,
|
|
@@ -737,7 +737,7 @@ export function createFactStorePre(opts = {}) {
|
|
|
737
737
|
function snapshot(opts = {}) {
|
|
738
738
|
const now = nowFn()
|
|
739
739
|
return {
|
|
740
|
-
schemaVersion: 1, namespace: 'dsh-auto-memory', policyVersion: FACT_POLICY_VERSION,
|
|
740
|
+
schemaVersion: 1, namespace: 'dsh-auto-memory-pre', policyVersion: FACT_POLICY_VERSION,
|
|
741
741
|
savedAt: now,
|
|
742
742
|
// ★ P2-2 修复:快照同样不得与库内对象共享 provenance 数组引用
|
|
743
743
|
// (宿主读快照后 push 会改脏已入账记录)。
|
package/lib/handoff-anchor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* P6(2026-09-09) 账本权重化截断纯核心(
|
|
2
|
+
* P6(2026-09-09) 账本权重化截断纯核心(handoff_ledger_weight_pre_v1)。
|
|
3
3
|
*
|
|
4
4
|
* 背景:buildContinueCarry 的 `ledger.slice(0, 8000)` 是位置截断,而账本段内异质——
|
|
5
5
|
* 位置截可能把高价值段整体截掉(M-CM7 §C/§G4)。本模块按账本自身四段标题赋权:
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
* - 只影响注入,不动文件原文。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
export const HANDOFF_LEDGER_WEIGHT_VERSION = '
|
|
15
|
+
export const HANDOFF_LEDGER_WEIGHT_VERSION = 'handoff_ledger_weight_pre_v1'
|
|
16
16
|
|
|
17
17
|
/** 四段标题 → 权重(降序)。标题为账本中的原样标题行(半角 '## '+单空格,不含行尾 \r)。 */
|
|
18
|
-
export const
|
|
18
|
+
export const HANDOFF_LEDGER_SECTION_WEIGHTS_PRE_V1 = Object.freeze([
|
|
19
19
|
Object.freeze({ title: '已试方案与失败原因', weight: 0.35 }),
|
|
20
20
|
Object.freeze({ title: '进度与下一步', weight: 0.30 }),
|
|
21
21
|
Object.freeze({ title: '目标', weight: 0.20 }),
|
|
22
22
|
Object.freeze({ title: '任务状态', weight: 0.15 }),
|
|
23
23
|
])
|
|
24
24
|
|
|
25
|
-
const TITLE_WEIGHT = new Map(
|
|
25
|
+
const TITLE_WEIGHT = new Map(HANDOFF_LEDGER_SECTION_WEIGHTS_PRE_V1.map((x) => [x.title, x.weight]))
|
|
26
26
|
/** 未知 '## ' 段:权重最低(最先被截),但仍保留标题与内容直到轮到它。 */
|
|
27
27
|
const UNKNOWN_WEIGHT = 0.05
|
|
28
28
|
const TRIM_MARK = '…(已按预算截断,全文见 handoff/ 最新账本)'
|