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,31 @@
1
+ /*
2
+ * 查询词项抽取(双后端 LIKE/bigram 通道共用)
3
+ *
4
+ * 背景:trigram 分词器(PG pg_trgm / SQLite FTS5 trigram)是子串语义,
5
+ * 对释义查询(如 "新端口怎么申请" vs 内容 "新增端口")召回不足。
6
+ * 本模块把查询分解为「拉丁词 + CJK 二元组」词项,供 LIKE 重叠通道计分。
7
+ */
8
+ import { normalizeContent } from "../domain/dedup.js"
9
+
10
+ const MAX_TERMS = 24
11
+
12
+ /** 拉丁/数字词(≥2 字符)+ CJK 连续段的滑动二元组 */
13
+ export function extractQueryTerms(query: string): string[] {
14
+ const norm = normalizeContent(query)
15
+ const terms: string[] = []
16
+ const seen = new Set<string>()
17
+ const push = (t: string) => {
18
+ if (!seen.has(t) && terms.length < MAX_TERMS) { seen.add(t); terms.push(t) }
19
+ }
20
+
21
+ // 拉丁词
22
+ for (const m of norm.matchAll(/[a-z0-9_]{2,}/g)) push(m[0])
23
+
24
+ // CJK 连续段 → 滑动二元组(单字段保留单字)
25
+ for (const seg of norm.matchAll(/[㐀-鿿぀-ヿ]+/g)) {
26
+ const s = seg[0]
27
+ if (s.length === 1) { push(s); continue }
28
+ for (let i = 0; i < s.length - 1; i++) push(s.slice(i, i + 2))
29
+ }
30
+ return terms
31
+ }
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Recall 审计写入(Plan §4.3/§7.1-10:每次召回记录候选、评分、选择,可重放)
3
+ */
4
+ import type { RecalledMemory } from "./types.js"
5
+
6
+ export interface RecallAuditRecord {
7
+ repositoryRef: string
8
+ query: string
9
+ stage: string
10
+ consumerRef?: string
11
+ scopeContext: unknown
12
+ candidateIds: string[]
13
+ items: RecalledMemory[]
14
+ excluded: { memoryId: string; reason: string }[]
15
+ rankingVersion: string
16
+ tokenBudget: number
17
+ injectedTokens: number
18
+ latencyMs: number
19
+ degradedMode?: string
20
+ }
21
+
22
+ export interface RecallAuditStore {
23
+ createRecall(data: {
24
+ repositoryRef: string
25
+ query: string
26
+ stage: string
27
+ consumerRef: string | null
28
+ scopeContext: unknown
29
+ candidateIds: unknown
30
+ selectedIds: unknown
31
+ scoreBreakdown: unknown
32
+ exclusionReasons: unknown
33
+ rankingVersion: string
34
+ tokenBudget: number
35
+ injectedTokens: number
36
+ latencyMs: number
37
+ degradedMode: string | null
38
+ }): Promise<{ id: string }>
39
+ createRecallItem(data: {
40
+ recallId: string
41
+ memoryId: string
42
+ selected: boolean
43
+ rank: number | null
44
+ }): Promise<unknown>
45
+ }
46
+
47
+ /** 落库一次召回(Recall + 每候选一条 RecallItem),返回 recallId */
48
+ export async function writeRecallAudit(
49
+ store: RecallAuditStore,
50
+ rec: RecallAuditRecord,
51
+ ): Promise<string> {
52
+ const selectedIds = rec.items.map((i) => i.memoryId)
53
+ const scoreBreakdown: Record<string, Record<string, number>> = {}
54
+ for (const item of rec.items) scoreBreakdown[item.memoryId] = item.scoreBreakdown
55
+
56
+ const recall = await store.createRecall({
57
+ repositoryRef: rec.repositoryRef,
58
+ query: rec.query,
59
+ stage: rec.stage,
60
+ consumerRef: rec.consumerRef ?? null,
61
+ scopeContext: rec.scopeContext,
62
+ candidateIds: rec.candidateIds,
63
+ selectedIds,
64
+ scoreBreakdown,
65
+ exclusionReasons: rec.excluded,
66
+ rankingVersion: rec.rankingVersion,
67
+ tokenBudget: rec.tokenBudget,
68
+ injectedTokens: rec.injectedTokens,
69
+ latencyMs: rec.latencyMs,
70
+ degradedMode: rec.degradedMode ?? null,
71
+ })
72
+
73
+ // 候选全集 = 选中 + 被排除(重放完整性)
74
+ const selectedSet = new Set(selectedIds)
75
+ const rankOf = new Map(selectedIds.map((id, i) => [id, i + 1]))
76
+ const excludedReason = new Map(rec.excluded.map((e) => [e.memoryId, e.reason]))
77
+ for (const id of rec.candidateIds) {
78
+ await store.createRecallItem({
79
+ recallId: recall.id,
80
+ memoryId: id,
81
+ selected: selectedSet.has(id),
82
+ rank: rankOf.get(id) ?? null,
83
+ })
84
+ void excludedReason.get(id) // reason 已落 Recall.exclusionReasons(JSON),Item 层不重复
85
+ }
86
+ return recall.id
87
+ }
@@ -0,0 +1,116 @@
1
+ /*
2
+ * 治理重排(Plan §7.3)
3
+ *
4
+ * finalScore = rrfScore
5
+ * + scopeBoost + kindBoost + importanceBoost + confidenceBoost + mandatoryConstraintBoost
6
+ * - stalePenalty - conflictPenalty - redundancyPenalty
7
+ *
8
+ * 权重与 rankingVersion 配置化;首版默认权重经评测校准,未评测前不作为不可变产品规则。
9
+ */
10
+ import { scopeRank, type ScopeContext } from "../domain/scope.js"
11
+ import type { MemoryStatus } from "../domain/state-machine.js"
12
+
13
+ export const RANKING_VERSION = "memory-rank-v1"
14
+ /** hybrid(lexical + vector 双通道)排序版本:Plan §3.4 轮 3「rankingVersion v2」 */
15
+ export const RANKING_VERSION_HYBRID = "memory-rank-v2"
16
+
17
+ /**
18
+ * RRF 分数放大系数(Plan 轮 3 融合迭代)。
19
+ *
20
+ * 问题:RRF 单通道差值约 1/61−1/80 ≈ 0.004,而治理 boost 量级为 0.1–0.5 —— boost 完全主导排序,
21
+ * 相关性(RRF)只起微调作用,表现为「重要度高的无关记忆排在相关记忆之前」。
22
+ * 处理:把 RRF 分数放大到与 boost 可比的量级(×100),使**相关性主导、治理项做同分位微调**。
23
+ * 该系数进入 rankingVersion 快照(权重变更即版本变更)。
24
+ */
25
+ export const RRF_SCORE_SCALE = 1000
26
+
27
+ export interface RerankWeights {
28
+ scopeBoost: number
29
+ kindBoost: Record<string, number>
30
+ importanceBoost: number
31
+ confidenceBoost: number
32
+ mandatoryConstraintBoost: number
33
+ stalePenalty: number
34
+ conflictPenalty: number
35
+ redundancyPenalty: number
36
+ }
37
+
38
+ /** 默认权重(v1):强约束信号最强,stale/冲突/冗余显著惩罚 */
39
+ export const DEFAULT_WEIGHTS: RerankWeights = {
40
+ scopeBoost: 0.15, // × (scopeRank/7)
41
+ kindBoost: { CONSTRAINT: 0.25, DECISION: 0.15, FAILURE: 0.1, PITFALL: 0.1, CONVENTION: 0.05 },
42
+ importanceBoost: 0.2, // × importance
43
+ confidenceBoost: 0.1, // × confidence
44
+ mandatoryConstraintBoost: 0.5, // 适用 scope 内的 ACTIVE CONSTRAINT
45
+ stalePenalty: 0.3,
46
+ conflictPenalty: 0.2,
47
+ redundancyPenalty: 0.1,
48
+ }
49
+
50
+ export interface RerankInput {
51
+ memoryId: string
52
+ rrfScore: number
53
+ kind: string
54
+ status: MemoryStatus
55
+ importance: number
56
+ confidence: number
57
+ scopeType: string
58
+ scopeValue: string
59
+ /** 与已选高分项近重复(由 context-builder 阶段标记,重排阶段先打罚分) */
60
+ redundant?: boolean
61
+ /** 与其他候选项存在未决冲突 */
62
+ conflicted?: boolean
63
+ }
64
+
65
+ export interface RerankResult {
66
+ memoryId: string
67
+ finalScore: number
68
+ scoreBreakdown: Record<string, number>
69
+ whySelected: string[]
70
+ }
71
+
72
+ export function rerankOne(
73
+ input: RerankInput,
74
+ scopeCtx: ScopeContext,
75
+ weights: RerankWeights = DEFAULT_WEIGHTS,
76
+ ): RerankResult {
77
+ const rank = scopeRank(input.scopeType as Parameters<typeof scopeRank>[0]) / 7
78
+ const scopeBoost = weights.scopeBoost * rank
79
+ const kindBoost = weights.kindBoost[input.kind] ?? 0
80
+ const importanceBoost = weights.importanceBoost * input.importance
81
+ const confidenceBoost = weights.confidenceBoost * input.confidence
82
+ const mandatory =
83
+ input.kind === "CONSTRAINT" && input.status === "ACTIVE" ? weights.mandatoryConstraintBoost : 0
84
+ const stalePenalty = input.status === "STALE" ? weights.stalePenalty : 0
85
+ const conflictPenalty = input.conflicted ? weights.conflictPenalty : 0
86
+ const redundancyPenalty = input.redundant ? weights.redundancyPenalty : 0
87
+
88
+ const finalScore =
89
+ input.rrfScore + scopeBoost + kindBoost + importanceBoost + confidenceBoost + mandatory
90
+ - stalePenalty - conflictPenalty - redundancyPenalty
91
+
92
+ const why: string[] = [`rrf=${input.rrfScore.toFixed(4)}`]
93
+ if (scopeBoost > 0) why.push(`scopeBoost(${input.scopeType}:${input.scopeValue})`)
94
+ if (kindBoost > 0) why.push(`kindBoost(${input.kind})`)
95
+ if (mandatory > 0) why.push("mandatoryConstraint")
96
+ if (stalePenalty > 0) why.push("stalePenalty(诊断模式)")
97
+ if (conflictPenalty > 0) why.push("conflictPenalty")
98
+ if (redundancyPenalty > 0) why.push("redundancyPenalty")
99
+
100
+ return {
101
+ memoryId: input.memoryId,
102
+ finalScore,
103
+ scoreBreakdown: {
104
+ rrfScore: input.rrfScore,
105
+ scopeBoost,
106
+ kindBoost,
107
+ importanceBoost,
108
+ confidenceBoost,
109
+ mandatoryConstraintBoost: mandatory,
110
+ stalePenalty: -stalePenalty,
111
+ conflictPenalty: -conflictPenalty,
112
+ redundancyPenalty: -redundancyPenalty,
113
+ },
114
+ whySelected: why,
115
+ }
116
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ * 检索管线公共类型(Plan §7/§8.1)
3
+ */
4
+ import type { MemoryStatus } from "../domain/state-machine.js"
5
+ import type { ScopeContext } from "../domain/scope.js"
6
+
7
+ export interface RankedId {
8
+ memoryId: string
9
+ rank: number
10
+ score?: number
11
+ }
12
+
13
+ export interface RecallFilter {
14
+ repositoryRef: string
15
+ statuses: readonly MemoryStatus[]
16
+ scopeCtx: ScopeContext
17
+ kinds?: string[]
18
+ /** validUntil 过滤基准时刻 */
19
+ now: Date
20
+ }
21
+
22
+ export interface ComponentHealth {
23
+ component: string
24
+ status: "ok" | "degraded" | "unavailable" | "disabled"
25
+ detail?: string
26
+ }
27
+
28
+ export interface LexicalSearchAdapter {
29
+ readonly id: string
30
+ search(query: string, filter: RecallFilter, limit: number): Promise<RankedId[]>
31
+ health(): Promise<ComponentHealth>
32
+ }
33
+
34
+ /** 最小原生 SQL 查询接口:PG/SQLite adapter 与测试桩共用(生产由 prisma.$queryRawUnsafe 适配) */
35
+ export interface RawQuerier {
36
+ query<T = Record<string, unknown>>(sql: string, params: unknown[]): Promise<T[]>
37
+ }
38
+
39
+ /** recall_memory 返回项(Plan §6.2:必须结构化,不依赖模型从自由文本推断) */
40
+ export interface RecalledMemory {
41
+ memoryId: string
42
+ kind: string
43
+ topic: string
44
+ content: string
45
+ scope: { type: string; value: string }
46
+ confidence: number
47
+ importance: number
48
+ sourceRefs: string[]
49
+ whySelected: string[]
50
+ scoreBreakdown: Record<string, number>
51
+ supersedes: string[]
52
+ }
@@ -0,0 +1,143 @@
1
+ /*
2
+ * pgvector 向量检索适配器(Plan §3.4 轮 3 Task 3.2 / Spec §6 §VectorCapability)
3
+ *
4
+ * 设计要点:
5
+ * 1. **Prisma schema 不表达向量列**(架构文档定案):扩展/表/索引由本适配器的原生 DDL 管理,
6
+ * `ensureSchema()` 幂等可重放;对应 migration 只负责首次交付(见 3.4);
7
+ * 2. **能力检测优先**:`capability()` 先看扩展、再看表;任一缺失都返回 unavailable/degraded,
8
+ * 调用方据此走 fts-only,绝不因向量缺失阻塞召回;
9
+ * 3. **维度真源**:表按 provider 维度建列;写入前校验向量长度,禁止脏向量进库。
10
+ */
11
+ import type { ComponentHealth, RankedId, RecallFilter, RawQuerier } from "../types.js"
12
+ import { MemoryError } from "../../domain/errors.js"
13
+ import type { VectorSearchAdapter } from "../../embedding/index.js"
14
+
15
+ export const PG_VECTOR_TABLE = "add_memory_vector"
16
+ export const PG_VECTOR_EXTENSION = "vector"
17
+
18
+ /** 向量字面量(pgvector 接受 '[1,2,3]' 形式) */
19
+ export function pgVectorLiteral(vector: readonly number[]): string {
20
+ return `[${vector.map((v) => (Number.isFinite(v) ? v : 0)).join(",")}]`
21
+ }
22
+
23
+ export function pgVectorSchemaDdl(dimension: number, table = PG_VECTOR_TABLE): string[] {
24
+ if (!Number.isFinite(dimension) || dimension <= 0) {
25
+ throw new MemoryError("ERR_DIMENSION_MISMATCH", `非法向量维度:${dimension}`)
26
+ }
27
+ return [
28
+ `CREATE EXTENSION IF NOT EXISTS vector;`,
29
+ `CREATE TABLE IF NOT EXISTS "${table}" (
30
+ "memory_id" text PRIMARY KEY,
31
+ "model" text NOT NULL,
32
+ "dim" integer NOT NULL,
33
+ "embedding" vector(${dimension}) NOT NULL,
34
+ "updated_at" timestamptz NOT NULL DEFAULT now()
35
+ );`,
36
+ `CREATE INDEX IF NOT EXISTS "${table}_embedding_hnsw" ON "${table}" USING hnsw ("embedding" vector_cosine_ops);`,
37
+ ]
38
+ }
39
+
40
+ export interface PgVectorAdapterOptions {
41
+ querier: RawQuerier
42
+ /** 维度真源(来自 provider 元数据) */
43
+ dimension: number
44
+ /** 模型标识:检索时按模型过滤,避免混用不同模型向量 */
45
+ model: string
46
+ table?: string
47
+ }
48
+
49
+ export interface PgVectorAdapter extends VectorSearchAdapter {
50
+ capability(): Promise<ComponentHealth>
51
+ ensureSchema(): Promise<{ statements: number }>
52
+ }
53
+
54
+ export function createPgVectorAdapter(opts: PgVectorAdapterOptions): PgVectorAdapter {
55
+ const table = opts.table ?? PG_VECTOR_TABLE
56
+
57
+ function assertDim(vector: readonly number[]): void {
58
+ if (vector.length !== opts.dimension) {
59
+ throw new MemoryError(
60
+ "ERR_DIMENSION_MISMATCH",
61
+ `向量维度不符:实际 ${vector.length} vs 期望 ${opts.dimension}`,
62
+ )
63
+ }
64
+ }
65
+
66
+ async function capability(): Promise<ComponentHealth> {
67
+ try {
68
+ const ext = await opts.querier.query<{ name: string }>(
69
+ `SELECT extname AS name FROM pg_extension WHERE extname = $1`,
70
+ [PG_VECTOR_EXTENSION],
71
+ )
72
+ if (ext.length === 0) {
73
+ return {
74
+ component: "vector-search",
75
+ status: "unavailable",
76
+ detail: `pgvector 扩展未安装(table=${table})→ 降级 fts-only`,
77
+ }
78
+ }
79
+ const tbl = await opts.querier.query<{ name: string | null }>(
80
+ `SELECT to_regclass($1)::text AS name`,
81
+ [`public.${table}`],
82
+ )
83
+ if (!tbl[0]?.name) {
84
+ return {
85
+ component: "vector-search",
86
+ status: "degraded",
87
+ detail: `pgvector 已装但表 ${table} 缺失 → 需 ensureSchema()`,
88
+ }
89
+ }
90
+ return { component: "vector-search", status: "ok", detail: `pgvector table=${table} dim=${opts.dimension}` }
91
+ } catch (error) {
92
+ return {
93
+ component: "vector-search",
94
+ status: "unavailable",
95
+ detail: `pgvector 探测失败:${error instanceof Error ? error.message : String(error)}`,
96
+ }
97
+ }
98
+ }
99
+
100
+ return {
101
+ async ensureSchema(): Promise<{ statements: number }> {
102
+ const ddl = pgVectorSchemaDdl(opts.dimension, table)
103
+ for (const sql of ddl) await opts.querier.query(sql, [])
104
+ return { statements: ddl.length }
105
+ },
106
+
107
+ async upsert(memoryId: string, vector: number[], model: string): Promise<void> {
108
+ assertDim(vector)
109
+ await opts.querier.query(
110
+ `INSERT INTO "${table}" ("memory_id", "model", "dim", "embedding")
111
+ VALUES ($1, $2, $3, $4::vector)
112
+ ON CONFLICT ("memory_id") DO UPDATE
113
+ SET "model" = EXCLUDED."model",
114
+ "dim" = EXCLUDED."dim",
115
+ "embedding" = EXCLUDED."embedding",
116
+ "updated_at" = now()`,
117
+ [memoryId, model, vector.length, pgVectorLiteral(vector)],
118
+ )
119
+ },
120
+
121
+ async search(vector: number[], filter: RecallFilter, limit: number): Promise<RankedId[]> {
122
+ assertDim(vector)
123
+ const rows = await opts.querier.query<{ memory_id: string; score: number | string }>(
124
+ `SELECT v."memory_id" AS memory_id, 1 - (v."embedding" <=> $1::vector) AS score
125
+ FROM "${table}" v
126
+ JOIN "AddMemory" m ON m."id" = v."memory_id"
127
+ WHERE m."repositoryRef" = $2
128
+ AND m."status"::text = ANY($3)
129
+ AND v."model" = $4
130
+ ORDER BY v."embedding" <=> $1::vector
131
+ LIMIT $5`,
132
+ [pgVectorLiteral(vector), filter.repositoryRef, [...filter.statuses], opts.model, limit],
133
+ )
134
+ return rows.map((r, i) => ({ memoryId: r.memory_id, rank: i + 1, score: Number(r.score) }))
135
+ },
136
+
137
+ async health(): Promise<ComponentHealth> {
138
+ return capability()
139
+ },
140
+
141
+ capability,
142
+ }
143
+ }
@@ -0,0 +1,118 @@
1
+ /*
2
+ * sqlite-vec 向量检索适配器(Plan §3.4 轮 3 Task 3.2 / Spec §6 §VectorCapability)
3
+ *
4
+ * ⚠️ 平台说明(诚实登记):sqlite-vec 是原生扩展,**本仓库当前环境未安装**,因此本文件的
5
+ * SQL 路径只在扩展存在时才被执行(capability 探测先行)。DDL 与函数名集中在文件顶部常量,
6
+ * 便于在具备扩展的环境上校正;未安装时 `capability()` 返回 unavailable,召回走 fts-only。
7
+ *
8
+ * 与 pgvector 适配器同契约:能力检测 → 幂等 ensureSchema → 维度校验 → upsert/search。
9
+ */
10
+ import type { ComponentHealth, RankedId, RecallFilter, RawQuerier } from "../types.js"
11
+ import { MemoryError } from "../../domain/errors.js"
12
+ import type { VectorSearchAdapter } from "../../embedding/index.js"
13
+
14
+ export const SQLITE_VEC_TABLE = "add_memory_vector"
15
+ /** 能力探测:vec0 模块是否注册(只读) */
16
+ export const SQLITE_VEC_CAPABILITY_SQL = `SELECT 1 AS ok FROM pragma_module_list WHERE name = 'vec0'`
17
+ /** 距离函数(余弦距离,越小越近) */
18
+ export const SQLITE_VEC_DISTANCE_FN = "vec_distance_cosine"
19
+
20
+ export function sqliteVecSchemaDdl(dimension: number, table = SQLITE_VEC_TABLE): string[] {
21
+ if (!Number.isFinite(dimension) || dimension <= 0) {
22
+ throw new MemoryError("ERR_DIMENSION_MISMATCH", `非法向量维度:${dimension}`)
23
+ }
24
+ return [
25
+ `CREATE VIRTUAL TABLE IF NOT EXISTS ${table} USING vec0(memory_id text primary key, embedding float[${dimension}]);`,
26
+ `CREATE TABLE IF NOT EXISTS ${table}_meta (memory_id text PRIMARY KEY, model text NOT NULL, dim integer NOT NULL, updated_at text NOT NULL DEFAULT (datetime('now')));`,
27
+ ]
28
+ }
29
+
30
+ export interface SqliteVecAdapterOptions {
31
+ querier: RawQuerier
32
+ dimension: number
33
+ model: string
34
+ table?: string
35
+ }
36
+
37
+ export interface SqliteVecAdapter extends VectorSearchAdapter {
38
+ capability(): Promise<ComponentHealth>
39
+ ensureSchema(): Promise<{ statements: number }>
40
+ }
41
+
42
+ export function createSqliteVecAdapter(opts: SqliteVecAdapterOptions): SqliteVecAdapter {
43
+ const table = opts.table ?? SQLITE_VEC_TABLE
44
+
45
+ function assertDim(vector: readonly number[]): void {
46
+ if (vector.length !== opts.dimension) {
47
+ throw new MemoryError(
48
+ "ERR_DIMENSION_MISMATCH",
49
+ `向量维度不符:实际 ${vector.length} vs 期望 ${opts.dimension}`,
50
+ )
51
+ }
52
+ }
53
+
54
+ async function capability(): Promise<ComponentHealth> {
55
+ try {
56
+ const rows = await opts.querier.query<{ ok: number }>(SQLITE_VEC_CAPABILITY_SQL, [])
57
+ if (rows.length === 0) {
58
+ return {
59
+ component: "vector-search",
60
+ status: "unavailable",
61
+ detail: "sqlite-vec(vec0) 未安装 → 降级 fts-only",
62
+ }
63
+ }
64
+ return { component: "vector-search", status: "ok", detail: `sqlite-vec table=${table} dim=${opts.dimension}` }
65
+ } catch (error) {
66
+ return {
67
+ component: "vector-search",
68
+ status: "unavailable",
69
+ detail: `sqlite-vec 探测失败:${error instanceof Error ? error.message : String(error)}`,
70
+ }
71
+ }
72
+ }
73
+
74
+ return {
75
+ async ensureSchema(): Promise<{ statements: number }> {
76
+ const ddl = sqliteVecSchemaDdl(opts.dimension, table)
77
+ for (const sql of ddl) await opts.querier.query(sql, [])
78
+ return { statements: ddl.length }
79
+ },
80
+
81
+ async upsert(memoryId: string, vector: number[], model: string): Promise<void> {
82
+ assertDim(vector)
83
+ // vec_f32 接受 JSON 文本形式;绑定为字符串参数
84
+ await opts.querier.query(
85
+ `INSERT INTO ${table}(memory_id, embedding) VALUES (?, vec_f32(?))
86
+ ON CONFLICT(memory_id) DO UPDATE SET embedding = vec_f32(?)`,
87
+ [memoryId, JSON.stringify(vector), JSON.stringify(vector)],
88
+ )
89
+ await opts.querier.query(
90
+ `INSERT INTO ${table}_meta(memory_id, model, dim) VALUES (?, ?, ?)
91
+ ON CONFLICT(memory_id) DO UPDATE SET model = excluded.model, dim = excluded.dim`,
92
+ [memoryId, model, vector.length],
93
+ )
94
+ },
95
+
96
+ async search(vector: number[], filter: RecallFilter, limit: number): Promise<RankedId[]> {
97
+ assertDim(vector)
98
+ const rows = await opts.querier.query<{ memory_id: string; distance: number | string }>(
99
+ `SELECT v.memory_id AS memory_id, ${SQLITE_VEC_DISTANCE_FN}(v.embedding, vec_f32(?)) AS distance
100
+ FROM ${table} v
101
+ JOIN ${table}_meta meta ON meta.memory_id = v.memory_id
102
+ JOIN "AddMemory" m ON m.id = v.memory_id
103
+ WHERE m.repositoryRef = ?
104
+ AND meta.model = ?
105
+ ORDER BY distance ASC
106
+ LIMIT ?`,
107
+ [JSON.stringify(vector), filter.repositoryRef, opts.model, limit],
108
+ )
109
+ return rows.map((r, i) => ({ memoryId: r.memory_id, rank: i + 1, score: 1 - Number(r.distance) }))
110
+ },
111
+
112
+ async health(): Promise<ComponentHealth> {
113
+ return capability()
114
+ },
115
+
116
+ capability,
117
+ }
118
+ }
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Memory 子系统发布开关(Spec §10,Plan §9.3)
3
+ *
4
+ * 约束:governance Hook(同步 spawn 子进程,≤200ms 预算)与 jobs(异步)共用本模块,
5
+ * 因此全部函数必须是同步、纯环境变量读取,禁止任何 IO/DB 访问。
6
+ *
7
+ * 开关:
8
+ * - ADD_MEMORY_RECALL_MODE = off | shadow | inject(默认 shadow:召回可执行并落审计,但 Hook 不注入上下文)
9
+ * - ADD_MEMORY_MAX_TOKENS:L1 注入 token 预算(默认 600)
10
+ * - ADD_MEMORY_EVIDENCE = off | on(默认 on:PostToolUse 白名单采证入队)
11
+ */
12
+
13
+ export type RecallMode = "off" | "shadow" | "inject"
14
+
15
+ export function recallMode(env: NodeJS.ProcessEnv = process.env): RecallMode {
16
+ const v = (env.ADD_MEMORY_RECALL_MODE ?? "shadow").toLowerCase()
17
+ return v === "off" || v === "inject" ? v : "shadow"
18
+ }
19
+
20
+ export function memoryMaxTokens(env: NodeJS.ProcessEnv = process.env): number {
21
+ const n = Number(env.ADD_MEMORY_MAX_TOKENS ?? 600)
22
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600
23
+ }
24
+
25
+ export function evidenceEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
26
+ return (env.ADD_MEMORY_EVIDENCE ?? "on").toLowerCase() !== "off"
27
+ }
28
+
29
+ /** 记忆工作目录(快照 + 事件队列所在):${magicDir}/memory/ */
30
+ export const MEMORY_DIR_NAME = "memory"
31
+ /** L1/L2 快照文件名 */
32
+ export const L1_SNAPSHOT_FILE = "l1-context.md"
33
+ export const L2_SNAPSHOT_FILE = "l2-context.md"
34
+ /** PostToolUse 采证事件队列(jsonl,append-only,由 consolidation job 消费) */
35
+ export const EVIDENCE_QUEUE_FILE = "evidence-queue.jsonl"
36
+ /** evidence 队列消费进度标记(已处理字节偏移) */
37
+ export const EVIDENCE_OFFSET_FILE = "evidence-queue.offset"
38
+ /** L1 快照新鲜度上限(毫秒,默认 7 天):过期不注入 */
39
+ export const L1_SNAPSHOT_TTL_MS = 7 * 24 * 60 * 60 * 1000
@@ -0,0 +1,22 @@
1
+ /*
2
+ * 审查文档命名识别(纯函数;2026-09-14 从 tools/review.ts 抽出)
3
+ *
4
+ * 抽出原因:① 纯函数不该和 prisma/工具注册绑在一起(拖进来就要求 DB 依赖,测试得打 mock);
5
+ * ② 仓库里并存两套命名,识别规则需要被用例直接钉住:
6
+ * - `{planPrefix}-review-v1.md`(历史)
7
+ * - `{planPrefix}-plan-v1-review.md`(create_hitl 的占位路径落点/现行)
8
+ * 旧匹配要求包含 `-review-`(review 后必须紧跟连字符),会把后一整类漏掉。
9
+ */
10
+ import { basename } from "path"
11
+
12
+ /** 从文件列表里挑出审查文档(.md 且文件名含 review,大小写不敏感) */
13
+ export function pickReviewFiles(files: readonly string[]): string[] {
14
+ return files.filter((f) => f.endsWith(".md") && basename(f).toLowerCase().includes("review"))
15
+ }
16
+
17
+ /** 从审查文件名推导 planName 前缀(两种命名都要剥干净) */
18
+ export function derivePlanNameFromReviewFile(file: string): string {
19
+ return basename(file, ".md")
20
+ .replace(/-review-(implementation|runtime).*$/, "") // 变体后缀
21
+ .replace(/-review.*$/, "") // 普通 review 后缀(含 -review-v1 / -review)
22
+ }