add-coder 0.3.34 → 0.3.35

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 (147) hide show
  1. package/dist/index.js +21 -3
  2. package/package.json +3 -3
  3. package/templates/.add-coder-src-hash.json +144 -90
  4. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -88
  5. package/templates/adapters/claude/hooks/notification.mjs +0 -4
  6. package/templates/adapters/claude/hooks/permission-denied.mjs +0 -4
  7. package/templates/adapters/claude/hooks/permission-gate.mjs +0 -4
  8. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -5
  9. package/templates/adapters/claude/hooks/pre-compact.mjs +0 -4
  10. package/templates/adapters/claude/hooks/pre-tool-use.mjs +0 -4
  11. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -4
  12. package/templates/adapters/claude/hooks/session-end.mjs +0 -4
  13. package/templates/adapters/claude/hooks/session-start.mjs +65 -5
  14. package/templates/adapters/claude/hooks/stop-check.mjs +6 -87
  15. package/templates/adapters/claude/hooks/stop-failure.mjs +0 -4
  16. package/templates/adapters/claude/hooks/subagent-guard.mjs +0 -4
  17. package/templates/adapters/claude/hooks/subagent-stop.mjs +0 -4
  18. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -88
  19. package/templates/adapters/codex/hooks/notification.mjs +0 -4
  20. package/templates/adapters/codex/hooks/permission-gate.mjs +0 -4
  21. package/templates/adapters/codex/hooks/post-tool-failure.mjs +0 -4
  22. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -5
  23. package/templates/adapters/codex/hooks/pre-compact.mjs +0 -4
  24. package/templates/adapters/codex/hooks/pre-tool-use.mjs +0 -4
  25. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -4
  26. package/templates/adapters/codex/hooks/session-end.mjs +0 -4
  27. package/templates/adapters/codex/hooks/session-start.mjs +65 -5
  28. package/templates/adapters/codex/hooks/stop-check.mjs +6 -87
  29. package/templates/adapters/codex/hooks/subagent-guard.mjs +0 -4
  30. package/templates/adapters/codex/hooks/subagent-stop.mjs +0 -4
  31. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -88
  32. package/templates/adapters/qoder/hooks/notification.mjs +0 -4
  33. package/templates/adapters/qoder/hooks/permission-gate.mjs +0 -4
  34. package/templates/adapters/qoder/hooks/post-tool-failure.mjs +0 -4
  35. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -5
  36. package/templates/adapters/qoder/hooks/pre-compact.mjs +0 -4
  37. package/templates/adapters/qoder/hooks/pre-tool-use.mjs +0 -4
  38. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -4
  39. package/templates/adapters/qoder/hooks/review-checklist.mjs +0 -4
  40. package/templates/adapters/qoder/hooks/session-end.mjs +0 -4
  41. package/templates/adapters/qoder/hooks/session-start.mjs +67 -5
  42. package/templates/adapters/qoder/hooks/stop-check.mjs +6 -87
  43. package/templates/adapters/qoder/hooks/subagent-guard.mjs +0 -4
  44. package/templates/adapters/qoder/hooks/subagent-stop.mjs +0 -4
  45. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -88
  46. package/templates/adapters/trae/hooks/notification.mjs +0 -4
  47. package/templates/adapters/trae/hooks/permission-gate.mjs +0 -4
  48. package/templates/adapters/trae/hooks/post-tool-failure.mjs +0 -4
  49. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -5
  50. package/templates/adapters/trae/hooks/pre-compact.mjs +0 -4
  51. package/templates/adapters/trae/hooks/pre-tool-use.mjs +0 -4
  52. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -4
  53. package/templates/adapters/trae/hooks/session-end.mjs +0 -4
  54. package/templates/adapters/trae/hooks/session-start.mjs +65 -5
  55. package/templates/adapters/trae/hooks/stop-check.mjs +6 -87
  56. package/templates/adapters/trae/hooks/subagent-guard.mjs +0 -4
  57. package/templates/adapters/trae/hooks/subagent-stop.mjs +0 -4
  58. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -88
  59. package/templates/adapters/vscode/hooks/notification.mjs +0 -4
  60. package/templates/adapters/vscode/hooks/permission-gate.mjs +0 -4
  61. package/templates/adapters/vscode/hooks/post-tool-failure.mjs +0 -4
  62. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -5
  63. package/templates/adapters/vscode/hooks/pre-compact.mjs +0 -4
  64. package/templates/adapters/vscode/hooks/pre-tool-use.mjs +0 -4
  65. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -4
  66. package/templates/adapters/vscode/hooks/session-end.mjs +0 -4
  67. package/templates/adapters/vscode/hooks/session-start.mjs +65 -5
  68. package/templates/adapters/vscode/hooks/stop-check.mjs +6 -87
  69. package/templates/adapters/vscode/hooks/subagent-guard.mjs +0 -4
  70. package/templates/adapters/vscode/hooks/subagent-stop.mjs +0 -4
  71. package/templates/core/governance/doc-format-guard.ts +29 -112
  72. package/templates/core/governance/post-tool-router.ts +33 -1
  73. package/templates/core/governance/prompt-router.ts +47 -0
  74. package/templates/core/governance/rules.ts +0 -4
  75. package/templates/core/governance/session-start-guard.ts +48 -1
  76. package/templates/core/governance/stop-router.ts +7 -101
  77. package/templates/core/prisma/add.prisma +203 -0
  78. package/templates/core/scripts/db-ensure.sh +61 -2
  79. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  80. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  81. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  82. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  83. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  84. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  85. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  86. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  87. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  88. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  89. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  90. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  91. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  92. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  93. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  94. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  95. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  96. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  97. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  98. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  99. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  100. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  101. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  102. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  103. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  104. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  105. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  106. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  107. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  108. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  109. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  110. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  111. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  112. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  113. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  114. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  115. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  116. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  117. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  118. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
  119. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  120. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  121. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  122. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  123. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  124. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  125. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  126. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  127. package/templates/core/scripts/mcp-server.ts +36 -0
  128. package/templates/core/templates/checklist-template.md +13 -0
  129. package/templates/core/templates/review-implementation-template.md +24 -0
  130. package/templates/core/templates/review-template.md +16 -0
  131. package/templates/core/validation/index.ts +136 -0
  132. package/templates/core/validation/policy.ts +91 -0
  133. package/templates/core/validation/registry.ts +61 -0
  134. package/templates/core/validation/schema-validator.ts +277 -0
  135. package/templates/core/validation/validators/add-route.ts +32 -0
  136. package/templates/core/validation/validators/checklist.ts +48 -0
  137. package/templates/core/validation/validators/handoff.ts +46 -0
  138. package/templates/core/validation/validators/hitl.ts +22 -0
  139. package/templates/core/validation/validators/index.ts +52 -0
  140. package/templates/core/validation/validators/plan.ts +20 -0
  141. package/templates/core/validation/validators/report.ts +16 -0
  142. package/templates/core/validation/validators/review.ts +30 -0
  143. package/templates/core/validation/validators/spec.ts +25 -0
  144. package/templates/core/validation/validators/tasks.ts +40 -0
  145. package/templates/core/validation/validators/types.ts +32 -0
  146. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
  147. package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
