@a9i5k4/dsh-auto-memory 0.1.29 → 0.1.30

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.
@@ -0,0 +1,406 @@
1
+ /**
2
+ * M8-2 Procedural Store 纯核心(docs/proactive-associative-memory-system-map.html M-04 Procedural)。
3
+ * 纯内存状态机,零 IO、零依赖(node:crypto 仅作确定性身份);持久化通过可注入 IO 接口,
4
+ * Host 接线时才接真实文件(本模块自身不读写磁盘,测试用内存 IO)。
5
+ *
6
+ * ★ 这是「记忆中枢」的核心价值层:把多次使用的固定流程固化成 skill,
7
+ * 让 M7 记忆召回系统在相似场景自动召回这些 skill,AI 按固定流程执行。
8
+ *
9
+ * 生命周期状态机(M-04 元代码逐行落地):
10
+ * observed → candidate → validated → active → deprecated
11
+ *
12
+ * 晋升规则(promote, 元代码原样):
13
+ * - sessionDiversity ≥ 3(跨独立会话)且 successCount ≥ 2 → 可晋升
14
+ * - correctionRate > MAX_CORRECTION 或 contradictions → 保持 candidate
15
+ * - 无 successCriteria 或 无 cross-session evidence → 保持 candidate
16
+ * - riskLevel=high 且未用户批准 → 返回 {action:'ask'} 等待批准
17
+ * - 一次成功或三次重复均不足以证明可靠(元代码铁律)
18
+ *
19
+ * 渐进激活(M6 六级 level):
20
+ * index → hint → excerpt → checklist → resource → full
21
+ * 高风险工具(SSH/部署/删除)永不因相似度自动执行。
22
+ *
23
+ * 输出:active procedure 渲染成 checklist 注入包(M6 Reference Tail 的 checklist level),
24
+ * 供 M7 召回系统在相似场景自动召回。
25
+ *
26
+ * 与 M5 evidence 衔接:promote 读 evidence stats(seen/read/cite/reuse/success/correction),
27
+ * 由调用方传入(本模块不直接读 evidence store,保持纯核心)。
28
+ *
29
+ * 全部同输入确定; UTF-8 无 BOM。
30
+ */
31
+ import { createHash } from 'node:crypto'
32
+
33
+ // ========== 冻结常量 ==========
34
+
35
+ export const PROCEDURE_POLICY_VERSION = 'procedure_store_v1'
36
+ export const PROCEDURE_ID_PREFIX = 'proc_'
37
+ export const PROCEDURE_ID_RE = /^proc_[0-9a-f]{32}$/
38
+
39
+ /** 阶段枚举(M-04 元代码)。 */
40
+ export const PROCEDURE_STAGES_V1 = Object.freeze(['observed', 'candidate', 'validated', 'active', 'deprecated'])
41
+
42
+ /** 自动归档老化(Hermes curator 借鉴,2026-08-28):active 技能 lastUsedAt 超过
43
+ * PROCEDURE_ARCHIVE_AFTER_DAYS_V1 天未使用且未 pinned → 自动 deprecated
44
+ * (reason=auto-archive-inactive;deprecated 可恢复,永不物理删除)。
45
+ * observed/candidate/validated 不老化——它们由晋升门槛时间控。 */
46
+ export const PROCEDURE_ARCHIVE_AFTER_DAYS_V1 = 90
47
+
48
+ /** 风险等级枚举。 */
49
+ export const PROCEDURE_RISKS_V1 = Object.freeze(['low', 'medium', 'high'])
50
+
51
+ /** 激活等级(M6 六级契约)。 */
52
+ export const PROCEDURE_LEVELS_V1 = Object.freeze(['index', 'hint', 'excerpt', 'checklist', 'resource', 'full'])
53
+
54
+ /** 晋升门槛(可调参数,默认值来自 M-04 元代码): 跨会话多样性≥3, 成功≥2。 */
55
+ export const PROCEDURE_DEFAULT_GATES_V1 = Object.freeze({
56
+ minSessionDiversity: 3,
57
+ minSuccessCount: 2,
58
+ maxCorrectionRate: 0.3, // correction 占总证据比例上限
59
+ maxContradictions: 0, // 矛盾数上限(0=任何矛盾都阻止)
60
+ highRiskRequiresApproval: true,
61
+ })
62
+
63
+ /** 一次成功或三次重复都不足以证明可靠 → 需要的最小成功数(元代码铁律)。 */
64
+ export const PROCEDURE_MIN_SUCCESS_V1 = 2
65
+
66
+ /** active 后可注入的默认 level(渐进激活;高风险降级为 hint)。 */
67
+ export const PROCEDURE_ACTIVE_LEVEL_V1 = 'checklist'
68
+
69
+ /** procedure 校验(固化/读回)。 */
70
+ export function validateProcedurePre(p) {
71
+ const q = []
72
+ if (!p || typeof p !== 'object' || Array.isArray(p)) return { ok: false, reason: 'not-object' }
73
+ if (typeof p.procedureId !== 'string' || !PROCEDURE_ID_RE.test(p.procedureId)) q.push('procedureId')
74
+ if (!PROCEDURE_STAGES_V1.includes(p.stage)) q.push('stage')
75
+ if (!PROCEDURE_RISKS_V1.includes(p.riskLevel)) q.push('riskLevel')
76
+ if (typeof p.title !== 'string' || !p.title.trim()) q.push('title')
77
+ if (!Array.isArray(p.sourceMemoryIds)) q.push('sourceMemoryIds')
78
+ if (!Array.isArray(p.sourceEpisodes)) q.push('sourceEpisodes')
79
+ if (!Array.isArray(p.steps) || !p.steps.length) q.push('steps')
80
+ if (!Array.isArray(p.checks)) q.push('checks')
81
+ if (!Array.isArray(p.successCriteria)) q.push('successCriteria')
82
+ if (!Array.isArray(p.rollback)) q.push('rollback')
83
+ if (typeof p.createdAt !== 'number' || !Number.isFinite(p.createdAt)) q.push('createdAt')
84
+ if (p.requiresApproval !== undefined && typeof p.requiresApproval !== 'boolean') q.push('requiresApproval')
85
+ // 2026-08-28 Hermes 借鉴字段(可选,向后兼容旧快照):lastUsedAt/pinned/origin
86
+ if (p.lastUsedAt !== undefined && (typeof p.lastUsedAt !== 'number' || !Number.isFinite(p.lastUsedAt))) q.push('lastUsedAt')
87
+ if (p.pinned !== undefined && typeof p.pinned !== 'boolean') q.push('pinned')
88
+ if (p.origin !== undefined && !['agent', 'user'].includes(p.origin)) q.push('origin')
89
+ if (q.length) return { ok: false, reason: 'invalid:' + q.join(',') }
90
+ return { ok: true, procedure: p }
91
+ }
92
+
93
+ /** ProcedureCandidate 校验(observe/candidate 输入)。 */
94
+ export function validateProcedureCandidatePre(c) {
95
+ const p = []
96
+ if (!c || typeof c !== 'object' || Array.isArray(c)) return { ok: false, reason: 'not-object' }
97
+ if (typeof c.title !== 'string' || !c.title.trim()) p.push('title')
98
+ if (!PROCEDURE_RISKS_V1.includes(c.riskLevel || 'low')) p.push('riskLevel')
99
+ if (!Array.isArray(c.steps) || !c.steps.length) p.push('steps')
100
+ if (c.successCriteria !== undefined && !Array.isArray(c.successCriteria)) p.push('successCriteria')
101
+ if (c.sourceMemoryIds !== undefined && !Array.isArray(c.sourceMemoryIds)) p.push('sourceMemoryIds')
102
+ if (c.sourceEpisodes !== undefined && !Array.isArray(c.sourceEpisodes)) p.push('sourceEpisodes')
103
+ if (c.preconditions !== undefined && !Array.isArray(c.preconditions)) p.push('preconditions')
104
+ if (c.checks !== undefined && !Array.isArray(c.checks)) p.push('checks')
105
+ if (c.rollback !== undefined && !Array.isArray(c.rollback)) p.push('rollback')
106
+ if (p.length) return { ok: false, reason: 'invalid:' + p.join(',') }
107
+ return { ok: true, candidate: c }
108
+ }
109
+
110
+ /**
111
+ * Procedural Store 工厂。
112
+ * @param {object} opts
113
+ * @param {object} opts.io 可选持久化 { save(snapshot), load() → snapshot, clear() }
114
+ * @param {function} opts.now 可选时钟
115
+ * @param {function} opts.approve 可选用户批准回调(high-risk 时调用;测试注入)
116
+ * @param {object} opts.gates { minSessionDiversity, minSuccessCount, maxCorrectionRate, maxContradictions, highRiskRequiresApproval }
117
+ */
118
+ export function createProcedureStorePre(opts = {}) {
119
+ const io = opts.io || { save() {}, load() { return null }, clear() {} }
120
+ const nowFn = typeof opts.now === 'function' ? opts.now : () => Date.now()
121
+ const approveFn = typeof opts.approve === 'function' ? opts.approve : null
122
+ // 2026-08-30:gates 保留调用方对象引用(支持 getter 活读配置);缺省才用冻结默认。
123
+ // 注意:不可 Object.assign 展开——会立即求值 getter 并冻结挂载时的配置快照。
124
+ const gates = opts.gates || PROCEDURE_DEFAULT_GATES_V1
125
+ const activeLevel = opts.activeLevel || PROCEDURE_ACTIVE_LEVEL_V1
126
+
127
+ let procedures = [] // 全部 procedure(各 stage)
128
+ let disposed = false
129
+ const stats = { observed: 0, candidates: 0, validated: 0, activated: 0, deprecated: 0, approvalAsked: 0 }
130
+
131
+ function defaultProcedureId(title, createdAt) {
132
+ const h = createHash('sha256').update(['procedure-pre-v1', String(title), String(createdAt)].join('\u0000')).digest('hex')
133
+ return PROCEDURE_ID_PREFIX + h.slice(0, 32)
134
+ }
135
+
136
+ // ---- 观察: 从 episode 或 judgement-shadow 的 procedure_candidate 进入 observed/candidate ----
137
+ function observe(cand) {
138
+ if (disposed) return { ok: false, reason: 'disposed' }
139
+ const v = validateProcedureCandidatePre(cand)
140
+ if (!v.ok) return { ok: false, reason: v.reason }
141
+ const c = v.candidate
142
+ const now = nowFn()
143
+ // 去重: 同 title 已存在 → 返回 existing(不重复 observe)
144
+ const existing = procedures.find((p) => p.title === c.title && p.stage !== 'deprecated')
145
+ if (existing) {
146
+ // 合并证据源
147
+ const seen = new Set(existing.sourceMemoryIds)
148
+ for (const id of c.sourceMemoryIds || []) if (!seen.has(id)) existing.sourceMemoryIds.push(id)
149
+ const seenEp = new Set(existing.sourceEpisodes)
150
+ for (const id of c.sourceEpisodes || []) if (!seenEp.has(id)) existing.sourceEpisodes.push(id)
151
+ void persist()
152
+ return { ok: true, procedure: existing, merged: true }
153
+ }
154
+ const p = {
155
+ procedureId: defaultProcedureId(c.title, now),
156
+ title: c.title,
157
+ stage: 'observed',
158
+ riskLevel: c.riskLevel || 'low',
159
+ requiresApproval: c.riskLevel === 'high' && gates.highRiskRequiresApproval,
160
+ sourceMemoryIds: c.sourceMemoryIds || [],
161
+ sourceEpisodes: c.sourceEpisodes || [],
162
+ preconditions: c.preconditions || [],
163
+ steps: c.steps,
164
+ checks: c.checks || [],
165
+ successCriteria: c.successCriteria || [],
166
+ rollback: c.rollback || [],
167
+ createdAt: now,
168
+ updatedAt: now,
169
+ lastUsedAt: now,
170
+ pinned: false,
171
+ origin: c.origin === 'user' ? 'user' : 'agent',
172
+ evidence: { seen: 0, read: 0, cite: 0, reuse: 0, success: 0, correction: 0, sessions: 0 },
173
+ approved: c.riskLevel !== 'high',
174
+ }
175
+ const pv = validateProcedurePre(p)
176
+ if (!pv.ok) return { ok: false, reason: 'procedure-invalid:' + pv.reason }
177
+ procedures.push(pv.procedure)
178
+ stats.observed++
179
+ void persist()
180
+ return { ok: true, procedure: pv.procedure, merged: false }
181
+ }
182
+
183
+ /**
184
+ * 证据喂入(由调用方在 M5 evidence 落盘后调)。
185
+ * @param {string} procedureId
186
+ * @param {object} ev { kind: 'seen'|'read'|'cite'|'reuse'|'success'|'correction', sessionRef? }
187
+ */
188
+ function addEvidence(procedureId, ev) {
189
+ if (disposed) return { ok: false, reason: 'disposed' }
190
+ const p = procedures.find((x) => x.procedureId === procedureId)
191
+ if (!p) return { ok: false, reason: 'not-found' }
192
+ if (!ev || typeof ev !== 'object' || !['seen', 'read', 'cite', 'reuse', 'success', 'correction'].includes(ev.kind)) {
193
+ return { ok: false, reason: 'bad-evidence' }
194
+ }
195
+ if (p.evidence[ev.kind] !== undefined) p.evidence[ev.kind]++
196
+ // session 去重
197
+ if (ev.sessionRef) {
198
+ if (!p._sessions) p._sessions = new Set()
199
+ p._sessions.add(String(ev.sessionRef))
200
+ p.evidence.sessions = p._sessions.size
201
+ }
202
+ p.updatedAt = nowFn()
203
+ p.lastUsedAt = p.updatedAt // 任何真实证据都算活动(Hermes: last_activity_at 驱动老化)
204
+ void persist()
205
+ return { ok: true, evidence: { ...p.evidence } }
206
+ }
207
+
208
+ /** 活动触点(act.skill 命中注入/用户手动调用时):驱动自动归档老化的时钟。 */
209
+ function touch(procedureId) {
210
+ const p = procedures.find((x) => x.procedureId === procedureId)
211
+ if (!p) return { ok: false, reason: 'not-found' }
212
+ p.lastUsedAt = nowFn()
213
+ void persist()
214
+ return { ok: true, lastUsedAt: p.lastUsedAt }
215
+ }
216
+
217
+ /** 置顶/取消置顶(pinned 豁免自动归档与 agent 改写——Hermes curator 不变量)。 */
218
+ function setPinned(procedureId, v) {
219
+ const p = procedures.find((x) => x.procedureId === procedureId)
220
+ if (!p) return { ok: false, reason: 'not-found' }
221
+ p.pinned = v === true
222
+ p.updatedAt = nowFn()
223
+ void persist()
224
+ return { ok: true, pinned: p.pinned }
225
+ }
226
+
227
+ /** 确定性老化(Hermes apply_automatic_transitions 移植):active 且未 pinned 且
228
+ * lastUsedAt 超过 ARCHIVE_AFTER_DAYS → deprecated(auto-archive-inactive)。
229
+ * 只归档不删除;返回变更计数。 */
230
+ function applyAutomaticTransitions(nowTs) {
231
+ const now = Number.isFinite(nowTs) ? nowTs : nowFn()
232
+ const cutoff = now - PROCEDURE_ARCHIVE_AFTER_DAYS_V1 * 86400000
233
+ const counts = { checked: 0, archived: 0 }
234
+ for (const p of procedures) {
235
+ counts.checked++
236
+ if (p.stage !== 'active' || p.pinned) continue
237
+ const last = Number.isFinite(p.lastUsedAt) ? p.lastUsedAt : (p.updatedAt || p.createdAt)
238
+ if (last < cutoff) {
239
+ p.stage = 'deprecated'
240
+ p.deprecatedAt = now
241
+ p.deprecateReason = 'auto-archive-inactive'
242
+ stats.deprecated++
243
+ counts.archived++
244
+ }
245
+ }
246
+ if (counts.archived) void persist()
247
+ return counts
248
+ }
249
+
250
+ /**
251
+ * 晋升判定(M-04 元代码 promote 逐行)。
252
+ * 返回 { decision: 'promote'|'keep'|'ask', procedure, reasonCodes }
253
+ */
254
+ function promote(procedureId, extraEvidence = {}) {
255
+ if (disposed) return { ok: false, reason: 'disposed' }
256
+ const p = procedures.find((x) => x.procedureId === procedureId)
257
+ if (!p) return { ok: false, reason: 'not-found' }
258
+ if (p.stage === 'deprecated') return { ok: true, decision: 'keep', reasonCodes: ['deprecated'] }
259
+
260
+ const ev = p.evidence
261
+ const reason = []
262
+ // 跨会话多样性
263
+ const diversity = extraEvidence.distinctSessions != null ? extraEvidence.distinctSessions : ev.sessions
264
+ const successCount = extraEvidence.successCount != null ? extraEvidence.successCount : ev.success
265
+ if (diversity < gates.minSessionDiversity) { reason.push('diversity-below-' + gates.minSessionDiversity); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
266
+ if (successCount < gates.minSuccessCount) { reason.push('success-below-' + gates.minSuccessCount); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
267
+ // correction 率
268
+ const total = ev.seen + ev.read + ev.cite + ev.reuse + ev.success + ev.correction
269
+ const corrRate = total > 0 ? ev.correction / total : 0
270
+ if (corrRate > gates.maxCorrectionRate) { reason.push('correction-rate-' + corrRate.toFixed(2)); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
271
+ // contradictions
272
+ if (gates.maxContradictions === 0 && ev.correction > 0) { reason.push('has-correction'); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
273
+ // successCriteria 必须有(元代码: 无 successCriteria → keepCandidate)
274
+ if (!p.successCriteria.length) { reason.push('no-success-criteria'); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
275
+ // 高风险需批准
276
+ if (p.requiresApproval && !p.approved) {
277
+ if (approveFn) {
278
+ const apr = approveFn(p)
279
+ if (apr && apr.approved) p.approved = true
280
+ else { stats.approvalAsked++; reason.push('high-risk-awaiting-approval'); return { ok: true, decision: 'ask', procedure: p, reasonCodes: reason } }
281
+ } else {
282
+ stats.approvalAsked++
283
+ reason.push('high-risk-awaiting-approval')
284
+ return { ok: true, decision: 'ask', procedure: p, reasonCodes: reason }
285
+ }
286
+ }
287
+ // 晋升
288
+ p.stage = 'validated'
289
+ p.updatedAt = nowFn()
290
+ stats.validated++
291
+ void persist()
292
+ return { ok: true, decision: 'promote', procedure: p, reasonCodes: reason }
293
+ }
294
+
295
+ /** 激活: validated → active(可被召回)。 */
296
+ function activate(procedureId) {
297
+ const p = procedures.find((x) => x.procedureId === procedureId)
298
+ if (!p) return { ok: false, reason: 'not-found' }
299
+ if (p.stage !== 'validated') return { ok: false, reason: 'stage-' + p.stage }
300
+ p.stage = 'active'
301
+ p.activatedAt = nowFn()
302
+ stats.activated++
303
+ void persist()
304
+ return { ok: true, procedure: p }
305
+ }
306
+
307
+ /** 降级/禁用: 任何 stage → deprecated(用户手动或 correction 爆表)。 */
308
+ function deprecate(procedureId, reason = 'user-disabled') {
309
+ const p = procedures.find((x) => x.procedureId === procedureId)
310
+ if (!p) return { ok: false, reason: 'not-found' }
311
+ p.stage = 'deprecated'
312
+ p.deprecatedAt = nowFn()
313
+ p.deprecateReason = String(reason)
314
+ stats.deprecated++
315
+ void persist()
316
+ return { ok: true, procedure: p }
317
+ }
318
+
319
+ // ---- 查询 ----
320
+ function query(q = {}) {
321
+ if (disposed) return []
322
+ return procedures
323
+ .filter((p) =>
324
+ (q.stage === undefined || p.stage === q.stage) &&
325
+ (q.riskLevel === undefined || p.riskLevel === q.riskLevel) &&
326
+ (q.title === undefined || p.title.includes(q.title)))
327
+ .map((p) => ({ ...p, evidence: { ...p.evidence } }))
328
+ }
329
+ function activeProcedures() {
330
+ return procedures.filter((p) => p.stage === 'active').map((p) => ({ ...p, evidence: { ...p.evidence } }))
331
+ }
332
+ function get(procedureId) {
333
+ const p = procedures.find((x) => x.procedureId === procedureId)
334
+ return p ? { ...p, evidence: { ...p.evidence } } : null
335
+ }
336
+
337
+ /**
338
+ * ★ 渲染成可注入的 checklist(M6 六级 level 的 checklist 形态)。
339
+ * 输出给 M7 召回系统:相似场景召回 active procedure → 注入 checklist 提示 AI 按固定流程走。
340
+ * 高风险 → 降级为 hint(仅提示"可参考流程",不自动给步骤)。
341
+ */
342
+ function renderChecklist(procedureId) {
343
+ const p = procedures.find((x) => x.procedureId === procedureId)
344
+ if (!p || p.stage !== 'active') return null
345
+ if (p.riskLevel === 'high') {
346
+ return {
347
+ procedureId: p.procedureId, title: p.title, level: 'hint', riskLevel: p.riskLevel,
348
+ text: '[技能提示] 场景匹配「' + p.title + '」(高风险流程,已确认可参考)。如需执行请先向用户确认,再按记忆中的固定步骤操作。',
349
+ }
350
+ }
351
+ const lines = []
352
+ lines.push('[技能] ' + p.title)
353
+ if (p.preconditions.length) lines.push('前置: ' + p.preconditions.join('; '))
354
+ p.steps.forEach(function (s, i) { lines.push((i + 1) + '. ' + s) })
355
+ if (p.checks.length) lines.push('检查: ' + p.checks.join('; '))
356
+ if (p.successCriteria.length) lines.push('完成标准: ' + p.successCriteria.join('; '))
357
+ if (p.rollback.length) lines.push('回滚: ' + p.rollback.join('; '))
358
+ return {
359
+ procedureId: p.procedureId, title: p.title, level: activeLevel, riskLevel: p.riskLevel,
360
+ text: lines.join('\n').slice(0, 2000), // 预算内
361
+ }
362
+ }
363
+
364
+ // ---- 持久化 ----
365
+ function snapshot() {
366
+ return {
367
+ schemaVersion: 1, namespace: 'dsh-auto-memory', policyVersion: PROCEDURE_POLICY_VERSION,
368
+ savedAt: nowFn(),
369
+ procedures: procedures.map((p) => ({ ...p, _sessions: p._sessions ? [...p._sessions] : undefined, evidence: { ...p.evidence } })),
370
+ }
371
+ }
372
+ function restore(data) {
373
+ if (!data || data.schemaVersion !== 1) return { ok: false, reason: 'bad-schema' }
374
+ if (!Array.isArray(data.procedures)) return { ok: false, reason: 'bad-procedures' }
375
+ procedures = []
376
+ for (const p of data.procedures) {
377
+ const v = validateProcedurePre(p)
378
+ if (!v.ok) continue
379
+ if (p._sessions) { p._sessions = new Set(p._sessions); p.evidence.sessions = p._sessions.size }
380
+ procedures.push(v.procedure)
381
+ }
382
+ return { ok: true, restored: procedures.length }
383
+ }
384
+ function clear() {
385
+ procedures = []
386
+ try { io.clear() } catch (_) {}
387
+ return { ok: true }
388
+ }
389
+ function dispose(reason) {
390
+ if (disposed) return
391
+ disposed = true
392
+ try { io.save(snapshot()) } catch (_) {}
393
+ }
394
+ function persist() {
395
+ try { io.save(snapshot()) } catch (_) {}
396
+ }
397
+
398
+ return {
399
+ observe, addEvidence, promote, activate, deprecate,
400
+ touch, setPinned, applyAutomaticTransitions,
401
+ query, activeProcedures, get, renderChecklist,
402
+ snapshot, restore, clear, dispose,
403
+ getStats: () => ({ ...stats }),
404
+ get size() { return procedures.length },
405
+ }
406
+ }