@a9i5k4/dsh-auto-memory 2.5.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +189 -7
  2. package/README.zh-CN.md +189 -7
  3. package/docs/CONTRIBUTORS.html +471 -0
  4. package/docs/FRONTEND-CO-CREATION.md +191 -0
  5. package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
  6. package/docs/HANDOFF-CRITERIA.md +92 -0
  7. package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
  8. package/docs/INTEGRATION-ANALYSIS.md +350 -348
  9. package/docs/PROMO-PROMPT-3.0.md +100 -0
  10. package/docs/USER-GUIDE.en.md +58 -3
  11. package/docs/USER-GUIDE.zh-CN.md +59 -4
  12. package/docs/WHITEPAPER.md +207 -0
  13. package/docs/internal/ACCEPT-35-LIVE.md +143 -0
  14. package/docs/internal/ACCEPTANCE-20260914.md +90 -0
  15. package/docs/internal/ARCH-REVIEW-BRIEF.md +411 -0
  16. package/docs/internal/ARCH-REVIEW-REQUEST.md +201 -0
  17. package/docs/internal/ARCH-REVIEW-ROUND2.md +169 -0
  18. package/docs/internal/ARCH-REVIEW-ROUND3.md +206 -0
  19. package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +397 -0
  20. package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +351 -0
  21. package/docs/internal/ART-DIRECTION-WIREFRAME.md +191 -181
  22. package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +181 -0
  23. package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +314 -0
  24. package/docs/internal/BATTLE-PLAN-20260917.md +871 -0
  25. package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +192 -0
  26. package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +72 -0
  27. package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +131 -0
  28. package/docs/internal/DECISIONS-20260914-SESSION.md +269 -0
  29. package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +219 -0
  30. package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +132 -0
  31. package/docs/internal/FEATURE-INVENTORY.md +531 -0
  32. package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +13 -0
  33. package/docs/internal/G-SERIES-EXECUTION-20260917.md +248 -0
  34. package/docs/internal/G3-DESIGN-20260918.md +82 -0
  35. package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +92 -0
  36. package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +74 -0
  37. package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +352 -0
  38. package/docs/internal/GPT-REVIEW-PROMPT.md +216 -0
  39. package/docs/internal/GROUP-WEBHOOK-SETUP.md +33 -0
  40. package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +309 -0
  41. package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +120 -0
  42. package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +74 -0
  43. package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +175 -0
  44. package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +389 -0
  45. package/docs/internal/ISSUE10-PLAN-20260919.md +254 -0
  46. package/docs/internal/ISSUE10B-FORENSICS-20260919.md +468 -0
  47. package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +150 -0
  48. package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +114 -0
  49. package/docs/internal/KICKOFF-P0.md +254 -0
  50. package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +79 -0
  51. package/docs/internal/MASTER-PLAN-3.0.md +411 -0
  52. package/docs/internal/MEMORY-GOVERNANCE-20260917.md +309 -0
  53. package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +85 -0
  54. package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +222 -0
  55. package/docs/internal/PENDING-FIXES-20260916.md +289 -0
  56. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +705 -0
  57. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +649 -0
  58. package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +225 -0
  59. package/docs/internal/PROGRESS-20260917.md +93 -0
  60. package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +128 -0
  61. package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +163 -0
  62. package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +127 -0
  63. package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +140 -0
  64. package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +138 -0
  65. package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +218 -0
  66. package/docs/internal/RAG-KARPATHY-PROGRAM.md +229 -0
  67. package/docs/internal/REPORT-P0-NIGHTLY.md +212 -0
  68. package/docs/internal/REPORT-P5-ACCEPTANCE.md +31 -0
  69. package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +153 -0
  70. package/docs/internal/RESUME-20260918.md +171 -0
  71. package/docs/internal/RESUME-20260919.md +104 -0
  72. package/docs/internal/REVIEW-WB-GRAPH-SELF.md +81 -0
  73. package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +198 -0
  74. package/docs/internal/ROADMAP-20260917-WEEK.md +439 -0
  75. package/docs/internal/ROADMAP.md +106 -0
  76. package/docs/internal/RUN-P0-NIGHTLY.md +227 -0
  77. package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +185 -0
  78. package/docs/internal/S10-GAP-INVENTORY-20260917.md +239 -0
  79. package/docs/internal/S10-GAPS-PLAIN-20260917.md +125 -0
  80. package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +360 -0
  81. package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +90 -0
  82. package/docs/internal/T6-EXECUTION-20260920.md +130 -0
  83. package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +146 -0
  84. package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +89 -0
  85. package/docs/internal/THESIS-OUTLINE-20260918.md +147 -0
  86. package/docs/internal/THREE-LAYER-CONTRACT.md +219 -0
  87. package/docs/internal/TODO-BACKLOG.md +263 -142
  88. package/docs/internal/TODO-GRAPH.html +715 -0
  89. package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +493 -0
  90. package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +710 -0
  91. package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +710 -0
  92. package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +703 -0
  93. package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +710 -0
  94. package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +715 -0
  95. package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +297 -0
  96. package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +104 -0
  97. package/docs/internal/WB-FORMAT-CONVENTION.md +112 -0
  98. package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +71 -0
  99. package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +56 -0
  100. package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +787 -0
  101. package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +112 -0
  102. package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +230 -0
  103. package/docs/prompts/M8-3-enable-verify.md +49 -49
  104. package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
  105. package/lib/acceptance.js +71 -0
  106. package/lib/activation-host.js +153 -18
  107. package/lib/activation-inbox.js +25 -7
  108. package/lib/board-mode.js +30 -0
  109. package/lib/client.js +1758 -90
  110. package/lib/config-io.js +156 -0
  111. package/lib/context-bridge.js +5 -2
  112. package/lib/context-host.js +86 -15
  113. package/lib/degrade.js +385 -0
  114. package/lib/dsh-home.js +143 -0
  115. package/lib/engine-identity.js +149 -0
  116. package/lib/engine-switch.js +247 -0
  117. package/lib/episodic-store.js +63 -12
  118. package/lib/evidence-store.js +10 -3
  119. package/lib/fact-store.js +22 -3
  120. package/lib/fs-retry.js +46 -0
  121. package/lib/index-sync.js +13 -1
  122. package/lib/index.js +3446 -263
  123. package/lib/intent-clean-safe.js +258 -0
  124. package/lib/intent-clean.js +12 -16
  125. package/lib/l0-extract.js +478 -149
  126. package/lib/l0-index-sync.js +195 -0
  127. package/lib/l0-index.js +349 -239
  128. package/lib/ledger-criteria.js +142 -0
  129. package/lib/m4-corpus.js +8 -2
  130. package/lib/m7-index-sync-host.js +73 -5
  131. package/lib/m7-wire.js +3 -3
  132. package/lib/memory-anchor.js +56 -1
  133. package/lib/memory-envelope.js +257 -0
  134. package/lib/memory-hub.js +138 -13
  135. package/lib/memory-index.js +4 -2
  136. package/lib/memory-mutation.js +246 -0
  137. package/lib/memory-writer.js +204 -24
  138. package/lib/note-status-apply.js +118 -0
  139. package/lib/note-status.js +196 -0
  140. package/lib/procedure-observation.js +48 -0
  141. package/lib/procedure-store.js +118 -20
  142. package/lib/python-setup.js +1 -1
  143. package/lib/python-sidecar-client.js +29 -3
  144. package/lib/recall-fusion.js +83 -12
  145. package/lib/rerank-host.js +160 -0
  146. package/lib/rules-edit.js +159 -0
  147. package/lib/rules-layer.js +261 -0
  148. package/lib/semantic-decide.js +41 -8
  149. package/lib/semantic-js.js +66 -6
  150. package/lib/shadow-host.js +3 -5
  151. package/lib/shadow-retrieval.js +3 -3
  152. package/lib/skill-export-host.js +153 -0
  153. package/lib/skill-export.js +239 -0
  154. package/lib/state-commit.js +245 -0
  155. package/lib/storage-manage.js +6 -0
  156. package/lib/subagent-gc.js +4 -8
  157. package/lib/temporal-parse.js +191 -159
  158. package/lib/tier-layer-inject.js +650 -0
  159. package/lib/tier0-catalog.js +735 -0
  160. package/lib/water-window.js +263 -186
  161. package/lib/wb-contract.js +691 -0
  162. package/lib/wb-sidecar.js +890 -0
  163. package/lib/ws-overview-rank.js +2 -2
  164. package/package.json +1 -1
  165. package/python/m7_embedding_v1.py +5 -5
  166. package/python/worker_semantic_v1.py +17 -6
  167. package/python/worker_v1.py +38 -4