@@ -0,0 +1,224 @@
1
+ /*
2
+ * 单元状态解析(Plan rank-calibration Task 2.1.9 / Spec §2.2 + §2.3)
3
+ *
4
+ * 架构约束:单元(Plan 业务闭包)的状态是**既有事实**,必须读出来——不能手填、不能另建状态表。
5
+ * closed = add-route 全部产出项 [x] 且存在 ROUND_CLOSED devlog
6
+ * in-flight = add-route 存在但仍有 [ ],或缺少 ROUND_CLOSED(并发协议下的飞行中单元)
7
+ * unknown = 连 add-route 都没有(§2.3 视为缺制品,样本不可作训练依据)
8
+ *
9
+ * 同时产出**单元引用表**(refs):报告按单元分层时直接引用这些制品,不复制内容(§2.3)。
10
+ */
11
+
12
+ import { validate as coreValidate, type ValidationIssue } from "../../../../../validation/index.js"
13
+
14
+ export interface UnitRefs {
15
+ planPath?: string
16
+ addRoutePath?: string
17
+ handoffPath?: string
18
+ specsDir?: string
19
+ planRecordId?: string
20
+ roundClosedAuditId?: string
21
+ }
22
+
23
+ export interface UnitState {
24
+ planKeyword: string
25
+ state: "closed" | "in-flight" | "unknown"
26
+ /**
27
+ * 诊断量(**不参与 closed 判定**):add-route 勾选与事实的差距。
28
+ * 勾选是过程进度,不是收敛证据——用它判 closed 会把"过程"当"结果"。
29
+ */
30
+ documentationLag: { openSteps: number; totalSteps: number; lagging: boolean }
31
+ /** 封口四要素(Spec §2.2:四项合取) */
32
+ evidence: {
33
+ roundClosed: boolean
34
+ handoff: boolean
35
+ acceptance: boolean
36
+ planStatus: boolean
37
+ }
38
+ /** handoff 合规校验明细(来自 core 校验层;存在 ≠ 合规,Spec §2.2) */
39
+ handoffValidation?: { type: "handoff.single" | "handoff.multi"; ok: boolean; issues: ValidationIssue[] }
40
+ /** 缺失的既有制品(§2.3:缺证据的单元不可作为训练依据) */
41
+ missingArtifacts: string[]
42
+ refs: UnitRefs
43
+ }
44
+
45
+ export interface UnitStateDeps {
46
+ projectRoot: string
47
+ magicDir: string
48
+ /** 读文件内容(不存在返回 null);测试注入 */
49
+ readFile?: (absPath: string) => string | null
50
+ /** 列 plans 目录下的文件名(不存在返回 []);测试注入 */
51
+ listDir?: (absDir: string) => string[]
52
+ /**
53
+ * 列 plans 目录下的**相对路径**(含日期子目录,如 `2026-09/12/x.md`);生产路径必须用这个 ——
54
+ * ADD 约定制品按 `plans/{YYYY-MM}/{DD}/` 分层,扁平列目录会漏掉全部真实 Plan(2026-09-13 真实单元自检暴露)。
55
+ */
56
+ listFiles?: (absDir: string) => string[]
57
+ /** 是否存在(specs 目录判断);测试注入 */
58
+ exists?: (absPath: string) => boolean
59
+ /** 查 ROUND_CLOSED devlog(测试注入) */
60
+ findRoundClosed?: (planKeyword: string) => Promise<{ id: string } | null>
61
+ /** 查 PlanRecord(测试注入) */
62
+ findPlanRecord?: (planKeyword: string) => Promise<{
63
+ id: string
64
+ lifecycle?: string
65
+ doneTasks?: number
66
+ totalTasks?: number
67
+ } | null>
68
+ /**
69
+ * handoff 合规校验(默认 = core 校验层,schema 真源)。
70
+ * 注入点仅供测试;生产路径必须走 core,不得再自解析模板(Spec §2.2/§2.4)。
71
+ */
72
+ validateHandoff?: (input: {
73
+ type: "handoff.single" | "handoff.multi"
74
+ path: string
75
+ expectRounds: number
76
+ }) => { ok: boolean; issues: ValidationIssue[] }
77
+ }
78
+
79
+ /**
80
+ * 轮次数推导(既有制品读取,不新建状态)。
81
+ *
82
+ * 语义:取各制品中 `轮次 N` 的最大编号;**编号从 0 起 → +1,从 1 起 → 不变**。
83
+ * 例:`轮次 0..3` → 4 轮;`轮次 1` → 1 轮(单轮)。
84
+ * 输入建议按真源优先级传入:[planContent, tasksContent, addRouteContent]
85
+ * (tasks.md 的「轮次拓扑」是最稳定的轮次真源;add-route 的轮次常写在代码块内)。
86
+ */
87
+ export function planRounds(...contents: readonly string[]): number {
88
+ const nums: number[] = []
89
+ for (const content of contents) {
90
+ for (const m of content.matchAll(/轮次\s*(\d+)/g)) nums.push(Number(m[1]))
91
+ }
92
+ if (nums.length === 0) return 1
93
+ const max = Math.max(...nums)
94
+ const min = Math.min(...nums)
95
+ return min === 0 ? max + 1 : max
96
+ }
97
+
98
+ /** 统计 add-route 产出项勾选情况(只认 `- [ ]` / `- [x]`) */
99
+ export function countSteps(content: string): { open: number; total: number } {
100
+ let open = 0
101
+ let total = 0
102
+ for (const line of content.split("\n")) {
103
+ if (/^- \[ \]/.test(line)) {
104
+ open++
105
+ total++
106
+ } else if (/^- \[[xX]\]/.test(line)) {
107
+ total++
108
+ }
109
+ }
110
+ return { open, total }
111
+ }
112
+
113
+ export async function resolveUnitState(
114
+ planKeyword: string,
115
+ deps: UnitStateDeps,
116
+ ): Promise<UnitState> {
117
+ const plansDir = `${deps.projectRoot}/${deps.magicDir}/plans`
118
+ const specsDirRoot = `${deps.projectRoot}/${deps.magicDir}/specs`
119
+ const listDir = deps.listFiles ?? deps.listDir ?? (() => [])
120
+ const readFile = deps.readFile ?? (() => null)
121
+ const exists = deps.exists ?? (() => false)
122
+
123
+ const has = (name: string, must: string[]): boolean =>
124
+ must.every((m) => name.toLowerCase().includes(m.toLowerCase()))
125
+
126
+ const files = listDir(plansDir)
127
+ // ★ 匹配基准是 plan **base** 名(去掉 -plan-vN):兄弟制品不带 plan 段——
128
+ // Plan=`{base}-plan-v1.md`、add-route=`{base}-add-route-v1.md`、handoff=`{base}-handoff-v1.md`。
129
+ // 用完整 planKeyword 去匹配会漏掉后两者(2026-09-13 真实单元自检暴露)。
130
+ const base = planKeyword.replace(/-plan-v\d+$/i, "")
131
+ const planRel = files.find((f) => has(f, [base, "-plan-v"]) && !f.includes(".hitl"))
132
+ const addRouteRel = files.find((f) => has(f, [base, "add-route"]))
133
+ const handoffRel = files.find((f) => has(f, [base, "handoff"]))
134
+
135
+ const refs: UnitRefs = {}
136
+ const missingArtifacts: string[] = []
137
+
138
+ if (planRel) refs.planPath = `${deps.magicDir}/plans/${planRel}`
139
+ else missingArtifacts.push("plan")
140
+ if (addRouteRel) refs.addRoutePath = `${deps.magicDir}/plans/${addRouteRel}`
141
+ else missingArtifacts.push("add-route")
142
+ if (handoffRel) refs.handoffPath = `${deps.magicDir}/plans/${handoffRel}`
143
+ else missingArtifacts.push("handoff")
144
+
145
+ if (exists(`${specsDirRoot}/${base}`)) refs.specsDir = `${deps.magicDir}/specs/${base}`
146
+ else missingArtifacts.push("specs")
147
+
148
+ const [roundClosed, planRecord] = await Promise.all([
149
+ deps.findRoundClosed ? deps.findRoundClosed(planKeyword) : Promise.resolve(null),
150
+ deps.findPlanRecord ? deps.findPlanRecord(planKeyword) : Promise.resolve(null),
151
+ ])
152
+ if (roundClosed) refs.roundClosedAuditId = roundClosed.id
153
+ else missingArtifacts.push("round-closed")
154
+ if (planRecord) refs.planRecordId = planRecord.id
155
+ else missingArtifacts.push("plan-record")
156
+
157
+ const addRouteContent = addRouteRel ? (readFile(`${plansDir}/${addRouteRel}`) ?? "") : ""
158
+ const { open, total } = countSteps(addRouteContent)
159
+
160
+ // 验收证据:checklist 存在且 [T] 项无未勾选(缺证据的单元不可作训练依据)
161
+ const checklistAbs = refs.specsDir ? `${deps.projectRoot}/${refs.specsDir}/checklist.md` : ""
162
+ const checklistContent = checklistAbs ? (readFile(checklistAbs) ?? "") : ""
163
+ const unverifiedT = (checklistContent.match(/^- \[ \] \[T\]/gm) ?? []).length
164
+ const verifiedT = (checklistContent.match(/^- \[[xX]\] \[T\]/gm) ?? []).length
165
+ const acceptance = unverifiedT + verifiedT > 0 && unverifiedT === 0
166
+
167
+ // PlanRecord 状态:存在且进度收敛(done === total 且 total > 0)
168
+ const planStatus =
169
+ !!planRecord && (planRecord.totalTasks ?? 0) > 0 && planRecord.doneTasks === planRecord.totalTasks
170
+
171
+ const evidence = {
172
+ roundClosed: !!roundClosed,
173
+ handoff: false, // 由下方 handoff 合规校验填充(存在 ≠ 合规)
174
+ acceptance,
175
+ planStatus,
176
+ }
177
+
178
+ // 轮次真源:Plan 正文 + tasks.md(若有)+ add-route,取最大编号推导
179
+ const specBaseFromRefs = refs.specsDir ? `${deps.projectRoot}/${refs.specsDir}` : ""
180
+ const tasksContent = specBaseFromRefs ? (readFile(`${specBaseFromRefs}/tasks.md`) ?? "") : ""
181
+ const planContentForRounds = planRel ? (readFile(`${plansDir}/${planRel}`) ?? "") : ""
182
+
183
+ // handoff 合规校验:走 core 校验层(schema 真源);缺文件即不合规
184
+ let handoffValidation: UnitState["handoffValidation"]
185
+ if (handoffRel) {
186
+ const rounds = planRounds(planContentForRounds, tasksContent, addRouteContent)
187
+ const type = rounds > 1 ? "handoff.multi" : "handoff.single"
188
+ const absPath = `${plansDir}/${handoffRel}`
189
+ const runner =
190
+ deps.validateHandoff ??
191
+ ((input: { type: "handoff.single" | "handoff.multi"; path: string; expectRounds: number }) => {
192
+ // core 校验层读 schema 真源;本模块不自解析模板(Spec §2.2/§2.4)
193
+ return coreValidate({
194
+ type: input.type,
195
+ path: input.path,
196
+ hook: "manual",
197
+ expectRounds: input.expectRounds,
198
+ projectRoot: deps.projectRoot,
199
+ magicDir: deps.magicDir,
200
+ })
201
+ })
202
+ const result = runner({ type, path: absPath, expectRounds: rounds })
203
+ handoffValidation = { type, ok: result.ok, issues: result.issues }
204
+ evidence.handoff = result.ok
205
+ }
206
+
207
+ let state: UnitState["state"]
208
+ if (!addRouteRel && !planRel) state = "unknown"
209
+ else if (evidence.roundClosed && evidence.handoff && evidence.acceptance && evidence.planStatus) {
210
+ state = "closed"
211
+ } else {
212
+ state = "in-flight"
213
+ }
214
+
215
+ return {
216
+ planKeyword,
217
+ state,
218
+ documentationLag: { openSteps: open, totalSteps: total, lagging: total > 0 && open > 0 },
219
+ evidence,
220
+ ...(handoffValidation ? { handoffValidation } : {}),
221
+ missingArtifacts,
222
+ refs,
223
+ }
224
+ }
@@ -0,0 +1,59 @@
1
+ /*
2
+ * 冲突检测(Plan §3 治理队列 / §7.2 同级冲突不得静默覆盖)
3
+ *
4
+ * 首版为保守启发式(无 embedding 依赖):同 scope 且高文本相似的
5
+ * CONSTRAINT/DECISION 互为疑似冲突,必须进入 review,由人裁决。
6
+ */
7
+ import { charBigrams, jaccardSimilarity } from "./dedup.js"
8
+
9
+ export interface Conflict {
10
+ memoryId: string
11
+ topic: string
12
+ similarity: number
13
+ reason: string
14
+ }
15
+
16
+ export interface ConflictCandidate {
17
+ id?: string
18
+ kind: string
19
+ topic: string
20
+ content: string
21
+ scopeType: string
22
+ scopeValue: string
23
+ }
24
+
25
+ export interface ConflictOptions {
26
+ /** 相似度阈值(字符二元组 Jaccard),默认 0.6 */
27
+ threshold?: number
28
+ /** 参与冲突判定的 kind 集合 */
29
+ kinds?: readonly string[]
30
+ }
31
+
32
+ const DEFAULT_CONFLICT_KINDS = ["CONSTRAINT", "DECISION", "CONVENTION"] as const
33
+
34
+ export function detectConflicts(
35
+ candidate: ConflictCandidate,
36
+ actives: ConflictCandidate[],
37
+ opts: ConflictOptions = {},
38
+ ): Conflict[] {
39
+ const threshold = opts.threshold ?? 0.6
40
+ const kinds = opts.kinds ?? DEFAULT_CONFLICT_KINDS
41
+ if (!kinds.includes(candidate.kind)) return []
42
+ const candVec = charBigrams(candidate.topic + " " + candidate.content)
43
+ const out: Conflict[] = []
44
+ for (const m of actives) {
45
+ if (m.id && m.id === candidate.id) continue
46
+ if (!kinds.includes(m.kind)) continue
47
+ if (m.scopeType !== candidate.scopeType || m.scopeValue !== candidate.scopeValue) continue
48
+ const sim = jaccardSimilarity(candVec, charBigrams(m.topic + " " + m.content))
49
+ if (sim >= threshold) {
50
+ out.push({
51
+ memoryId: m.id ?? "",
52
+ topic: m.topic,
53
+ similarity: Math.round(sim * 1000) / 1000,
54
+ reason: `同 scope(${m.scopeType}:${m.scopeValue}) 存在高相似 ${m.kind},疑似冲突,需人工裁决`,
55
+ })
56
+ }
57
+ }
58
+ return out.sort((a, b) => b.similarity - a.similarity)
59
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * 去重与内容规范化(Plan §4.4)
3
+ * contentHash = sha256(normalize(text));幂等键 = repositoryRef + contentHash + scopeType + scopeValue
4
+ */
5
+ import { createHash } from "crypto"
6
+
7
+ /** 规范化:NFKC(全半角折叠)→ 小写 → 折叠全部空白为单空格 → trim */
8
+ export function normalizeContent(s: string): string {
9
+ return s.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim()
10
+ }
11
+
12
+ export function contentHash(s: string): string {
13
+ return createHash("sha256").update(normalizeContent(s), "utf8").digest("hex")
14
+ }
15
+
16
+ /** 幂等键(与 AddMemory 的 @@unique 约束一一对应) */
17
+ export function memoryIdempotencyKey(input: {
18
+ repositoryRef: string
19
+ content: string
20
+ scopeType: string
21
+ scopeValue: string
22
+ }): string {
23
+ return [
24
+ input.repositoryRef,
25
+ contentHash(input.content),
26
+ input.scopeType,
27
+ input.scopeValue,
28
+ ].join("|")
29
+ }
30
+
31
+ /** 字符二元组集合(冲突检测 / 近重复合并的轻量相似度基础,无需 embedding) */
32
+ export function charBigrams(s: string): Set<string> {
33
+ const n = normalizeContent(s)
34
+ const out = new Set<string>()
35
+ for (let i = 0; i < n.length - 1; i++) out.add(n.slice(i, i + 2))
36
+ if (n.length === 1) out.add(n)
37
+ return out
38
+ }
39
+
40
+ export function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
41
+ if (a.size === 0 && b.size === 0) return 1
42
+ let inter = 0
43
+ for (const x of a) if (b.has(x)) inter++
44
+ return inter / (a.size + b.size - inter || 1)
45
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Memory 子系统稳定错误码(Plan §12.3:MCP 错误采用稳定错误码,不能只返回自由文本)
3
+ * 契约:code 字符串冻结,变更需走 Review;message 可携带上下文细节。
4
+ */
5
+ export const MEMORY_ERROR = {
6
+ ERR_REPOSITORY_MISMATCH: "调用方 repositoryRef 与运行时上下文不一致,越权访问被拒绝",
7
+ ERR_ORG_SCOPE_DISABLED: "ORGANIZATION scope 首版禁用(Plan §17-7 定案)",
8
+ ERR_ILLEGAL_TRANSITION: "非法状态迁移",
9
+ ERR_EVIDENCE_REQUIRED: "进入 ACTIVE 必须至少关联一条 Evidence",
10
+ ERR_APPROVAL_REQUIRED: "进入 ACTIVE 必须提供 approvedBy/approvedAt",
11
+ ERR_SUPERSESSION_INVALID: "supersede 需要有效的 supersededById 且 repository/scope 兼容",
12
+ ERR_INVARIANT: "字段不变量违反(如 importance/confidence 越出 [0,1])",
13
+ ERR_FTS_UNAVAILABLE: "FTS 能力不可用且无法降级",
14
+ ERR_SECRET_DETECTED: "内容命中密钥/凭证模式,拒写",
15
+ ERR_NOT_FOUND: "记录不存在",
16
+ ERR_EMBEDDING_DISABLED: "EmbeddingProvider=none,向量能力未启用",
17
+ ERR_DIMENSION_MISMATCH: "嵌入维度与真源不一致(模型元数据 vs 声明维度),拒绝写入脏向量",
18
+ } as const
19
+
20
+ export type MemoryErrorCode = keyof typeof MEMORY_ERROR
21
+
22
+ export class MemoryError extends Error {
23
+ readonly code: MemoryErrorCode
24
+ constructor(code: MemoryErrorCode, detail?: string) {
25
+ super(detail ? `${code}: ${MEMORY_ERROR[code]}(${detail})` : `${code}: ${MEMORY_ERROR[code]}`)
26
+ this.name = "MemoryError"
27
+ this.code = code
28
+ }
29
+ }
30
+
31
+ export function isMemoryError(e: unknown): e is MemoryError {
32
+ return e instanceof MemoryError
33
+ }
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Handoff Digest 候选构造(Plan §3.1 轮 2 / Spec §3 §HandoffDigest)
3
+ *
4
+ * 纯函数:输入 HANDOFF Evidence 行 → 输出可落库的 HANDOFF_DIGEST 候选提案。
5
+ * 三条不变量:
6
+ * 1. **绝不直接 ACTIVE**——提案只带 status=CANDIDATE 语义(由调用方落库时体现);
7
+ * 2. **幂等**——dedupKey 由「来源引用 + 摘要内容」派生,同一 evidence 重放得到同一键;
8
+ * 3. **内容是可验证结论**,不是裸数值或空串:excerpt 为空时显式标注「摘要待人工补充」。
9
+ */
10
+ import { contentHash, normalizeContent } from "./dedup.js"
11
+
12
+ export const HANDOFF_DIGEST_KIND = "HANDOFF_DIGEST" as const
13
+
14
+ /** 摘要正文最大字符数(超出截断并标注) */
15
+ export const HANDOFF_EXCERPT_MAX = 300
16
+
17
+ export interface HandoffDigestInput {
18
+ /** handoff 文档路径或引用(evidence.sourceRef) */
19
+ handoffRef: string
20
+ /** evidence 摘要原文 */
21
+ excerpt: string
22
+ /** 关联 Plan 关键词(evidence.planKeyword,可为空) */
23
+ planKeyword?: string | null
24
+ }
25
+
26
+ export interface HandoffDigestProposal {
27
+ kind: typeof HANDOFF_DIGEST_KIND
28
+ topic: string
29
+ content: string
30
+ scopeType: "PLAN" | "REPOSITORY"
31
+ scopeValue: string
32
+ contentHash: string
33
+ dedupKey: string
34
+ metadata: {
35
+ handoffRef: string
36
+ planKeyword: string | null
37
+ excerptTruncated: boolean
38
+ }
39
+ }
40
+
41
+ /** 从 handoff 引用提取主题名(去目录、去扩展名) */
42
+ export function handoffTopicOf(handoffRef: string): string {
43
+ const base = handoffRef.split(/[\\/]/).pop() ?? handoffRef
44
+ const stem = base.replace(/\.(md|markdown|txt)$/i, "")
45
+ return stem.length > 0 ? stem : handoffRef
46
+ }
47
+
48
+ /**
49
+ * 构造 HANDOFF_DIGEST 候选提案。
50
+ *
51
+ * scope 选择:有 planKeyword → PLAN scope(历史语义,默认召回需显式放行);
52
+ * 无 planKeyword → REPOSITORY scope(可被常规召回命中)。
53
+ */
54
+ export function buildHandoffDigest(
55
+ input: HandoffDigestInput,
56
+ opts: { repositoryRef: string; maxExcerpt?: number },
57
+ ): HandoffDigestProposal {
58
+ const maxExcerpt = opts.maxExcerpt ?? HANDOFF_EXCERPT_MAX
59
+ const topic = handoffTopicOf(input.handoffRef)
60
+ const normalizedExcerpt = normalizeContent(input.excerpt ?? "")
61
+ const planKeyword = input.planKeyword && input.planKeyword.trim().length > 0
62
+ ? input.planKeyword.trim()
63
+ : null
64
+
65
+ const truncated = normalizedExcerpt.length > maxExcerpt
66
+ const body = truncated
67
+ ? `${normalizedExcerpt.slice(0, maxExcerpt)}…(已截断)`
68
+ : normalizedExcerpt
69
+
70
+ const content =
71
+ body.length > 0
72
+ ? `交接摘要 ${topic}:${body}(来源 ${input.handoffRef})`
73
+ : `交接文档 ${input.handoffRef} 已归档,摘要待人工补充(自动摘要为空)。`
74
+
75
+ const scopeType: "PLAN" | "REPOSITORY" = planKeyword ? "PLAN" : "REPOSITORY"
76
+ const scopeValue = planKeyword ?? opts.repositoryRef
77
+
78
+ return {
79
+ kind: HANDOFF_DIGEST_KIND,
80
+ topic,
81
+ content,
82
+ scopeType,
83
+ scopeValue,
84
+ contentHash: contentHash(content),
85
+ dedupKey: contentHash(`${HANDOFF_DIGEST_KIND}|${input.handoffRef}|${normalizedExcerpt}`),
86
+ metadata: {
87
+ handoffRef: input.handoffRef,
88
+ planKeyword,
89
+ excerptTruncated: truncated,
90
+ },
91
+ }
92
+ }
@@ -0,0 +1,79 @@
1
+ /*
2
+ * 指标到知识的转换(Plan §10)
3
+ *
4
+ * entropy/code quality/Gate score 只写 AddMetricSnapshot;单个数值不能自动成为长期文本记忆。
5
+ * 候选生成需满足:同 scope 异常连续达阈值 / 多独立证据指向同根因 / 显著偏离基线且有可解释事件。
6
+ */
7
+
8
+ export interface MetricPoint {
9
+ repositoryRef: string
10
+ metricType: string
11
+ value: number
12
+ baseline?: number | null
13
+ planKeyword?: string | null
14
+ measuredAt: Date
15
+ sourceRef: string
16
+ }
17
+
18
+ export interface MetricCandidateRule {
19
+ /** 同 scope 连续异常阈值(默认 3 次) */
20
+ streakThreshold: number
21
+ /** 显著偏离基线的相对幅度(默认 20%) */
22
+ deviationRatio: number
23
+ /** 判定"异常"的方向:高于基线为坏(higher-worse)或低于基线为坏(lower-worse) */
24
+ direction: "higher-worse" | "lower-worse"
25
+ }
26
+
27
+ export const DEFAULT_METRIC_RULE: MetricCandidateRule = {
28
+ streakThreshold: 3,
29
+ deviationRatio: 0.2,
30
+ direction: "lower-worse",
31
+ }
32
+
33
+ export interface MetricCandidateProposal {
34
+ topic: string
35
+ content: string
36
+ evidenceSourceRefs: string[]
37
+ rule: string
38
+ }
39
+
40
+ function isAnomaly(p: MetricPoint, rule: MetricCandidateRule): boolean {
41
+ if (p.baseline == null) return false
42
+ const dev = Math.abs(p.value - p.baseline) / (Math.abs(p.baseline) || 1)
43
+ if (dev < rule.deviationRatio) return false
44
+ return rule.direction === "lower-worse" ? p.value < p.baseline : p.value > p.baseline
45
+ }
46
+
47
+ /**
48
+ * 检测「同 scope 连续异常」:按时间排序后尾部连续异常数 ≥ streakThreshold 时生成候选建议。
49
+ * 纯函数;是否真正创建 Candidate 由调用方(consolidation job / 人工)决定。
50
+ */
51
+ export function detectAnomalyStreak(
52
+ points: MetricPoint[],
53
+ rule: MetricCandidateRule = DEFAULT_METRIC_RULE,
54
+ ): MetricCandidateProposal | null {
55
+ if (points.length < rule.streakThreshold) return null
56
+ const sorted = [...points].sort((a, b) => a.measuredAt.getTime() - b.measuredAt.getTime())
57
+ let streak = 0
58
+ for (let i = sorted.length - 1; i >= 0; i--) {
59
+ if (isAnomaly(sorted[i], rule)) streak++
60
+ else break
61
+ }
62
+ if (streak < rule.streakThreshold) return null
63
+ const last = sorted[sorted.length - 1]
64
+ const streakPoints = sorted.slice(-streak)
65
+ return {
66
+ topic: `${last.metricType} 连续 ${streak} 次显著偏离基线`,
67
+ content:
68
+ `指标 ${last.metricType} 在最近 ${streak} 次测量中连续偏离基线超过 ${rule.deviationRatio * 100}%` +
69
+ `(baseline=${streakPoints[0].baseline}, 最新值=${last.value})。` +
70
+ `建议人工审核是否形成可验证结论后再转为长期记忆。`,
71
+ evidenceSourceRefs: streakPoints.map((p) => p.sourceRef),
72
+ rule: `anomaly-streak>=${rule.streakThreshold} deviation>=${rule.deviationRatio} ${rule.direction}`,
73
+ }
74
+ }
75
+
76
+ /** Gate 执行后生成 sourceRef(Review P1 #2 回流:{gate}:{planKeyword}:{runId}) */
77
+ export function metricSourceRef(gate: string, planKeyword: string, runId: string): string {
78
+ return `${gate}:${planKeyword}:${runId}`
79
+ }
@@ -0,0 +1,94 @@
1
+ /*
2
+ * Memory Scope 规则(Plan §7.2)
3
+ *
4
+ * 优先级:symbol > path > module > branch > repository > organization
5
+ * ORGANIZATION 首版禁用(写入拒绝,§17-7);PLAN/SPEC 只在对应流程或显式历史查询中参与。
6
+ */
7
+ import { MemoryError } from "./errors.js"
8
+
9
+ export type MemoryScopeType =
10
+ | "ORGANIZATION" | "REPOSITORY" | "BRANCH" | "MODULE"
11
+ | "PATH" | "SYMBOL" | "PLAN" | "SPEC"
12
+
13
+ export interface Scope {
14
+ type: MemoryScopeType
15
+ value: string
16
+ }
17
+
18
+ /** 召回时的上下文坐标(由调用方从运行时环境组装) */
19
+ export interface ScopeContext {
20
+ repository: string
21
+ branch?: string
22
+ module?: string
23
+ paths?: string[]
24
+ symbols?: string[]
25
+ planKeyword?: string
26
+ specRef?: string
27
+ /** 显式历史查询时放行 PLAN/SPEC scope(默认 false) */
28
+ includePlanScope?: boolean
29
+ }
30
+
31
+ const RANK: Record<MemoryScopeType, number> = {
32
+ SYMBOL: 7,
33
+ PATH: 6,
34
+ MODULE: 5,
35
+ BRANCH: 4,
36
+ REPOSITORY: 3,
37
+ PLAN: 2,
38
+ SPEC: 2,
39
+ ORGANIZATION: 1,
40
+ }
41
+
42
+ export function scopeRank(t: MemoryScopeType): number {
43
+ return RANK[t]
44
+ }
45
+
46
+ /** 写入守卫:ORGANIZATION 首版禁用 */
47
+ export function assertScopeWritable(scope: Scope): void {
48
+ if (scope.type === "ORGANIZATION") {
49
+ throw new MemoryError("ERR_ORG_SCOPE_DISABLED")
50
+ }
51
+ }
52
+
53
+ /** 判定一条 Memory 的 scope 在当前上下文是否有效(召回过滤用) */
54
+ export function scopeApplies(memScope: Scope, ctx: ScopeContext): boolean {
55
+ switch (memScope.type) {
56
+ case "ORGANIZATION":
57
+ return false // 首版禁用
58
+ case "REPOSITORY":
59
+ return memScope.value === ctx.repository
60
+ case "BRANCH":
61
+ return !!ctx.branch && ctx.branch === memScope.value
62
+ case "MODULE":
63
+ // module 语义:当前任一路径以该模块目录为前缀
64
+ return !!ctx.module && ctx.module === memScope.value ||
65
+ (ctx.paths ?? []).some((p) => p === memScope.value || p.startsWith(memScope.value + "/"))
66
+ case "PATH":
67
+ return (ctx.paths ?? []).some(
68
+ (p) => p === memScope.value || p.startsWith(memScope.value.replace(/\/?$/, "/")),
69
+ )
70
+ case "SYMBOL":
71
+ return (ctx.symbols ?? []).includes(memScope.value)
72
+ case "PLAN":
73
+ return !!ctx.includePlanScope && !!ctx.planKeyword && ctx.planKeyword === memScope.value
74
+ case "SPEC":
75
+ return !!ctx.includePlanScope && !!ctx.specRef && ctx.specRef === memScope.value
76
+ }
77
+ }
78
+
79
+ /**
80
+ * supersede 兼容性(Plan §4.4):新旧 Memory 的 repository/scope 必须兼容。
81
+ * 兼容 = 同类型同值;或 PATH/MODULE 类型间存在前缀覆盖;或一方为 REPOSITORY(全库覆盖)。
82
+ */
83
+ export function scopesCompatible(a: Scope, b: Scope): boolean {
84
+ if (a.type === b.type && a.value === b.value) return true
85
+ if (a.type === "REPOSITORY" || b.type === "REPOSITORY") {
86
+ return a.type === "REPOSITORY" && b.type === "REPOSITORY" ? a.value === b.value : true
87
+ }
88
+ const pathLike = (t: MemoryScopeType) => t === "PATH" || t === "MODULE"
89
+ if (pathLike(a.type) && pathLike(b.type)) {
90
+ return a.value.startsWith(b.value.replace(/\/?$/, "/")) ||
91
+ b.value.startsWith(a.value.replace(/\/?$/, "/"))
92
+ }
93
+ return false
94
+ }
@@ -0,0 +1,50 @@
1
+ /*
2
+ * 密钥/凭证扫描(Plan §11:Evidence 摘录在写入前执行秘密与敏感信息检测)
3
+ * 命中即拒写(ERR_SECRET_DETECTED)。宁可误报,不可漏报。
4
+ */
5
+ import { MemoryError } from "./errors.js"
6
+
7
+ export interface SecretHit {
8
+ pattern: string
9
+ index: number
10
+ preview: string
11
+ }
12
+
13
+ interface SecretPattern {
14
+ name: string
15
+ re: RegExp
16
+ }
17
+
18
+ const PATTERNS: SecretPattern[] = [
19
+ { name: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
20
+ { name: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/ },
21
+ { name: "github-token", re: /\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/ },
22
+ { name: "openai-style-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
23
+ { name: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
24
+ { name: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/i },
25
+ { name: "conn-string-password", re: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:[^@\s]+@/i },
26
+ { name: "generic-secret-assign", re: /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token)\b\s*[:=]\s*["'][^"'\s]{8,}["']/i },
27
+ { name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
28
+ ]
29
+
30
+ export function scanSecrets(text: string): SecretHit[] {
31
+ const hits: SecretHit[] = []
32
+ for (const p of PATTERNS) {
33
+ const m = p.re.exec(text)
34
+ if (m) {
35
+ hits.push({
36
+ pattern: p.name,
37
+ index: m.index,
38
+ preview: text.slice(Math.max(0, m.index - 10), m.index + 20).replace(/\s+/g, " ").slice(0, 40),
39
+ })
40
+ }
41
+ }
42
+ return hits
43
+ }
44
+
45
+ export function assertNoSecrets(text: string): void {
46
+ const hits = scanSecrets(text)
47
+ if (hits.length > 0) {
48
+ throw new MemoryError("ERR_SECRET_DETECTED", hits.map((h) => h.pattern).join(","))
49
+ }
50
+ }