@a9i5k4/dsh-auto-memory 3.0.1 → 3.1.0
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/README.md +13 -8
- package/README.zh-CN.md +13 -8
- package/docs/HANDBOOK.md +88 -52
- package/docs/USER-GUIDE.en.md +9 -9
- package/docs/USER-GUIDE.zh-CN.md +9 -9
- package/docs/screenshots/promo/promo-0-banner-v4.png +0 -0
- package/docs/screenshots/promo/promo-1b-auto-recall.png +0 -0
- package/lib/activation-host.js +6 -1
- package/lib/client.js +819 -272
- package/lib/context-host.js +7 -1
- package/lib/episodic-store.js +90 -16
- package/lib/fact-store.js +463 -41
- package/lib/hub-io.js +217 -0
- package/lib/index.js +224 -45
- package/lib/intent-clean-safe.js +1 -1
- package/lib/memory-hub.js +37 -5
- package/lib/note-status.js +9 -1
- package/lib/procedure-store.js +252 -31
- package/lib/procedure-switch.js +38 -0
- package/lib/python-sidecar-client.js +285 -8
- package/package.json +6 -2
- package/docs/internal/ACCEPT-35-LIVE.md +0 -143
- package/docs/internal/ACCEPTANCE-20260914.md +0 -90
- package/docs/internal/ARCH-REVIEW-BRIEF.md +0 -411
- package/docs/internal/ARCH-REVIEW-REQUEST.md +0 -201
- package/docs/internal/ARCH-REVIEW-ROUND2.md +0 -169
- package/docs/internal/ARCH-REVIEW-ROUND3.md +0 -206
- package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +0 -397
- package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +0 -351
- package/docs/internal/ART-DIRECTION-WIREFRAME.md +0 -191
- package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +0 -181
- package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +0 -314
- package/docs/internal/BATTLE-PLAN-20260917.md +0 -871
- package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +0 -192
- package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +0 -72
- package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +0 -131
- package/docs/internal/CUA-VISION-FIX-NOTES.md +0 -78
- package/docs/internal/DECISIONS-20260914-SESSION.md +0 -269
- package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +0 -292
- package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +0 -219
- package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +0 -132
- package/docs/internal/FEATURE-INVENTORY.md +0 -531
- package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +0 -13
- package/docs/internal/G-SERIES-EXECUTION-20260917.md +0 -248
- package/docs/internal/G3-DESIGN-20260918.md +0 -82
- package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +0 -92
- package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +0 -74
- package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +0 -352
- package/docs/internal/GPT-REVIEW-PROMPT.md +0 -216
- package/docs/internal/GROUP-DIGEST-SETUP.md +0 -62
- package/docs/internal/GROUP-LISTENER-SETUP.md +0 -49
- package/docs/internal/GROUP-WEBHOOK-SETUP.md +0 -93
- package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +0 -309
- package/docs/internal/HANDOFF-TO-ZCODE.md +0 -168
- package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +0 -120
- package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +0 -74
- package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +0 -175
- package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +0 -389
- package/docs/internal/ISSUE10-PLAN-20260919.md +0 -254
- package/docs/internal/ISSUE10B-FORENSICS-20260919.md +0 -468
- package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +0 -150
- package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +0 -114
- package/docs/internal/KICKOFF-P0.md +0 -254
- package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +0 -79
- package/docs/internal/MASTER-PLAN-3.0.md +0 -411
- package/docs/internal/MEMORY-GOVERNANCE-20260917.md +0 -309
- package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +0 -85
- package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +0 -222
- package/docs/internal/NEXT-VERSION-TODO.md +0 -95
- package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +0 -80
- package/docs/internal/PENDING-FIXES-20260916.md +0 -289
- package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +0 -705
- package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +0 -649
- package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +0 -225
- package/docs/internal/PROGRESS-20260917.md +0 -93
- package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +0 -128
- package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +0 -163
- package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +0 -127
- package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +0 -140
- package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +0 -138
- package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +0 -218
- package/docs/internal/RAG-KARPATHY-PROGRAM.md +0 -229
- package/docs/internal/RELEASE-PROCESS.md +0 -99
- package/docs/internal/REPORT-P0-NIGHTLY.md +0 -212
- package/docs/internal/REPORT-P5-ACCEPTANCE.md +0 -31
- package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +0 -153
- package/docs/internal/RESUME-20260918.md +0 -171
- package/docs/internal/RESUME-20260919.md +0 -104
- package/docs/internal/REVIEW-WB-GRAPH-SELF.md +0 -81
- package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +0 -198
- package/docs/internal/ROADMAP-20260917-WEEK.md +0 -439
- package/docs/internal/ROADMAP.md +0 -106
- package/docs/internal/RUN-P0-NIGHTLY.md +0 -227
- package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +0 -185
- package/docs/internal/S10-GAP-INVENTORY-20260917.md +0 -239
- package/docs/internal/S10-GAPS-PLAIN-20260917.md +0 -125
- package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +0 -360
- package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +0 -90
- package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +0 -261
- package/docs/internal/T6-EXECUTION-20260920.md +0 -130
- package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +0 -146
- package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +0 -89
- package/docs/internal/THESIS-OUTLINE-20260918.md +0 -147
- package/docs/internal/THREE-LAYER-CONTRACT.md +0 -219
- package/docs/internal/TODO-BACKLOG.md +0 -263
- package/docs/internal/TODO-GRAPH.html +0 -715
- package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +0 -493
- package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +0 -703
- package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +0 -715
- package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +0 -297
- package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +0 -104
- package/docs/internal/WB-FORMAT-CONVENTION.md +0 -112
- package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +0 -71
- package/docs/internal/WB-GRAPH-INTEGRATION-PLAN.md +0 -386
- package/docs/internal/WB-GRAPH-RESEARCH-BRIEF.md +0 -118
- package/docs/internal/WB-GRAPH-RESEARCH-EXTERNAL.md +0 -228
- package/docs/internal/WB-GRAPH-RESEARCH-LOCAL.md +0 -190
- package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +0 -56
- package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +0 -787
- package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +0 -112
- package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +0 -230
package/lib/memory-hub.js
CHANGED
|
@@ -70,7 +70,7 @@ export function createMemoryHubPre(opts = {}) {
|
|
|
70
70
|
: (createProcedureStorePre ? createProcedureStorePre({ now: nowFn }) : null)
|
|
71
71
|
|
|
72
72
|
const cfg = opts.config || {}
|
|
73
|
-
const stats = { judgedRows: 0, consumedSemantic: 0, consumedProcedure: 0, consumedEpisodic: 0, skipped: 0, checklistsRendered: 0 }
|
|
73
|
+
const stats = { judgedRows: 0, consumedSemantic: 0, consumedProcedure: 0, consumedEpisodic: 0, rejectedSemantic: 0, rejectedProcedure: 0, skipped: 0, checklistsRendered: 0 }
|
|
74
74
|
|
|
75
75
|
// ---- 1) 消费 judgement-shadow(喂给对应 store) ----
|
|
76
76
|
function ingestJudgement(row) {
|
|
@@ -87,16 +87,36 @@ export function createMemoryHubPre(opts = {}) {
|
|
|
87
87
|
: factCandidateFromRow(row)
|
|
88
88
|
if (!cand) { stats.skipped++; return { skipped: true, reason: 'not-fact' } }
|
|
89
89
|
const r = cand._suggestion === 'supersede_suggest' ? stores.facts.supersede(cand) : stores.facts.upsert(cand)
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// ★2026-09-22(B-3 对齐):事实入口判据上线后 `upsert` **会**返回 `ok:false`(不是事实陈述 /
|
|
91
|
+
// 缺字段等)。旧实现在此**无条件** `consumedSemantic++` 并回 `{consumed:'semantic'}`,
|
|
92
|
+
// 于是宿主日志(index.js hubFeedTick 里 `if (r.consumed) fed++`)与 `?action=feed` 端点
|
|
93
|
+
// 都把「被拒收」读成「已喂进去」 ⇒ 诊断面在说谎。
|
|
94
|
+
// 本次只对齐口径(与下方 episodic 分支「如实区分」的既有纪律一致):**不改任何入库行为**,
|
|
95
|
+
// 拒收照样拒收,只是不再记成 consumed,并把 code/message 一路透传给人看。
|
|
96
|
+
const accepted = r.ok !== false
|
|
97
|
+
if (accepted) stats.consumedSemantic++
|
|
98
|
+
else { stats.rejectedSemantic++; stats.skipped++ }
|
|
99
|
+
if (!accepted) {
|
|
100
|
+
const out = { skipped: true, reason: 'rejected-semantic', outcome: r.outcome, accepted: false }
|
|
101
|
+
if (r.code) out.code = r.code
|
|
102
|
+
if (r.message) out.message = r.message
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
return { consumed: 'semantic', outcome: r.outcome, accepted: true }
|
|
92
106
|
}
|
|
93
107
|
if (layer === 'procedure' && stores.procedures) {
|
|
94
108
|
// 从 row 构造 procedure candidate(行内可能只有 sourceIds + 摘要)
|
|
95
109
|
const cand = procedureCandidateFromRow(row)
|
|
96
110
|
if (!cand) { stats.skipped++; return { skipped: true, reason: 'not-procedure' } }
|
|
97
111
|
const r = stores.procedures.observe(cand)
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
// ★2026-09-22:与上面 semantic 分支同一口径 —— 观察被拒(非法命令/已弃用等)不算「已消费」,
|
|
113
|
+
// 否则诊断面同样会把拒收读成成功。不改 observe 的任何入库行为。
|
|
114
|
+
const accepted = r.ok !== false
|
|
115
|
+
if (accepted) stats.consumedProcedure++
|
|
116
|
+
else { stats.rejectedProcedure++; stats.skipped++ }
|
|
117
|
+
return accepted
|
|
118
|
+
? { consumed: 'procedure', outcome: 'observed', accepted: true }
|
|
119
|
+
: { skipped: true, reason: 'rejected-procedure', outcome: r.reason, accepted: false }
|
|
100
120
|
}
|
|
101
121
|
if (layer === 'episodic' && stores.episodic) {
|
|
102
122
|
// ★ 2026-09-19 上游 PR #77 / issue #63 同步落地(**P0 数据丢失**):
|
|
@@ -243,6 +263,18 @@ export function createMemoryHubPre(opts = {}) {
|
|
|
243
263
|
promotion = stores.procedures.evaluatePromotion
|
|
244
264
|
? stores.procedures.evaluatePromotion(p.procedureId)
|
|
245
265
|
: null
|
|
266
|
+
// ★A/B(2026-09-22):把「人工能否越过」并进**同一条**只读投影。
|
|
267
|
+
// 界面据此决定要不要给「强制晋升(人工)」按钮:只有统计门拦时才给,
|
|
268
|
+
// 结构门拦时不给(避免再造一个「点了没反应」的假通道 —— 那正是 A-9 收掉旧按钮的原因)。
|
|
269
|
+
// 注意:**不改 evaluatePromotion 的签名与函数体**(smoke-test-r2-promotion-pre.mjs
|
|
270
|
+
// 用字面正则锁签名、用源码切片锁门限顺序);promotionOverrideView 只读、只复用其结论。
|
|
271
|
+
if (promotion && stores.procedures.promotionOverrideView) {
|
|
272
|
+
const ov = stores.procedures.promotionOverrideView(p.procedureId)
|
|
273
|
+
if (ov && ov.ok !== false) {
|
|
274
|
+
promotion.overridable = ov.overridable === true
|
|
275
|
+
promotion.gateKind = ov.gateKind
|
|
276
|
+
}
|
|
277
|
+
}
|
|
246
278
|
} catch (_) { promotion = null }
|
|
247
279
|
return {
|
|
248
280
|
procedureId: p.procedureId, title: p.title, stage: p.stage, riskLevel: p.riskLevel,
|
package/lib/note-status.js
CHANGED
|
@@ -141,7 +141,15 @@ export function statusOfBodyPre(body) {
|
|
|
141
141
|
export function stripStatusLinePre(body) {
|
|
142
142
|
const s = String(body == null ? '' : body)
|
|
143
143
|
if (!s.includes(NOTE_STATUS_OPEN_V1)) return s
|
|
144
|
-
|
|
144
|
+
// ★ 2026-09-21 bugfix(A-4):原实现 `split(/\r?\n/).join('\n')` 会把**所有** CRLF 改写成裸 LF,
|
|
145
|
+
// 而本函数的调用方 `applyStatusToRecordPre` 作用于**目标条目正文**并回写整篇文档 ⇒
|
|
146
|
+
// 每次 supersedes/retract/restore 都破坏该条目的行尾,与「除状态行外逐字节保持原样」的
|
|
147
|
+
// 文档承诺相反;本仓文件全 CRLF,还会连带 recordDigest 漂移、GUI diff 放大。
|
|
148
|
+
// 修法:**先探测原文换行风格,再按同风格重组** —— 保持行尾不变(纯函数语义也随之更严格)。
|
|
149
|
+
const nl = s.includes('\r\n') ? '\r\n' : '\n'
|
|
150
|
+
const kept = s.split(/\r?\n/).filter((ln) => parseStatusLinePre(ln) === null)
|
|
151
|
+
// 注意:split 后不含行尾符,join 时统一用探测到的风格;末尾空行会被自然保留(与 split 前条目数一致)
|
|
152
|
+
return kept.join(nl)
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
/**
|
package/lib/procedure-store.js
CHANGED
|
@@ -7,15 +7,43 @@ import { isObservationOnlyPre, normalizeProcedureObservationPre, matchingProcedu
|
|
|
7
7
|
* ★ 这是「记忆中枢」的核心价值层:把多次使用的固定流程固化成 skill,
|
|
8
8
|
* 让 M7 记忆召回系统在相似场景自动召回这些 skill,AI 按固定流程执行。
|
|
9
9
|
*
|
|
10
|
-
* 生命周期状态机(M-04
|
|
11
|
-
* observed →
|
|
10
|
+
* 生命周期状态机(M-04 元代码逐行落地;P2-3 收敛为两态跳变):
|
|
11
|
+
* observed → validated → active → deprecated
|
|
12
|
+
* 注:原枚举含 `candidate`,但全仓**无任何** stage='candidate' 写入点(observe 只产 observed、
|
|
13
|
+
* promote 直接 observed→validated)⇒ 它是死状态,已从枚举移除;旧快照若含该 stage,
|
|
14
|
+
* restore() 按"两态跳变"语义归入 observed(不静默丢弃)。
|
|
12
15
|
*
|
|
13
16
|
* 晋升规则(promote, 元代码原样):
|
|
14
17
|
* - sessionDiversity ≥ 3(跨独立会话)且 successCount ≥ 2 → 可晋升
|
|
15
|
-
* - correctionRate > MAX_CORRECTION 或 contradictions → 保持
|
|
16
|
-
* - 无 successCriteria 或 无 cross-session evidence → 保持
|
|
17
|
-
* - riskLevel=high 且未用户批准 → 返回 {
|
|
18
|
+
* - correctionRate > MAX_CORRECTION 或 contradictions → 保持 observed
|
|
19
|
+
* - 无 successCriteria 或 无 cross-session evidence → 保持 observed
|
|
20
|
+
* - riskLevel=high 且未用户批准 → 返回 {decision:'ask'} 等待批准
|
|
18
21
|
* - 一次成功或三次重复均不足以证明可靠(元代码铁律)
|
|
22
|
+
*
|
|
23
|
+
* ★ ok 与 promoted 的语义差异(P2-5,2026-09-21):
|
|
24
|
+
* promote() 的 `ok` 只表示**这次调用被正常处理**(未抛错、未命中 disposed/not-found),
|
|
25
|
+
* **拒绝晋升同样是 ok:true**;要判断"到底晋升了没有"必须读 `promoted`(boolean)。
|
|
26
|
+
* 加这个字段的原因:宿主 lib/index.js:10934-10936 记录过该坑 —— 旧日志只记 `r.ok`,
|
|
27
|
+
* 于是"拒绝晋升"与"晋升成功"都写成 ok:true 的假阳性,事后完全看不出是否晋升。
|
|
28
|
+
* `ok` 语义保持不变(兼容既有调用方);新增调用方请以 `promoted` 为准。
|
|
29
|
+
*
|
|
30
|
+
* ★ 落盘失败可见(A-8,2026-09-21):
|
|
31
|
+
* 凡走 persist() 的写操作,返回值都带 `persisted:boolean`;为 false 时附
|
|
32
|
+
* `persistReason` 与 `persistFailures`,getStats() 另透出 `persistFailures`/`lastPersistError`。
|
|
33
|
+
* 此前 persist() 用**空 catch** 吞掉落盘失败 ⇒ 写盘坏了无从察觉(本模块零依赖、不打日志,
|
|
34
|
+
* 故只能靠返回值与 stats 留痕)。
|
|
35
|
+
*
|
|
36
|
+
* ★ 人工批准只解一道门(B-1,2026-09-21):
|
|
37
|
+
* 高风险条目入库时 `approved = c.riskLevel !== 'high'` ⇒ 恒为 false,而在此之前**全仓没有任何代码**
|
|
38
|
+
* 能把它置真 ⇒ promote() 走到高风险门只能返回 `{decision:'ask', reasonCodes:['high-risk-awaiting-approval']}`,
|
|
39
|
+
* 高风险技能永远晋升不了(死路)。`approve(procedureId, by)` 就是那条唯一的解门通路:
|
|
40
|
+
* · 它**只**置 `approved/approvedAt/approvedBy/updatedAt` 并落盘 —— 不改 stage、不触发晋升;
|
|
41
|
+
* · 它**不跳过** promote() 的另外五道结构门(已弃用短路 / 纯观察行短路 / 缺 successCriteria /
|
|
42
|
+
* 纠正率超限 / 存在纠正记录):批准后仍可能被这些门拒绝,必须再调 promote() 才知道最终结果;
|
|
43
|
+
* · 所以 `approved:true` 的语义是**"高风险这道门不再拦"**,不是"这条已晋升/已激活"。
|
|
44
|
+
* 返回的 `reason` 一律取自**冻结的机器码集合**(`not-found` / `stage-deprecated` / `disposed`),
|
|
45
|
+
* 人类可读文案由消费端(前端/工具文案层)做映射 —— 码值稳定可枚举,不随文案变化。
|
|
46
|
+
}
|
|
19
47
|
*
|
|
20
48
|
* 渐进激活(M6 六级 level):
|
|
21
49
|
* index → hint → excerpt → checklist → resource → full
|
|
@@ -37,13 +65,14 @@ export const PROCEDURE_POLICY_VERSION = 'procedure_store_v1'
|
|
|
37
65
|
export const PROCEDURE_ID_PREFIX = 'proc_'
|
|
38
66
|
export const PROCEDURE_ID_RE = /^proc_[0-9a-f]{32}$/
|
|
39
67
|
|
|
40
|
-
/** 阶段枚举(M-04 元代码)。
|
|
41
|
-
|
|
68
|
+
/** 阶段枚举(M-04 元代码)。P2-3:移除死状态 'candidate' —— 全仓无写入点,
|
|
69
|
+
* observe 只产 observed、promote 直接 observed→validated(两态跳变)。 */
|
|
70
|
+
export const PROCEDURE_STAGES_V1 = Object.freeze(['observed', 'validated', 'active', 'deprecated'])
|
|
42
71
|
|
|
43
72
|
/** 自动归档老化(Hermes curator 借鉴,2026-08-28):active 技能 lastUsedAt 超过
|
|
44
73
|
* PROCEDURE_ARCHIVE_AFTER_DAYS_V1 天未使用且未 pinned → 自动 deprecated
|
|
45
74
|
* (reason=auto-archive-inactive;deprecated 可恢复,永不物理删除)。
|
|
46
|
-
* observed/
|
|
75
|
+
* observed/validated 不老化——它们由晋升门槛时间控(P2-3 后已无 candidate 态)。 */
|
|
47
76
|
export const PROCEDURE_ARCHIVE_AFTER_DAYS_V1 = 90
|
|
48
77
|
|
|
49
78
|
/** 风险等级枚举。 */
|
|
@@ -89,6 +118,20 @@ export function validateProcedurePre(p) {
|
|
|
89
118
|
if (p.lastUsedAt !== undefined && (typeof p.lastUsedAt !== 'number' || !Number.isFinite(p.lastUsedAt))) q.push('lastUsedAt')
|
|
90
119
|
if (p.pinned !== undefined && typeof p.pinned !== 'boolean') q.push('pinned')
|
|
91
120
|
if (p.origin !== undefined && !['agent', 'user'].includes(p.origin)) q.push('origin')
|
|
121
|
+
// ★A-6(2026-09-21):evidence 形状校验。此前清单里**没有 evidence** ⇒
|
|
122
|
+
// ① 缺键时 promote() 的 `ev.seen + ev.read + …` 得 NaN,`NaN > cap` 为 false **静默放行**;
|
|
123
|
+
// ② evidence 整个缺失时 addEvidence 抛 TypeError,被宿主 lib/index.js:7644 空 catch 吞掉。
|
|
124
|
+
// 兼容策略:**存在即须为对象且各计数为有限数;缺失不 reject**(旧快照本就无此字段),
|
|
125
|
+
// 由 restore()/addEvidence() 用 normalizeEvidencePre 补零 —— 比"拒绝入袋"更安全(不丢条目)。
|
|
126
|
+
if (p.evidence !== undefined) {
|
|
127
|
+
if (!p.evidence || typeof p.evidence !== 'object' || Array.isArray(p.evidence)) q.push('evidence')
|
|
128
|
+
else {
|
|
129
|
+
for (const k of ['seen', 'read', 'cite', 'reuse', 'success', 'correction', 'sessions']) {
|
|
130
|
+
const vv = p.evidence[k]
|
|
131
|
+
if (vv !== undefined && (typeof vv !== 'number' || !Number.isFinite(vv))) q.push('evidence.' + k)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
92
135
|
if (q.length) return { ok: false, reason: 'invalid:' + q.join(',') }
|
|
93
136
|
return { ok: true, procedure: p }
|
|
94
137
|
}
|
|
@@ -130,7 +173,28 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
130
173
|
|
|
131
174
|
let procedures = [] // 全部 procedure(各 stage)
|
|
132
175
|
let disposed = false
|
|
133
|
-
|
|
176
|
+
// P2-3:删除死字段 stats.candidates —— 全仓无 stage='candidate' 写入点,该计数永远为 0。
|
|
177
|
+
// B-1(2026-09-21):approved = **真的写下去**的人工批准次数(只统计 approve() 的写入分支;
|
|
178
|
+
// notRequired / alreadyApproved 是幂等空操作,不计数 —— 它们没有新增任何批准留痕)。
|
|
179
|
+
const stats = { observed: 0, validated: 0, activated: 0, deprecated: 0, approvalAsked: 0, approved: 0 }
|
|
180
|
+
// A-8:落盘失败痕迹(本模块零依赖不打日志,故只能留在这里供 getStats() 透出)。
|
|
181
|
+
let persistFailures = 0
|
|
182
|
+
let lastPersistError = null
|
|
183
|
+
|
|
184
|
+
/** 证据计数键与回填函数(A-6)。放模块级以免每次调用重建闭包。 */
|
|
185
|
+
function emptyEvidencePre() {
|
|
186
|
+
return { seen: 0, read: 0, cite: 0, reuse: 0, success: 0, correction: 0, sessions: 0 }
|
|
187
|
+
}
|
|
188
|
+
function normalizeEvidencePre(ev) {
|
|
189
|
+
const out = emptyEvidencePre()
|
|
190
|
+
if (ev && typeof ev === 'object' && !Array.isArray(ev)) {
|
|
191
|
+
for (const k of Object.keys(out)) {
|
|
192
|
+
const v = ev[k]
|
|
193
|
+
if (typeof v === 'number' && Number.isFinite(v)) out[k] = v
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out
|
|
197
|
+
}
|
|
134
198
|
|
|
135
199
|
function defaultProcedureId(title, createdAt) {
|
|
136
200
|
const h = createHash('sha256').update(['procedure-v1', String(title), String(createdAt)].join('\u0000')).digest('hex')
|
|
@@ -203,6 +267,10 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
203
267
|
if (!ev || typeof ev !== 'object' || !['seen', 'read', 'cite', 'reuse', 'success', 'correction'].includes(ev.kind)) {
|
|
204
268
|
return { ok: false, reason: 'bad-evidence' }
|
|
205
269
|
}
|
|
270
|
+
// ★A-6(2026-09-21)收尾:自增前必须先规范化 —— 旧快照/异常来源可能缺键,
|
|
271
|
+
// 裸 `p.evidence[ev.kind]++` 会得 NaN(静默污染 promote 的门限算术)或抛 TypeError
|
|
272
|
+
// (被宿主 lib/index.js:7644 空 catch 吞掉 ⇒ 表现为「证据永远不累积」且无留痕)。
|
|
273
|
+
p.evidence = normalizeEvidencePre(p.evidence)
|
|
206
274
|
if (p.evidence[ev.kind] !== undefined) p.evidence[ev.kind]++
|
|
207
275
|
// session 去重
|
|
208
276
|
if (ev.sessionRef) {
|
|
@@ -279,46 +347,49 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
279
347
|
* 并**留痕**:`p.authorizedBy` + reasonCodes 追加 `model-authorized`,供审计与前端展示。
|
|
280
348
|
*/
|
|
281
349
|
function promote(procedureId, extraEvidence = {}, opts = {}) {
|
|
282
|
-
if (disposed) return { ok: false, reason: 'disposed' }
|
|
350
|
+
if (disposed) return { ok: false, promoted: false, reason: 'disposed' }
|
|
283
351
|
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
284
|
-
if (!p) return { ok: false, reason: 'not-found' }
|
|
285
|
-
if (p.stage === 'deprecated') return { ok: true, decision: 'keep', reasonCodes: ['deprecated'] }
|
|
352
|
+
if (!p) return { ok: false, promoted: false, reason: 'not-found' }
|
|
353
|
+
if (p.stage === 'deprecated') return { ok: true, promoted: false, decision: 'keep', reasonCodes: ['deprecated'] }
|
|
286
354
|
// issue #30:纯 episode 观察行没有 successCriteria,本来就无法晋升;
|
|
287
355
|
// 显式短路并给出 `observation-only` 原因码,而不是让它落到下面报 `no-success-criteria`
|
|
288
356
|
// (后者会让使用者误以为"缺判据、补上就能晋升",其实是这行**结构上**不该晋升)。
|
|
289
357
|
// ★ T4:此短路**不受授权影响** —— 结构上不该晋升的行,模型授权也不放行。
|
|
290
|
-
if (isObservationOnlyPre(p)) return { ok: true, decision: 'keep', procedure: p, reasonCodes: ['observation-only'] }
|
|
358
|
+
if (isObservationOnlyPre(p)) return { ok: true, promoted: false, decision: 'keep', procedure: p, reasonCodes: ['observation-only'] }
|
|
291
359
|
|
|
292
360
|
const ev = p.evidence
|
|
293
361
|
const reason = []
|
|
294
362
|
const authorizedBy = opts && opts.authorizedBy ? String(opts.authorizedBy) : ''
|
|
295
|
-
|
|
363
|
+
// ★A/B(2026-09-22):授权来源分人留痕 —— 人工越权与模型越权必须在审计面可区分。
|
|
364
|
+
// 注意:此行的形态变化会让 r2 的「源码级原因码提取」不再捕获它(正则要求引号紧跟括号),
|
|
365
|
+
// 这正是期望的:它本来就属 AUTH_ONLY 集合、需被排除在门限顺序比对之外。
|
|
366
|
+
if (authorizedBy) reason.push(authorizedBy === 'user' ? 'user-authorized' : 'model-authorized')
|
|
296
367
|
// 跨会话多样性
|
|
297
368
|
const diversity = extraEvidence.distinctSessions != null ? extraEvidence.distinctSessions : ev.sessions
|
|
298
369
|
const successCount = extraEvidence.successCount != null ? extraEvidence.successCount : ev.success
|
|
299
370
|
// ★ T4:两条**统计证据门**在模型授权下跳过(见上方函数注释);未授权时行为逐字节不变。
|
|
300
371
|
if (!authorizedBy) {
|
|
301
|
-
if (diversity < gates.minSessionDiversity) { reason.push('diversity-below-' + gates.minSessionDiversity); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
302
|
-
if (successCount < gates.minSuccessCount) { reason.push('success-below-' + gates.minSuccessCount); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
372
|
+
if (diversity < gates.minSessionDiversity) { reason.push('diversity-below-' + gates.minSessionDiversity); return { ok: true, promoted: false, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
373
|
+
if (successCount < gates.minSuccessCount) { reason.push('success-below-' + gates.minSuccessCount); return { ok: true, promoted: false, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
303
374
|
}
|
|
304
375
|
// correction 率
|
|
305
376
|
const total = ev.seen + ev.read + ev.cite + ev.reuse + ev.success + ev.correction
|
|
306
377
|
const corrRate = total > 0 ? ev.correction / total : 0
|
|
307
|
-
if (corrRate > gates.maxCorrectionRate) { reason.push('correction-rate-' + corrRate.toFixed(2)); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
378
|
+
if (corrRate > gates.maxCorrectionRate) { reason.push('correction-rate-' + corrRate.toFixed(2)); return { ok: true, promoted: false, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
308
379
|
// contradictions
|
|
309
|
-
if (gates.maxContradictions === 0 && ev.correction > 0) { reason.push('has-correction'); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
380
|
+
if (gates.maxContradictions === 0 && ev.correction > 0) { reason.push('has-correction'); return { ok: true, promoted: false, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
310
381
|
// successCriteria 必须有(元代码: 无 successCriteria → keepCandidate)
|
|
311
|
-
if (!p.successCriteria.length) { reason.push('no-success-criteria'); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
382
|
+
if (!p.successCriteria.length) { reason.push('no-success-criteria'); return { ok: true, promoted: false, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
312
383
|
// 高风险需批准
|
|
313
384
|
if (p.requiresApproval && !p.approved) {
|
|
314
385
|
if (approveFn) {
|
|
315
386
|
const apr = approveFn(p)
|
|
316
387
|
if (apr && apr.approved) p.approved = true
|
|
317
|
-
else { stats.approvalAsked++; reason.push('high-risk-awaiting-approval'); return { ok: true, decision: 'ask', procedure: p, reasonCodes: reason } }
|
|
388
|
+
else { stats.approvalAsked++; reason.push('high-risk-awaiting-approval'); return { ok: true, promoted: false, decision: 'ask', procedure: p, reasonCodes: reason } }
|
|
318
389
|
} else {
|
|
319
390
|
stats.approvalAsked++
|
|
320
391
|
reason.push('high-risk-awaiting-approval')
|
|
321
|
-
return { ok: true, decision: 'ask', procedure: p, reasonCodes: reason }
|
|
392
|
+
return { ok: true, promoted: false, decision: 'ask', procedure: p, reasonCodes: reason }
|
|
322
393
|
}
|
|
323
394
|
}
|
|
324
395
|
// 晋升
|
|
@@ -329,7 +400,7 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
329
400
|
if (authorizedBy) p.authorizedBy = authorizedBy
|
|
330
401
|
stats.validated++
|
|
331
402
|
void persist()
|
|
332
|
-
return { ok: true, decision: 'promote', procedure: p, reasonCodes: reason }
|
|
403
|
+
return { ok: true, promoted: true, decision: 'promote', procedure: p, reasonCodes: reason }
|
|
333
404
|
}
|
|
334
405
|
|
|
335
406
|
/**
|
|
@@ -386,6 +457,54 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
386
457
|
return { ok: true, decision: 'promote', reasonCodes: reason, detail: { diversity, successCount } }
|
|
387
458
|
}
|
|
388
459
|
|
|
460
|
+
/**
|
|
461
|
+
* ★A/B(2026-09-22):**人工可越权性**只读投影 —— 回答「这道 keep 人能不能强制越过」。
|
|
462
|
+
*
|
|
463
|
+
* 为什么单独一个函数、而不是给 evaluatePromotion 加参数:
|
|
464
|
+
* · `smoke-test-r2-promotion-pre.mjs` 用**字面正则**锁住了 `evaluatePromotion(procedureId)` 的签名,
|
|
465
|
+
* 并用**源码切片**比对它与 `promote()` 的 reason.push 序列;动它的签名或函数体会同时打红两条守卫。
|
|
466
|
+
* · 更要紧的是「不重复实现门限」:本函数**不重写任何一道门**,而是复用 evaluatePromotion 的结论,
|
|
467
|
+
* 只判断「拦它的原因码里是否**只有**那两道统计门」。
|
|
468
|
+
*
|
|
469
|
+
* 语义依据(与 `promote()` 逐条对齐,见 :368 `if (!authorizedBy) {`):
|
|
470
|
+
* · 被授权时**只**跳过 `diversity-below-*` 与 `success-below-*` 两道统计门;
|
|
471
|
+
* · 结构门(deprecated / observation-only / correction-rate-* / has-correction /
|
|
472
|
+
* no-success-criteria / high-risk-awaiting-approval)**授权也拦**
|
|
473
|
+
* ⇒ 只要原因码里出现其中任何一个,就**不可越权**(界面上不该给强制按钮)。
|
|
474
|
+
*
|
|
475
|
+
* 纯只读:只调 evaluatePromotion,不写盘、不改状态、不动统计。异常一律 fail-soft 返回不可越权。
|
|
476
|
+
*
|
|
477
|
+
* @param {string} procedureId
|
|
478
|
+
* @returns {{ok:boolean, decision?:string, overridable:boolean, reasonCodes?:string[], gateKind?:string}}
|
|
479
|
+
* `gateKind`:`promote`(已可晋升,无需越权)/ `statistical`(仅统计门拦,可人工越权)/
|
|
480
|
+
* `structural`(结构门拦,人工也不放行)/ `unknown`(投影失败)
|
|
481
|
+
*/
|
|
482
|
+
function promotionOverrideView(procedureId) {
|
|
483
|
+
try {
|
|
484
|
+
const view = evaluatePromotion(procedureId)
|
|
485
|
+
if (!view || view.ok === false) return { ok: false, overridable: false, gateKind: 'unknown' }
|
|
486
|
+
if (view.decision === 'promote') {
|
|
487
|
+
return { ok: true, decision: 'promote', overridable: false, gateKind: 'promote', reasonCodes: view.reasonCodes || [] }
|
|
488
|
+
}
|
|
489
|
+
if (view.decision === 'ask') {
|
|
490
|
+
// 高风险待批准:本来就有人工通路(approve),不该再给"强制晋升"。
|
|
491
|
+
return { ok: true, decision: 'ask', overridable: false, gateKind: 'structural', reasonCodes: view.reasonCodes || [] }
|
|
492
|
+
}
|
|
493
|
+
const codes = Array.isArray(view.reasonCodes) ? view.reasonCodes : []
|
|
494
|
+
const isStat = (c) => /^diversity-below-/.test(c) || /^success-below-/.test(c)
|
|
495
|
+
const stat = codes.length > 0 && codes.every(isStat)
|
|
496
|
+
return {
|
|
497
|
+
ok: true,
|
|
498
|
+
decision: 'keep',
|
|
499
|
+
overridable: stat,
|
|
500
|
+
gateKind: stat ? 'statistical' : 'structural',
|
|
501
|
+
reasonCodes: codes,
|
|
502
|
+
}
|
|
503
|
+
} catch (_) {
|
|
504
|
+
return { ok: false, overridable: false, gateKind: 'unknown' }
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
389
508
|
/** 激活: validated → active(可被召回)。 */
|
|
390
509
|
function activate(procedureId) {
|
|
391
510
|
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
@@ -398,6 +517,56 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
398
517
|
return { ok: true, procedure: p }
|
|
399
518
|
}
|
|
400
519
|
|
|
520
|
+
/**
|
|
521
|
+
* ★B-1(2026-09-21):**人工批准原语** —— 只解「高风险需人工确认」这一道门。
|
|
522
|
+
*
|
|
523
|
+
* 为什么需要它(取证):高风险条目入库时 `approved = c.riskLevel !== 'high'`(observe, :235)
|
|
524
|
+
* 恒为 false,而在本函数之前**全仓没有任何代码能把它置真**:promote() 走到高风险门只会返回
|
|
525
|
+
* `{decision:'ask', reasonCodes:['high-risk-awaiting-approval']}`(宿主建 store 未传 opts.approve,
|
|
526
|
+
* 路由白名单也没有 approve 动作)⇒ 高风险技能的晋升是**死路**。本函数就是那条唯一通路。
|
|
527
|
+
*
|
|
528
|
+
* 边界(**只有一道门**;文件头同名小节是同一条约定):
|
|
529
|
+
* · 只置 `approved/approvedAt/approvedBy/updatedAt` 并落盘 —— **不改 stage、不触发晋升、不写 authorizedBy**;
|
|
530
|
+
* · **不跳过** promote() 的另外五道结构门(已弃用短路 / 纯观察行短路 / 缺 successCriteria /
|
|
531
|
+
* 纠正率超限 / 存在纠正记录)⇒ 批准之后仍可能被这些门拒绝,必须再调 promote() 才知道结果;
|
|
532
|
+
* · 所以返回里的 `approved:true` 意思是**"高风险这道门不再拦"**,不是"这条已晋升/已激活"。
|
|
533
|
+
* 前端要展示"已批准、待晋升"时请读 `procedure.approved`,不要拿它当 stage 用。
|
|
534
|
+
*
|
|
535
|
+
* 幂等:重复调用不重复写盘,也不覆盖首次的 approvedAt/approvedBy(批准留痕不可被后续调用改写)。
|
|
536
|
+
* 失败码一一取自**冻结的机器码集合**(`not-found` / `stage-deprecated` / `disposed`);人类可读
|
|
537
|
+
* 文案由消费端(前端 / 工具文案层)映射 —— 码值稳定可枚举,不随文案措辞变化。
|
|
538
|
+
*
|
|
539
|
+
* @param {string} procedureId
|
|
540
|
+
* @param {string} [by='user'] 批准者标识(用户手动=user;宿主/模型代批可传其它值,原样记入 approvedBy)
|
|
541
|
+
* @returns {{ok:boolean, approved:boolean, reason?:string, notRequired?:boolean, alreadyApproved?:boolean, persisted?:boolean, procedure?:object}}
|
|
542
|
+
*/
|
|
543
|
+
function approve(procedureId, by = 'user') {
|
|
544
|
+
if (disposed) return { ok: false, reason: 'disposed' }
|
|
545
|
+
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
546
|
+
if (!p) return { ok: false, reason: 'not-found' }
|
|
547
|
+
// 已弃用:更强的短路,先判 —— 弃用条目不该被"批准"(批准不改变它的命运,只可能造出误导性留痕)。
|
|
548
|
+
// 冻结码 'stage-deprecated' 与 activate()/deprecate() 的 'stage-' + stage 同形,前端可统一映射。
|
|
549
|
+
if (p.stage === 'deprecated') return { ok: false, reason: 'stage-deprecated', procedure: p }
|
|
550
|
+
// 本就不需要批准(requiresApproval 为 false,含旧快照缺该字段的行 —— 判据与 promote() 一致):
|
|
551
|
+
// **纯只读返回**:不写盘、不动状态。没有门可解,写 approved 只会伪造一条"有人批过"的留痕。
|
|
552
|
+
if (!p.requiresApproval) return { ok: true, approved: true, notRequired: true, procedure: p }
|
|
553
|
+
// 幂等:已批准过 —— 不重复写盘,也不覆盖首次的 approvedAt/approvedBy。
|
|
554
|
+
if (p.approved) return { ok: true, approved: true, alreadyApproved: true, procedure: p }
|
|
555
|
+
const now = nowFn()
|
|
556
|
+
p.approved = true
|
|
557
|
+
p.approvedAt = now
|
|
558
|
+
p.approvedBy = String(by)
|
|
559
|
+
p.updatedAt = now
|
|
560
|
+
stats.approved++
|
|
561
|
+
const r = persist()
|
|
562
|
+
// A-8 同口径:落盘失败**不静默** —— ok 仍为 true(内存里这道门确实开了),但把失败痕迹透出,
|
|
563
|
+
// 由调用方决定是否上报(persisted:false 时附 persistReason/persistFailures)。
|
|
564
|
+
// 键序与冻结契约一致(ok/approved/persisted/procedure),附加字段只出现在失败时。
|
|
565
|
+
const out = { ok: true, approved: true, persisted: r.ok, procedure: p }
|
|
566
|
+
if (!r.ok) { out.persistReason = r.reason; out.persistFailures = r.persistFailures }
|
|
567
|
+
return out
|
|
568
|
+
}
|
|
569
|
+
|
|
401
570
|
/** 降级/禁用: 任何 stage → deprecated(用户手动或 correction 爆表)。 */
|
|
402
571
|
function deprecate(procedureId, reason = 'user-disabled') {
|
|
403
572
|
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
@@ -433,6 +602,23 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
433
602
|
* 输出给 M7 召回系统:相似场景召回 active procedure → 注入 checklist 提示 AI 按固定流程走。
|
|
434
603
|
* 高风险 → 降级为 hint(仅提示"可参考流程",不自动给步骤)。
|
|
435
604
|
*/
|
|
605
|
+
/**
|
|
606
|
+
* ★P2-7(2026-09-21):checklist 行 → 注入文本,**超长截断必须可见**。
|
|
607
|
+
*
|
|
608
|
+
* 旧实现 `lines.join('\n').slice(0, 2000)` 在超长时**静默**丢掉尾部 ——
|
|
609
|
+
* 而注入文本的尾部恰好是「检查 / 完成标准 / 回滚」,模型据此判断不了
|
|
610
|
+
* "这条技能是否被砍过",也就无从知道该不该照做。本仓既有口径是
|
|
611
|
+
* 「裁剪永远可见」(见 memory-envelope-pre 的同类处理),此处对齐。
|
|
612
|
+
*/
|
|
613
|
+
function renderChecklistTextPre(lines) {
|
|
614
|
+
const full = lines.join('\n')
|
|
615
|
+
const CAP = 2000
|
|
616
|
+
if (full.length <= CAP) return full
|
|
617
|
+
const droppedChars = full.length - CAP
|
|
618
|
+
return full.slice(0, CAP)
|
|
619
|
+
+ '\n…(清单超长已截断:尾部 ' + droppedChars + ' 字符未展开,可能含检查/完成标准/回滚,共 ' + lines.length + ' 段)'
|
|
620
|
+
}
|
|
621
|
+
|
|
436
622
|
function renderChecklist(procedureId) {
|
|
437
623
|
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
438
624
|
if (!p || p.stage !== 'active') return null
|
|
@@ -451,7 +637,7 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
451
637
|
if (p.rollback.length) lines.push('回滚: ' + p.rollback.join('; '))
|
|
452
638
|
return {
|
|
453
639
|
procedureId: p.procedureId, title: p.title, level: activeLevel, riskLevel: p.riskLevel,
|
|
454
|
-
text: lines
|
|
640
|
+
text: renderChecklistTextPre(lines), // ★P2-7(2026-09-21):超长截断必须可见(此前静默砍掉尾部)
|
|
455
641
|
}
|
|
456
642
|
}
|
|
457
643
|
|
|
@@ -468,37 +654,72 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
468
654
|
if (!Array.isArray(data.procedures)) return { ok: false, reason: 'bad-procedures' }
|
|
469
655
|
procedures = []
|
|
470
656
|
for (const p of data.procedures) {
|
|
471
|
-
|
|
657
|
+
// P2-3:旧快照可能含已移除的 'candidate' 态 —— 按"两态跳变"语义归入 observed(不丢条目)。
|
|
658
|
+
const migrated = p && p.stage === 'candidate' ? { ...p, stage: 'observed' } : p
|
|
659
|
+
const v = validateProcedurePre(migrated)
|
|
472
660
|
if (!v.ok) continue
|
|
473
|
-
|
|
661
|
+
// A-6:evidence 回填为有限数(缺键/脏值按 0)——否则 promote 的门限算术会得 NaN 而静默放行。
|
|
662
|
+
v.procedure.evidence = normalizeEvidencePre(v.procedure.evidence)
|
|
663
|
+
if (migrated._sessions) {
|
|
664
|
+
v.procedure._sessions = new Set(migrated._sessions)
|
|
665
|
+
v.procedure.evidence.sessions = v.procedure._sessions.size
|
|
666
|
+
}
|
|
474
667
|
// issue #30:旧快照里没有 observationOnly 字段,恢复时按同一判据补齐
|
|
475
668
|
// (纯增量元数据迁移:不改身份/文本/风险/证据/阶段)。
|
|
476
669
|
procedures.push(normalizeProcedureObservationPre(v.procedure))
|
|
477
670
|
}
|
|
671
|
+
// ★B-1(2026-09-21):stats.approved 是本轮新增计数,旧快照(以及本模块自己的快照 ——
|
|
672
|
+
// snapshot() 不含 stats 字段)里都没有这个键。还原后它**必须仍是有限数**:getStats() 是
|
|
673
|
+
// 前端计数面板的数据源,透出 undefined 会让「已批准 N 条」凭空消失,而各消费方的兜底
|
|
674
|
+
// 写法又不一致。有快照值就用快照值(数字/数字串都收),没有或脏值则保留当前值兜底 ——
|
|
675
|
+
// 不无脑清零:restore() 并不还原其它统计(observed/validated/...),单清一个反而更不一致。
|
|
676
|
+
stats.approved = Number(data.stats && data.stats.approved) || Number(stats.approved) || 0
|
|
478
677
|
return { ok: true, restored: procedures.length }
|
|
479
678
|
}
|
|
679
|
+
/** 清空(失败不再静默:io.clear 抛错时返回 ok:false + error)。 */
|
|
480
680
|
function clear() {
|
|
481
681
|
procedures = []
|
|
482
|
-
try { io.clear() } catch (
|
|
682
|
+
try { if (typeof io.clear === 'function') io.clear() } catch (e) {
|
|
683
|
+
return { ok: false, reason: 'io-clear-failed', error: String((e && e.message) || e) }
|
|
684
|
+
}
|
|
483
685
|
return { ok: true }
|
|
484
686
|
}
|
|
687
|
+
/** 处置:落盘一次(与 persist 同口径返回,失败可见)。保留 reason 形参以兼容宿主既有调用。 */
|
|
485
688
|
function dispose(reason) {
|
|
486
|
-
if (disposed) return
|
|
689
|
+
if (disposed) return { ok: true, alreadyDisposed: true, persisted: true }
|
|
487
690
|
disposed = true
|
|
488
|
-
|
|
691
|
+
const r = persist()
|
|
692
|
+
if (reason) r.disposeReason = String(reason)
|
|
693
|
+
return r
|
|
489
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* ★A-8(2026-09-21):落盘并**返回结果**。此前实现是 `try { io.save(...) } catch (_) {}`,
|
|
697
|
+
* 落盘失败被**完全吞掉** ⇒ 记忆只在内存里、重启即丢,而调用方与统计都看不出异常。
|
|
698
|
+
* 现在:失败仍不抛(保持既有容错语义),但①计数 persistFailures ②记 lastPersistError
|
|
699
|
+
* ③把 { persisted:false, ... } 透传给调用它的写操作返回值,由调用方决定是否上报。
|
|
700
|
+
*/
|
|
490
701
|
function persist() {
|
|
491
|
-
try {
|
|
702
|
+
try {
|
|
703
|
+
io.save(snapshot())
|
|
704
|
+
lastPersistError = null
|
|
705
|
+
return { ok: true, persisted: true }
|
|
706
|
+
} catch (e) {
|
|
707
|
+
persistFailures++
|
|
708
|
+
lastPersistError = String((e && e.message) || e)
|
|
709
|
+
return { ok: false, persisted: false, reason: 'persist-failed', error: lastPersistError, persistFailures }
|
|
710
|
+
}
|
|
492
711
|
}
|
|
493
712
|
|
|
494
713
|
return {
|
|
495
714
|
// ★R2:只读判定投影(供 overview() 展示「为什么不能晋升」)
|
|
496
715
|
evaluatePromotion,
|
|
497
|
-
|
|
716
|
+
promotionOverrideView,
|
|
717
|
+
observe, addEvidence, promote, activate, deprecate, approve,
|
|
498
718
|
touch, setPinned, applyAutomaticTransitions,
|
|
499
719
|
query, activeProcedures, get, renderChecklist,
|
|
500
720
|
snapshot, restore, clear, dispose,
|
|
501
|
-
|
|
721
|
+
// A-8:persistFailures/lastPersistError 一并透出(落盘健康度可观察)
|
|
722
|
+
getStats: () => ({ ...stats, persistFailures, lastPersistError }),
|
|
502
723
|
get size() { return procedures.length },
|
|
503
724
|
}
|
|
504
725
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ★ B-2 修复(2026-09-22):**procedure 技能生效总闸的语义纠正与兼容层**。
|
|
3
|
+
*
|
|
4
|
+
* 背景(代码取证):
|
|
5
|
+
* - 旧键 `procedurePromotionEnabled` 的名字/文案是「技能固化与晋升」(client.js:5023),
|
|
6
|
+
* 但它的**唯一两个消费者**都与"晋升"无关:
|
|
7
|
+
* · context-host.js:537 → `skillEnabled = memoryHubEnabled && procedurePromotionEnabled !== false`(决定技能是否**注入上下文**)
|
|
8
|
+
* · activation-host.js:330 → `if (procedurePromotionEnabled === false) return null`(决定**主动唤起**这条臂开不开)
|
|
9
|
+
* 真正的"晋升"由 store 的门限(procedure-store.js)与路由动作决定,与该键无关。
|
|
10
|
+
* - 它默认 `false`(index.js:546)⇒ **新用户装完即"技能永不生效",且没有任何提示**。
|
|
11
|
+
* - 用户硬规则:「单一开关不得顺带改变其他功能的行为」——本键正是反例。
|
|
12
|
+
*
|
|
13
|
+
* 纠正方案(**不新增死开关**):
|
|
14
|
+
* - 新增 `procedureInjectEnabled`(默认 **true**)作为语义正确的总闸,供上面两个消费者读取。
|
|
15
|
+
* - 旧键保留为**兼容别名**:新键缺省时按旧键取值,两键都缺省时取 `true`。
|
|
16
|
+
* ⇒ 老用户显式关掉过总闸的,行为不翻面(尊重既有选择);从未设置过的,技能开始正常生效。
|
|
17
|
+
* - **不建 `procedureAutoPromoteEnabled`**:全仓无任何自动晋升代码路径
|
|
18
|
+
* (`applyAutomaticTransitions` 只导出、零调用 —— 见 BUGLIST P2-4),建了它就是死配置。
|
|
19
|
+
* "固化"由既有 `hubMechanicalProcedureFeedEnabled`(T10)管,"晋升"由门限 + 路由动作管。
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ 本文件是并行开发的**接口契约**:其他文件只允许通过本模块解析该开关,
|
|
22
|
+
* 不得各自 inline 一份判断(否则又会出现口径漂移)。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** 解析「技能注入/唤起总闸」。@param {object} cfg 插件 config(engine.config) @returns {boolean} */
|
|
26
|
+
export function resolveProcedureInjectEnabledPre(cfg) {
|
|
27
|
+
const c = cfg || {}
|
|
28
|
+
if (typeof c.procedureInjectEnabled === 'boolean') return c.procedureInjectEnabled
|
|
29
|
+
// 兼容别名:旧键语义 = 总闸(见文件头取证),保留其显式选择
|
|
30
|
+
if (typeof c.procedurePromotionEnabled === 'boolean') return c.procedurePromotionEnabled
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 是否仍在使用已弃用的旧键(供设置页/诊断提示"建议迁移")。 */
|
|
35
|
+
export function usesLegacyProcedureSwitchPre(cfg) {
|
|
36
|
+
const c = cfg || {}
|
|
37
|
+
return typeof c.procedureInjectEnabled !== 'boolean' && typeof c.procedurePromotionEnabled === 'boolean'
|
|
38
|
+
}
|