@@ -0,0 +1,239 @@
1
+ /**
2
+ * M9-3 Skill 导出层(procedure → SKILL.md 目录束)。
3
+ *
4
+ * ★ 2026-09-19 用户拍板(三项):
5
+ * ⑪-1 形态:`SKILL.md` + **附上过程中用到的程序**(如 `.py`),且 SKILL.md 里
6
+ * 必须明写:这些程序**只是参考性的** —— 若当前做的事情与之前**根本不同**,
7
+ * 可以用来**迁移**,**不能直接运行**。
8
+ * ⑪-2 时机:**晋升为 `active` 后自动导出**。
9
+ * ⑪-3 目录:**用户级**(软件层面,可跨项目迁移);但导出物**必须标注适用于哪个项目**,
10
+ * 项目不一样时**只作参考,不能直接用**。
11
+ *
12
+ * 本模块是**纯渲染层**(无 IO、无状态),便于直接单测:
13
+ * - `renderSkillMarkdownPre(procedure, opts)` → SKILL.md 文本
14
+ * - `skillDirNamePre(procedure)` → 目录名(稳定、可预测)
15
+ * - `SKILL_USAGE_NOTICE_V1` → 使用约束条款(导出物必须原样包含)
16
+ *
17
+ * 设计纪律(与 procedure 引擎同源):
18
+ * ① **不新增状态源** —— 导出物是 procedure 的**派生物**,不是新的事实来源;
19
+ * ② **不自动执行任何导出的程序** —— 程序是**参考资料**,不是可调用入口;
20
+ * ③ 全部 fail-soft,但**返回可观察结果**(ok/reason),不静默。
21
+ */
22
+
23
+ // ★ T7-b(2026-09-20):纯计算依赖,用于给「纯中文标题」生成稳定的 ASCII 目录名。
24
+ // node 内建模块,不引入外部依赖、不做 IO、不持有状态 —— 与本模块「纯渲染层」定位不冲突。
25
+ import { createHash } from 'node:crypto'
26
+
27
+ /** 导出物的使用约束条款 —— **必须原样出现在每个 SKILL.md 中**(用户 ⑪-1 硬要求)。 */
28
+ export const SKILL_USAGE_NOTICE_V1 = [
29
+ '> **⚠️ 使用约束(必读)**',
30
+ '>',
31
+ '> 本技能由 **dsh-auto-memory** 从一次真实工作过程**自动沉淀**而来,**不是通用最佳实践**。',
32
+ '>',
33
+ '> 1. **附带程序仅供参考**:本目录下的脚本(如 `.py`)是**当时那次工作用过的程序**,',
34
+ '> **不是可直接调用的工具**。它们可能依赖当时的路径、环境变量、数据格式或版本。',
35
+ '> 2. **场景根本不同时 → 只做迁移,不要直接运行**:若当前任务与下方「来源」描述的场景',
36
+ '> **有根本差异**,请把附带的程序**当作思路参考**,按当前场景**重写**,而不是直接执行。',
37
+ '> 3. **跨项目使用须先核对**:本技能的「适用项目」若与当前项目**不一致**,',
38
+ '> **一律只作参考**,不得直接套用其路径、命令与判据。',
39
+ '> 4. **高风险步骤需人工确认**:涉及删除、发布、付费、外部发送等动作,必须先向用户复述确认。',
40
+ ].join('\n')
41
+
42
+ /** SKILL.md 中必须出现的约束锚点(供套件断言,防止被后续改动悄悄删掉)。 */
43
+ export const SKILL_NOTICE_ANCHORS_V1 = Object.freeze([
44
+ '使用约束(必读)',
45
+ '附带程序仅供参考',
46
+ '只做迁移,不要直接运行',
47
+ '跨项目使用须先核对',
48
+ '高风险步骤需人工确认',
49
+ ])
50
+
51
+ const MAX_TITLE_LEN = 80
52
+
53
+ /**
54
+ * 目录名 / `name:` 字段:`mem-skill-<slug>-<procId前12位>` —— 稳定、可预测、不冲突。
55
+ *
56
+ * ★ T7-b(2026-09-20 用户报「所有中文技能都叫 untitled」)修复:
57
+ * **根因**:原实现 `title.replace(/[^a-z0-9]+/g,'-')` 只保留 ASCII ⇒ **纯中文标题 slug 成空串**
58
+ * ⇒ 一律回落字面量 `'untitled'`。后果是每个中文技能都叫 `mem-skill-untitled-<id>`,
59
+ * 用户看到的技能列表名字毫无意义(实测:本机导出的
60
+ * `~/.dsh/skills/mem-skill-untitled-aa2153f33507/SKILL.md`)。
61
+ *
62
+ * **为什么不能用「保留中文」这个修法**:DSH 宿主对 skill name 是**硬校验**,不是建议 ——
63
+ * `SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/`(宿主 `index.js:17`),且
64
+ * `:481` 直接 `throw new Error('loaded skill has invalid name "…"')`。
65
+ * ⇒ 中文进 name 会让**技能加载直接失败**(比 untitled 更糟)。故必须产出 ASCII 安全名。
66
+ *
67
+ * **采用的修法**:slug 为空时,取标题的 **sha1 前 8 位十六进制**做 `t-<hash>` 前缀。
68
+ * - ASCII 安全:`t-1a2b3c4d` 完全符合上面的 SKILL_NAME 正则;
69
+ * - 稳定:同一 title ⇒ 同一 hash ⇒ 目录名不变(自动化重导出不会产生新目录);
70
+ * - 可区分:不同标题 hash 不同,不再出现「一堆 untitled」;
71
+ * - 中文原文**不丢**:仍完整出现在 SKILL.md 的 `# <title>` 与 `description` 里,
72
+ * 用户/模型在技能列表看到的是中文标题,name 只承担「唯一标识」职责。
73
+ *
74
+ * ⚠️ 哈希算法用 node 内建 `createHash('sha1')`(纯计算,不引入 IO/状态),
75
+ * 与 FNV-1a 那种"自造哈希"区分开 —— 本仓有过 sha256Hex 同名异义的教训(#86-1)。
76
+ */
77
+ export function skillDirNamePre(procedure) {
78
+ const id = String((procedure && procedure.procedureId) || '')
79
+ const short = id.replace(/^proc_/, '').slice(0, 12)
80
+ const title = String((procedure && procedure.title) || '')
81
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40)
82
+ // 纯中文(或纯符号)标题:ASCII slug 为空 ⇒ 用标题哈希代替,绝不留 'untitled'
83
+ const stem = slug || ('t-' + createHash('sha1').update(title, 'utf8').digest('hex').slice(0, 8))
84
+ return 'mem-skill-' + stem + '-' + (short || 'noid')
85
+ }
86
+
87
+ /** 一句话摘要(取步骤首条或标题)——用于 SKILL.md 的 description。 */
88
+ /**
89
+ * 一句话摘要 —— 用于 SKILL.md 的 `description:` 字段。
90
+ *
91
+ * ★ T7-b(2026-09-20)修正取值优先级:**title 优先,steps[0] 兜底**(原来是反的)。
92
+ * 为什么反了不行:`description` 是模型在技能目录里看到的那句话,应当回答
93
+ * 「这个技能**是干什么的**」—— title 正是这件事;而 steps[0] 是**第一个动作**
94
+ * (实测导出的描述成了「备份:Copy-Item lib/index.js …」,完全看不出技能用途)。
95
+ * 仅当 title 缺失时才用 steps[0] 兜底(此时有总比没有好)。
96
+ */
97
+ function summaryOfPre(p) {
98
+ const t = String(p.title || '').trim()
99
+ const step = Array.isArray(p.steps) && p.steps.length ? String(p.steps[0]).trim() : ''
100
+ const s = t || step
101
+ return s.length > MAX_TITLE_LEN ? s.slice(0, MAX_TITLE_LEN - 1) + '…' : s
102
+ }
103
+
104
+ /**
105
+ * 渲染 SKILL.md。
106
+ *
107
+ * @param {object} procedure procedure 记录(stage 应为 active)
108
+ * @param {object} opts
109
+ * @param {string} opts.projectName 适用项目名(**必填**,⑪-3)
110
+ * @param {string} opts.projectPath 项目根绝对路径(用于溯源)
111
+ * @param {string[]} opts.programs 附带程序文件名列表(⑪-1,仅列名)
112
+ * @param {string} opts.exportedAt 导出时间(ISO 字符串;缺省由调用方给)
113
+ * @returns {{ok:true, fileName:string, dirName:string, content:string} | {ok:false, reason:string}}
114
+ */
115
+ export function renderSkillMarkdownPre(procedure, opts = {}) {
116
+ const p = procedure || {}
117
+ if (!p.procedureId) return { ok: false, reason: 'no-procedure-id' }
118
+ if (!p.title) return { ok: false, reason: 'no-title' }
119
+ const projectName = String(opts.projectName || '').trim()
120
+ if (!projectName) return { ok: false, reason: 'no-project-name' } // ⑪-3 硬要求:必须标项目
121
+ const projectPath = String(opts.projectPath || '').trim()
122
+ const programs = Array.isArray(opts.programs) ? opts.programs.filter(Boolean).map(String) : []
123
+ const exportedAt = String(opts.exportedAt || '')
124
+
125
+ const L = []
126
+ L.push('---')
127
+ L.push('name: ' + skillDirNamePre(p))
128
+ L.push('description: ' + summaryOfPre(p).replace(/\r?\n/g, ' '))
129
+ L.push('---')
130
+ L.push('')
131
+ L.push('# ' + String(p.title).trim())
132
+ L.push('')
133
+ L.push(SKILL_USAGE_NOTICE_V1)
134
+ L.push('')
135
+
136
+ // ── 来源(⑪-3:必须标注适用于哪个项目)──
137
+ L.push('## 来源')
138
+ L.push('')
139
+ L.push('- **适用项目**:`' + projectName + '`' + (projectPath ? '(`' + projectPath + '`)' : ''))
140
+ L.push('- **沉淀自**:dsh-auto-memory 技能库(procedure `' + p.procedureId + '`)')
141
+ if (p.riskLevel) L.push('- **风险等级**:`' + p.riskLevel + '`' + (p.requiresApproval ? '(需人工批准)' : ''))
142
+ if (exportedAt) L.push('- **导出时间**:' + exportedAt)
143
+ L.push('')
144
+ L.push('> 若你的当前项目**不是** `' + projectName + '`,请**只作参考**,不要直接套用下方路径与命令。')
145
+ L.push('')
146
+
147
+ // ── 适用条件 ──
148
+ if (Array.isArray(p.preconditions) && p.preconditions.length) {
149
+ L.push('## 何时适用')
150
+ L.push('')
151
+ for (const x of p.preconditions) L.push('- ' + String(x))
152
+ L.push('')
153
+ }
154
+
155
+ // ── 步骤 ──
156
+ if (Array.isArray(p.steps) && p.steps.length) {
157
+ L.push('## 步骤')
158
+ L.push('')
159
+ p.steps.forEach((s, i) => L.push(String(i + 1) + '. ' + String(s)))
160
+ L.push('')
161
+ }
162
+
163
+ // ── 检查点 ──
164
+ if (Array.isArray(p.checks) && p.checks.length) {
165
+ L.push('## 检查点')
166
+ L.push('')
167
+ for (const x of p.checks) L.push('- [ ] ' + String(x))
168
+ L.push('')
169
+ }
170
+
171
+ // ── 成功判据 ──
172
+ if (Array.isArray(p.successCriteria) && p.successCriteria.length) {
173
+ L.push('## 成功判据')
174
+ L.push('')
175
+ for (const x of p.successCriteria) L.push('- ' + String(x))
176
+ L.push('')
177
+ }
178
+
179
+ // ── 回滚 ──
180
+ if (Array.isArray(p.rollback) && p.rollback.length) {
181
+ L.push('## 回滚')
182
+ L.push('')
183
+ for (const x of p.rollback) L.push('- ' + String(x))
184
+ L.push('')
185
+ }
186
+
187
+ // ── 附带程序(⑪-1)──
188
+ if (programs.length) {
189
+ L.push('## 附带程序(**参考性**)')
190
+ L.push('')
191
+ L.push('以下文件是**当时那次工作用过的程序**,随本技能一起导出:')
192
+ L.push('')
193
+ for (const f of programs) L.push('- `' + f + '`')
194
+ L.push('')
195
+ L.push('> **不要直接运行**。先读一遍,判断它与当前场景的差异;')
196
+ L.push('> 若场景根本不同,请**按当前场景迁移重写**;确需运行时,先向用户说明并确认。')
197
+ L.push('')
198
+ }
199
+
200
+ // ── 证据 ──
201
+ if (Array.isArray(p.sourceEpisodes) && p.sourceEpisodes.length) {
202
+ L.push('## 依据')
203
+ L.push('')
204
+ L.push('- 来源 episode 数:' + p.sourceEpisodes.length)
205
+ if (p.evidence && typeof p.evidence === 'object') {
206
+ const ev = p.evidence
207
+ const parts = []
208
+ if (Number.isFinite(ev.seen)) parts.push('seen=' + ev.seen)
209
+ if (Number.isFinite(ev.success)) parts.push('success=' + ev.success)
210
+ if (Number.isFinite(ev.correction)) parts.push('correction=' + ev.correction)
211
+ if (parts.length) L.push('- 证据统计:' + parts.join(' / '))
212
+ }
213
+ L.push('')
214
+ }
215
+
216
+ return { ok: true, fileName: 'SKILL.md', dirName: skillDirNamePre(p), content: L.join('\n') + '\n' }
217
+ }
218
+
219
+ /**
220
+ * 导出完整性校验(供套件与调用方共用)。
221
+ * **判据是"用户 ⑪-1/⑪-3 的硬要求"本身**,不是实现细节。
222
+ */
223
+ export function validateSkillMarkdownPre(content, expect = {}) {
224
+ const s = String(content == null ? '' : content)
225
+ const problems = []
226
+ if (!s.trim()) problems.push('empty')
227
+ for (const a of SKILL_NOTICE_ANCHORS_V1) if (!s.includes(a)) problems.push('missing-notice:' + a)
228
+ if (!/^---\r?\n[\s\S]*?\r?\n---/.test(s)) problems.push('no-frontmatter')
229
+ if (!/^name:\s*\S+/m.test(s)) problems.push('no-name')
230
+ if (!/^description:\s*\S+/m.test(s)) problems.push('no-description')
231
+ if (expect.projectName && !s.includes('`' + expect.projectName + '`')) problems.push('no-project-tag')
232
+ // ⑪-1:附带程序必须被运行约束**包裹**,而不是只列个名。
233
+ // ★ 判据必须锚定属于「程序块」的形态:
234
+ // 顶层约束条款里也出现「不要直接运行」四字(后接 `**:`),
235
+ // 只用该四字做判据会**同义反复**(永远为真)——套件 [5] 组已实跑抓到这一版缺陷。
236
+ // 程序块独有形态 = 加粗短语**后紧跟句号**:`**不要直接运行**。`
237
+ if (expect.hasPrograms && !/\*\*不要直接运行\*\*。/.test(s)) problems.push('programs-not-constrained')
238
+ return { ok: problems.length === 0, problems }
239
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * 统一状态提交契约(state_commit_v1)—— P1 主体(2026-09-15)。
3
+ *
4
+ * 依据:`docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md`(已获用户批准)§2.1 / §2.2 / §2.3,
5
+ * `MASTER-PLAN-3.0.md` Phase 1、`TODO-GRAPH.html` 卡 V2-P1。
6
+ *
7
+ * **一句话目标**:把「谁在写、写的什么版本、写完算不算数」收敛成**一个提交边界**。
8
+ *
9
+ * **三条硬规则(本模块是它们的唯一实现点)**:
10
+ * 1. **miv 是内容身份 + 状态清单摘要**(哈希,**不递增、不比较**)。
11
+ * 任何代码不得写 `if (miv > lastMiv)` —— 它不是版本序。
12
+ * 2. **`boardId` 在工作区内稳定、不含 sessionId**:由 `sha256(workspaceKey + '|' + scope)` 派生。
13
+ * 接续后新窗口换 sessionId,若 boardId 跟着变,图就会被当成两块,共享语义直接崩。
14
+ * 3. **必须废弃的口径**:白板 `index.json` 的 `rebuilt_at` **不得当版本序** ——
15
+ * 它是时间戳,重建时间变而内容没变时它会变 ⇒ 用它当版本序会造成**假失效 + 真混版**。
16
+ * 断言 T1-5c 专门钉死这一条。
17
+ *
18
+ * **边界(不在本模块做)**:
19
+ * - 不加长期编辑锁、不按会话分片(卡内明确否决)——并发靠「队列串行 + 边界内比较」。
20
+ * - 不解析白板格式(KICKOFF §3.4:白板线拥有 `parseWhiteboardPre`)。
21
+ * - `atomicReplace` 不是 CAS,本模块不给它加语义;提交校验必须发生在**队列内部**。
22
+ *
23
+ * S9 合规:零 IO、零外部依赖(只用 node:crypto)、纯函数、无网络/无 LLM/无子进程/无 await。
24
+ * UTF-8 无 BOM。
25
+ */
26
+ import { createHash } from 'node:crypto'
27
+
28
+ export const STATE_COMMIT_VERSION_PRE = 'state_commit_v1'
29
+
30
+ /** miv 前缀(与 `shadow-retrieval.js:144` 既有口径一致:`idx_` + first32hex)。 */
31
+ export const MIV_PREFIX_PRE = 'idx_'
32
+
33
+ /**
34
+ * 三种状态的**命名隔离**(卡内要求"三种状态不能混")。
35
+ * 三者**不得互转**:记忆有效状态是"条目还算不算数",任务进度是"活干完没有",
36
+ * 归档位置是"东西放哪儿"。`archived` 一词两义的问题以独立常量解决,**映射由白板线适配器负责**。
37
+ */
38
+ export const MEMORY_STATUS_PRE = Object.freeze(['current', 'superseded', 'retracted'])
39
+ export const TASK_STATE_PRE = Object.freeze(['open', 'done', 'passed'])
40
+ export const ARCHIVE_STATE_PRE = Object.freeze(['active', 'archived'])
41
+
42
+ /** 提交单据必填字段(缺任一 ⇒ fail-closed 拒绝,不猜测)。 */
43
+ export const COMMIT_REQUIRED_V1 = Object.freeze(['workspaceKey', 'boardId', 'txId', 'actor'])
44
+
45
+ /** 冲突/拒绝原因码 → 可读中文。 */
46
+ export const COMMIT_REASONS_V1 = Object.freeze({
47
+ 'not-object': '传入的不是对象',
48
+ 'missing-field': '提交单据缺必填字段',
49
+ 'invalid-writes': 'writes 形状非法(必须是数组)',
50
+ 'digest-mismatch': '提交前摘要不匹配(外部编辑或并发写)',
51
+ 'state-version-mismatch': '状态版本不匹配(外部变更或并发提交)',
52
+ 'actor-invalid': 'actor 形状非法(需 { sessionId, kind })',
53
+ })
54
+
55
+ /** 原因码 → 可读中文(未知码原样返回)。 */
56
+ export function describeCommitReasonPre(code) {
57
+ const k = String(code == null ? '' : code)
58
+ return COMMIT_REASONS_V1[k] || k || '未知原因'
59
+ }
60
+
61
+ function asNonEmptyString(v) {
62
+ if (typeof v !== 'string') return null
63
+ const t = v.trim()
64
+ return t ? t : null
65
+ }
66
+
67
+ function canonicalJson(v) {
68
+ // 稳定键序序列化(避免插入顺序导致同内容不同哈希)
69
+ if (v === null || typeof v !== 'object') return JSON.stringify(v)
70
+ if (Array.isArray(v)) return '[' + v.map(canonicalJson).join(',') + ']'
71
+ const keys = Object.keys(v).sort()
72
+ return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(v[k])).join(',') + '}'
73
+ }
74
+
75
+ function sha256HexPre(text) {
76
+ return createHash('sha256').update(String(text), 'utf8').digest('hex')
77
+ }
78
+
79
+ /**
80
+ * **miv 单源**(P1 步 2):`memoryIndexVersionPre(projection)`。
81
+ *
82
+ * ⚠️ **与既有实现的边界(2026-09-15 施工期核实,必须分清)**:
83
+ * 本函数算的是 **"状态清单摘要"口径**的 miv —— 输入是**规范化投影**
84
+ * (`{records:[{id,status,l0?}], boardCards?}`),用于「同一份快照」的**身份判定**,
85
+ * 以及 pinned 状态进入摘要(状态变了 miv 必变)。
86
+ * 它**不替换**契约 §8 的 `shadow-retrieval.js:145 memoryIndexVersion(sources)` ——
87
+ * 那个吃的是 **source tuples**(scope/sourceRef/epoch/version/fileDigest),是**建索引**侧的真源。
88
+ * 两者**输入不同、用途不同**,不可互相替换;本函数是"提交边界侧"的口径,
89
+ * 且**刻意保持与 §8 相同的前缀与长度**(`idx_` + 32 hex),使二者在外部看来同形。
90
+ *
91
+ * ⚠️ 本函数**不负责**收敛 `index.js:4188 tierCurrentMivPre()`(那处的缓存/指纹语义属 T0-2 已交付内容),
92
+ * P1 不动它 —— 见设计稿 §2.2 边界说明。任何"用本函数替换 tier 自造版"的改动都**不在 P1 范围**。
93
+ *
94
+ * 输入:`{ records: [{id, status, l0?}], boardCards?: [{id, status}], scope? }`
95
+ * 输出:`'idx_' + first32hex(sha256(canonical))`
96
+ *
97
+ * canonical 构成(按 id 升序,换行拼接,无尾随空白):
98
+ * 每条 → `<id>\t<status>\t<contentDigest>`;`contentDigest` = 内容摘要(无内容时取空串)。
99
+ * `boardCards` 若给出,追加在 records 之后(同样排序)—— 白板卡片状态变化也进摘要。
100
+ *
101
+ * ⚠️ **不递增、不比较**。这是哈希身份。`rebuilt_at`(时间戳)**不得**参与本函数,也不得
102
+ * 被任何调用方当作版本序使用 —— 见文件头第 3 条硬规则。
103
+ */
104
+ export function memoryIndexVersionPre(projection) {
105
+ const o = projection && typeof projection === 'object' ? projection : {}
106
+ const records = Array.isArray(o.records) ? o.records : []
107
+ const boardCards = Array.isArray(o.boardCards) ? o.boardCards : []
108
+ const tuples = []
109
+ for (const r of records) {
110
+ if (!r || typeof r !== 'object') continue
111
+ const id = asNonEmptyString(r.id)
112
+ if (!id) continue
113
+ const status = asNonEmptyString(r.status) || ''
114
+ const contentDigest = r.l0 == null ? '' : sha256HexPre(canonicalJson(r.l0)).slice(0, 16)
115
+ tuples.push([id, status, contentDigest])
116
+ }
117
+ for (const c of boardCards) {
118
+ if (!c || typeof c !== 'object') continue
119
+ const id = asNonEmptyString(c.id)
120
+ if (!id) continue
121
+ tuples.push(['board:' + id, asNonEmptyString(c.status) || '', ''])
122
+ }
123
+ tuples.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
124
+ const canonical = tuples.map((t) => t.join('\t')).join('\n')
125
+ return MIV_PREFIX_PRE + sha256HexPre(canonical).slice(0, 32)
126
+ }
127
+
128
+ /**
129
+ * **boardId 派生**(卡内硬约束):工作区内稳定、**不含 sessionId**。
130
+ *
131
+ * ⚠️ 入参**只接受 workspaceKey 与 scope**。刻意不接收 sessionId/agent 对象 ——
132
+ * 从签名上就杜绝"顺手把会话号掺进去"这个错误。
133
+ */
134
+ export function boardIdPre(workspaceKey, scope) {
135
+ const ws = asNonEmptyString(workspaceKey)
136
+ if (!ws) return null
137
+ const sc = asNonEmptyString(scope) || 'Workspace'
138
+ return 'board_' + sha256HexPre(ws + '|' + sc).slice(0, 24)
139
+ }
140
+
141
+ /**
142
+ * 构造并校验提交单据(P1 步 1)。
143
+ *
144
+ * 形状:`{ workspaceKey, boardId, txId, expectedDigest?, expectedStateVersion?,
145
+ * actor: { sessionId, contSeq?, kind }, writes: [{ path, content, expectedDigest? }],
146
+ * stateChanges?: [{ target, from, to }] }`
147
+ *
148
+ * **fail-closed**:缺必填字段 / actor 形状非法 / writes 非数组 ⇒ `{ok:false}`,不猜测、不补默认值。
149
+ *
150
+ * **向后兼容(T1-8)**:`expectedStateVersion` 与 `expectedDigest` 均为**可选**;
151
+ * 不传时下游行为必须与本契约引入前**逐字节一致**(该校验由调用方在队列内执行)。
152
+ */
153
+ export function buildStateCommitPre(input) {
154
+ const o = input && typeof input === 'object' ? input : null
155
+ if (!o) return { ok: false, reason: 'not-object', detail: describeCommitReasonPre('not-object') }
156
+
157
+ const missing = []
158
+ const workspaceKey = asNonEmptyString(o.workspaceKey)
159
+ if (!workspaceKey) missing.push('workspaceKey')
160
+ const boardId = asNonEmptyString(o.boardId)
161
+ if (!boardId) missing.push('boardId')
162
+ const txId = asNonEmptyString(o.txId)
163
+ if (!txId) missing.push('txId')
164
+
165
+ const actor = o.actor && typeof o.actor === 'object' ? o.actor : null
166
+ const actorSession = actor ? asNonEmptyString(actor.sessionId) : null
167
+ const actorKind = actor ? asNonEmptyString(actor.kind) : null
168
+ if (!actor || !actorSession || !actorKind) {
169
+ return { ok: false, reason: 'actor-invalid', detail: describeCommitReasonPre('actor-invalid'), missing: actor ? ['actor.sessionId', 'actor.kind'] : ['actor'] }
170
+ }
171
+ if (missing.length) {
172
+ return { ok: false, reason: 'missing-field', detail: describeCommitReasonPre('missing-field'), missing }
173
+ }
174
+
175
+ if (o.writes !== undefined && !Array.isArray(o.writes)) {
176
+ return { ok: false, reason: 'invalid-writes', detail: describeCommitReasonPre('invalid-writes') }
177
+ }
178
+
179
+ const commit = {
180
+ schemaVersion: STATE_COMMIT_VERSION_PRE,
181
+ workspaceKey,
182
+ boardId,
183
+ txId,
184
+ actor: {
185
+ sessionId: actorSession,
186
+ contSeq: Number.isFinite(Number(actor.contSeq)) ? Number(actor.contSeq) : undefined,
187
+ kind: actorKind,
188
+ },
189
+ writes: Array.isArray(o.writes) ? o.writes : [],
190
+ stateChanges: Array.isArray(o.stateChanges) ? o.stateChanges : [],
191
+ }
192
+ // 可选字段:仅在显式给出时带上(保证"不传 = 行为不变")
193
+ if (o.expectedDigest != null) commit.expectedDigest = String(o.expectedDigest)
194
+ if (o.expectedStateVersion != null) commit.expectedStateVersion = String(o.expectedStateVersion)
195
+ return { ok: true, commit }
196
+ }
197
+
198
+ /**
199
+ * 冲突描述(卡内 T1-7C 要求:拒绝信息必须带**当前版本 + 冲突目标**,不静默覆盖)。
200
+ * 纯函数:只组装信息,不做 IO。
201
+ */
202
+ export function commitConflictPre(commit, observed) {
203
+ const c = commit && typeof commit === 'object' ? commit : {}
204
+ const ob = observed && typeof observed === 'object' ? observed : {}
205
+ const expected = c.expectedStateVersion != null ? String(c.expectedStateVersion) : (c.expectedDigest != null ? String(c.expectedDigest) : '(none)')
206
+ const actual = ob.stateVersion != null ? String(ob.stateVersion) : (ob.fileDigest != null ? String(ob.fileDigest) : '(unknown)')
207
+ const target = ob.target != null ? String(ob.target) : (Array.isArray(c.writes) && c.writes[0] && c.writes[0].path ? String(c.writes[0].path) : '(unknown)')
208
+ const who = c.actor && c.actor.sessionId ? String(c.actor.sessionId) : '(unknown)'
209
+ const contSeq = c.actor && c.actor.contSeq != null ? '@' + String(c.actor.contSeq) : ''
210
+ const which = ob.kind === 'state-version' ? 'state-version-mismatch' : 'digest-mismatch'
211
+ return {
212
+ ok: false,
213
+ reason: which,
214
+ detail: describeCommitReasonPre(which),
215
+ // 可见冲突三要素:期望值 / 实测值 / 冲突目标(+ 谁)
216
+ expected,
217
+ observed: actual,
218
+ target,
219
+ txId: c.txId != null ? String(c.txId) : '',
220
+ boardId: c.boardId != null ? String(c.boardId) : '',
221
+ actor: who + contSeq,
222
+ text: '[提交被拒] tx=' + (c.txId != null ? String(c.txId) : '(none)')
223
+ + ' 目标=' + target
224
+ + ' 期望=' + expected + ' 实测=' + actual
225
+ + ' 冲突方=' + who + contSeq,
226
+ }
227
+ }
228
+
229
+ /** 成功回执:提交边界返回的可观测凭据(含 miv —— 快照侧据此判"同一份")。 */
230
+ export function commitReceiptPre(commit, result) {
231
+ const c = commit && typeof commit === 'object' ? commit : {}
232
+ const r = result && typeof result === 'object' ? result : {}
233
+ return {
234
+ schemaVersion: STATE_COMMIT_VERSION_PRE,
235
+ ok: true,
236
+ txId: c.txId != null ? String(c.txId) : '',
237
+ boardId: c.boardId != null ? String(c.boardId) : '',
238
+ workspaceKey: c.workspaceKey != null ? String(c.workspaceKey) : '',
239
+ digest: r.digest != null ? String(r.digest) : '',
240
+ stateVersion: r.stateVersion != null ? String(r.stateVersion) : '',
241
+ miv: r.miv != null ? String(r.miv) : '',
242
+ at: Number.isFinite(Number(r.at)) ? Number(r.at) : Date.now(),
243
+ actor: c.actor && c.actor.sessionId ? String(c.actor.sessionId) : '',
244
+ }
245
+ }
@@ -116,6 +116,12 @@ export function createStorageManagerPre(opts = {}) {
116
116
  /** 读取既有 sidecar(尽力而为):用于 rebuildSidecar 继承 epoch/version,避免无谓的 epoch 漂移。 */
117
117
  function readSidecarPrev(file) {
118
118
  try {
119
+ // ★ issue #55 修复(2026-09-18,2026-09-19 补回):函数体内**自行活读** docStore。
120
+ // 旧写法直接引用闭包里的 `docStore`,但该名只在 `repair`/`deleteMemory` 内部局部声明
121
+ // ⇒ 本函数作用域内**未定义** ⇒ 每次调用抛 ReferenceError ⇒ 被下方 `catch (_)` 吞掉
122
+ // ⇒ **恒返回 null** ⇒ `rebuildSidecar` 拿不到 prev ⇒ epoch 漂移、fresh 被翻成 stale。
123
+ // 必须走 `docStoreOf()` 工厂活读(getter 语义:memoryAnchorEnabled 后续生效也能拿到)。
124
+ const docStore = docStoreOf()
119
125
  if (!docStore || typeof docStore.sidecarPath !== 'function') return null
120
126
  const sp = docStore.sidecarPath(file)
121
127
  if (!sp) return null
@@ -23,6 +23,7 @@ import { readdir, stat, rename, mkdir, access } from 'node:fs/promises'
23
23
  import { readFile } from 'node:fs/promises'
24
24
  import { zstdDecompressSync } from 'node:zlib'
25
25
  import path from 'node:path'
26
+ import { retryRename } from './fs-retry.js'
26
27
 
27
28
  /** 本插件 spawn 的子代理 label 前缀(runSubagent 的 label 参数)。 */
28
29
  export const PLUGIN_LABEL_PREFIX = 'auto-memory-'
@@ -258,14 +259,9 @@ export async function recycleSessions(opts = {}) {
258
259
  if (await exists(destDir)) { out.skipped.push({ sid: c.sid, reason: 'backup-exists' }); continue }
259
260
  if (!apply) { out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir, dryRun: true }); out.bytes += c.sizeBytes; continue }
260
261
  await mkdir(path.dirname(destDir), { recursive: true })
261
- // Windows 下 rename 对被占用目录抛 EPERM(宿主/杀软句柄未释放)→ 短退避重试,仍失败则跳过等下次
262
- let renamed = false
263
- let lastErr = null
264
- for (const delay of [0, 250, 800, 2000]) {
265
- if (delay) await new Promise((r) => setTimeout(r, delay))
266
- try { await rename(c.dir, destDir); renamed = true; break } catch (e) { lastErr = e }
267
- }
268
- if (!renamed) throw lastErr
262
+ // Windows 下 rename 对被占用目录抛 EPERM(宿主/杀软句柄未释放)⇒ 复用统一的有界退避重试
263
+ // (issue #48:退避语义此前在本文件与 index.js 各写一份,口径漂移风险高)。
264
+ await retryRename(c.dir, destDir, { fs: { rename }, delays: [0, 250, 800, 2000] })
269
265
  out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir })
270
266
  out.bytes += c.sizeBytes
271
267
  if (projcacheRoot) {