add-coder 0.3.33 → 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 (98) hide show
  1. package/dist/index.js +21 -3
  2. package/package.json +3 -3
  3. package/templates/.add-coder-src-hash.json +95 -41
  4. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
  5. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
  6. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
  7. package/templates/adapters/claude/hooks/session-start.mjs +65 -1
  8. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
  9. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
  10. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
  11. package/templates/adapters/codex/hooks/session-start.mjs +65 -1
  12. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
  13. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
  14. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
  15. package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
  16. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
  17. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
  18. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
  19. package/templates/adapters/trae/hooks/session-start.mjs +65 -1
  20. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
  21. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
  22. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
  23. package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
  24. package/templates/core/governance/doc-format-guard.ts +29 -112
  25. package/templates/core/governance/post-tool-router.ts +33 -1
  26. package/templates/core/governance/prompt-router.ts +47 -0
  27. package/templates/core/governance/session-start-guard.ts +48 -1
  28. package/templates/core/prisma/add.prisma +203 -0
  29. package/templates/core/scripts/db-ensure.sh +61 -2
  30. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  31. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  32. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  33. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  34. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  35. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  36. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  37. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  38. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  39. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  40. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  41. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  42. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  43. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  44. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  45. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  46. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  47. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  48. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  49. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  50. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  51. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  52. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  53. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  54. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  55. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  56. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  57. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  58. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  59. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  60. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  61. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  62. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  63. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  64. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  65. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  66. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  67. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  68. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  69. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
  70. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  71. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  72. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  73. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  74. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  75. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  76. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  77. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  78. package/templates/core/scripts/mcp-server.ts +36 -0
  79. package/templates/core/templates/checklist-template.md +13 -0
  80. package/templates/core/templates/review-implementation-template.md +24 -0
  81. package/templates/core/templates/review-template.md +16 -0
  82. package/templates/core/validation/index.ts +136 -0
  83. package/templates/core/validation/policy.ts +91 -0
  84. package/templates/core/validation/registry.ts +61 -0
  85. package/templates/core/validation/schema-validator.ts +277 -0
  86. package/templates/core/validation/validators/add-route.ts +32 -0
  87. package/templates/core/validation/validators/checklist.ts +48 -0
  88. package/templates/core/validation/validators/handoff.ts +46 -0
  89. package/templates/core/validation/validators/hitl.ts +22 -0
  90. package/templates/core/validation/validators/index.ts +52 -0
  91. package/templates/core/validation/validators/plan.ts +20 -0
  92. package/templates/core/validation/validators/report.ts +16 -0
  93. package/templates/core/validation/validators/review.ts +30 -0
  94. package/templates/core/validation/validators/spec.ts +25 -0
  95. package/templates/core/validation/validators/tasks.ts +40 -0
  96. package/templates/core/validation/validators/types.ts +32 -0
  97. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
  98. package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
@@ -0,0 +1,43 @@
1
+ /*
2
+ * RRF 融合(Plan §7.3):RRF(d) = Σ 1 / (k + rank_i(d))
3
+ * 避免强行比较 PG、SQLite 和不同 FTS 实现的原始分值。
4
+ * k 可配置化(写入 Recall.rankingVersion 的配置快照)。
5
+ */
6
+ import type { RankedId } from "./types.js"
7
+
8
+ export const DEFAULT_RRF_K = 10
9
+
10
+ /**
11
+ * 加权 RRF:score(d) = Σ_i w_i / (k + rank_i(d))
12
+ * 通道权重让「高精度通道」主导、低精度通道只做补充(Plan 轮 3 融合迭代)。
13
+ * weights 缺省为 1(退化为经典 RRF,保持向后兼容)。
14
+ */
15
+ export function rrfFuse(
16
+ lists: RankedId[][],
17
+ k: number = DEFAULT_RRF_K,
18
+ weights?: readonly number[],
19
+ ): Map<string, number> {
20
+ const scores = new Map<string, number>()
21
+ lists.forEach((list, i) => {
22
+ const w = weights?.[i] ?? 1
23
+ if (w <= 0) return
24
+ for (const item of list) {
25
+ const prev = scores.get(item.memoryId) ?? 0
26
+ scores.set(item.memoryId, prev + w / (k + item.rank))
27
+ }
28
+ })
29
+ return scores
30
+ }
31
+
32
+ /** 便捷入口:融合后按分数降序返回有序 id 列表 */
33
+ export function rrfRank(
34
+ lists: RankedId[][],
35
+ k: number = DEFAULT_RRF_K,
36
+ weights?: readonly number[],
37
+ ): RankedId[] {
38
+ const scores = rrfFuse(lists, k, weights)
39
+ return [...scores.entries()]
40
+ .map(([memoryId, score]) => ({ memoryId, rank: 0, score }))
41
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
42
+ .map((r, i) => ({ ...r, rank: i + 1 }))
43
+ }
@@ -0,0 +1,285 @@
1
+ /*
2
+ * Hybrid Recall 编排管线(Plan §7.1 十步流水线)
3
+ *
4
+ * 1. Repository/tenant 边界校验(调用方前置,此处防御性复核)
5
+ * 2. Lifecycle 过滤(默认仅 ACTIVE;诊断模式放行 STALE)
6
+ * 3. Scope 过滤与强约束匹配
7
+ * 4. FTS/BM25 候选生成(双后端 adapter)
8
+ * 5. Vector 候选生成(能力可用时;首版 none)
9
+ * 6. Reciprocal Rank Fusion
10
+ * 7. 治理重排
11
+ * 8. 冲突、重复和多样性处理
12
+ * 9. Token-budget 摘要与裁剪
13
+ * 10. 写入 AddMemoryRecall
14
+ */
15
+ import { DEFAULT_RECALL_STATUSES, DIAGNOSTIC_RECALL_STATUSES, type MemoryStatus } from "../domain/state-machine.js"
16
+ import { scopeApplies, type ScopeContext } from "../domain/scope.js"
17
+ import { rrfFuse, DEFAULT_RRF_K } from "./fusion.js"
18
+ import {
19
+ rerankOne,
20
+ DEFAULT_WEIGHTS,
21
+ RANKING_VERSION,
22
+ RANKING_VERSION_HYBRID,
23
+ RRF_SCORE_SCALE,
24
+ type RerankWeights,
25
+ } from "./reranker.js"
26
+ import { buildContext, estimateTokens } from "./context-builder.js"
27
+ import { writeRecallAudit, type RecallAuditStore } from "./recall-writer.js"
28
+ import type { LexicalSearchAdapter, RankedId, RecallFilter, RecalledMemory } from "./types.js"
29
+
30
+ /** 向量通道默认权重与候选预算(Plan 轮 3 融合迭代:低精度通道不与词法等权) */
31
+ export const DEFAULT_VECTOR_WEIGHT = 0.3
32
+ export const DEFAULT_VECTOR_TOP_K = 5
33
+
34
+ /** 管线需要的记忆行字段子集(与 AddMemoryRow 对齐) */
35
+ export interface MemoryRowLike {
36
+ id: string
37
+ kind: string
38
+ status: string
39
+ topic: string
40
+ content: string
41
+ summary: string | null
42
+ scopeType: string
43
+ scopeValue: string
44
+ repositoryRef: string
45
+ importance: number
46
+ confidence: number
47
+ validUntil: Date | null
48
+ supersedes?: { id: string }[]
49
+ }
50
+
51
+ export interface RecallPipelineInput {
52
+ query: string
53
+ stage: string
54
+ repositoryRef: string
55
+ scopeCtx: ScopeContext
56
+ maxTokens: number
57
+ kinds?: string[]
58
+ limit?: number
59
+ consumerRef?: string
60
+ /** 诊断模式:放行 STALE(带警告标记),默认 false */
61
+ diagnostic?: boolean
62
+ }
63
+
64
+ export interface RecallPipelineDeps {
65
+ lexical: LexicalSearchAdapter[]
66
+ /** Vector 候选(可选;首版不传即 FTS-only) */
67
+ vector?: { search(query: string, filter: RecallFilter, limit: number): Promise<RankedId[]> } | null
68
+ /** 向量通道权重(RRF 加权;默认 DEFAULT_VECTOR_WEIGHT —— 低精度通道不与词法等权) */
69
+ vectorWeight?: number
70
+ /** 向量候选预算(默认 DEFAULT_VECTOR_TOP_K —— 不按总 limit 灌入,避免稀释词法信号) */
71
+ vectorTopK?: number
72
+ /** 补位模式阈值:仅当词法候选数 < 该值时启用向量通道(缺省=始终补充) */
73
+ vectorFallbackThreshold?: number
74
+ fetchByIds(ids: string[]): Promise<MemoryRowLike[]>
75
+ fetchEvidenceSourceRefs(memoryIds: string[]): Promise<Map<string, string[]>>
76
+ audit?: RecallAuditStore | null
77
+ weights?: RerankWeights
78
+ rrfK?: number
79
+ rankingVersion?: string
80
+ degradedMode?: string
81
+ now?: Date
82
+ }
83
+
84
+ export interface RecallPipelineResult {
85
+ items: RecalledMemory[]
86
+ recallId: string | null
87
+ degradedMode: string | null
88
+ /** 实际参与融合的候选通道(Spec §7 RecallResultMeta) */
89
+ fusedChannels: ("lexical" | "vector")[]
90
+ excluded: { memoryId: string; reason: string }[]
91
+ candidateCount: number
92
+ injectedTokens: number
93
+ latencyMs: number
94
+ rankingVersion: string
95
+ }
96
+
97
+ export async function recallPipeline(
98
+ input: RecallPipelineInput,
99
+ deps: RecallPipelineDeps,
100
+ ): Promise<RecallPipelineResult> {
101
+ const start = Date.now()
102
+ const now = deps.now ?? new Date()
103
+ const statuses: readonly MemoryStatus[] = input.diagnostic ? DIAGNOSTIC_RECALL_STATUSES : DEFAULT_RECALL_STATUSES
104
+ const limit = input.limit ?? 20
105
+
106
+ // Step 1(防御性复核):repository 边界
107
+ if (input.scopeCtx.repository !== input.repositoryRef) {
108
+ throw new Error(`ERR_REPOSITORY_MISMATCH: scopeCtx.repository=${input.scopeCtx.repository} 与 repositoryRef 不一致`)
109
+ }
110
+
111
+ const filter: RecallFilter = {
112
+ repositoryRef: input.repositoryRef,
113
+ statuses,
114
+ scopeCtx: input.scopeCtx,
115
+ kinds: input.kinds,
116
+ now,
117
+ }
118
+
119
+ // Step 4/5:候选生成(FTS 多通道 + 可选 Vector)
120
+ const channelLists: RankedId[][] = []
121
+ let lexicalChannels = 0
122
+ for (const adapter of deps.lexical) {
123
+ const multi = adapter as LexicalSearchAdapter & {
124
+ searchChannels?(q: string, f: RecallFilter, l: number): Promise<RankedId[][]>
125
+ }
126
+ if (typeof multi.searchChannels === "function") {
127
+ const channels = await multi.searchChannels(input.query, filter, limit)
128
+ channelLists.push(...channels)
129
+ lexicalChannels += channels.length
130
+ } else {
131
+ channelLists.push(await adapter.search(input.query, filter, limit))
132
+ lexicalChannels += 1
133
+ }
134
+ }
135
+ let vectorUsed = false
136
+ const channelWeights: number[] = channelLists.map(() => 1) // 词法通道权重恒为 1
137
+ if (deps.vector) {
138
+ const lexicalCandidates = new Set(channelLists.flat().map((c) => c.memoryId)).size
139
+ const fallbackOnly = deps.vectorFallbackThreshold != null
140
+ const shouldUseVector = !fallbackOnly || lexicalCandidates < (deps.vectorFallbackThreshold as number)
141
+ try {
142
+ if (shouldUseVector) {
143
+ const vectorCandidates = await deps.vector.search(
144
+ input.query,
145
+ filter,
146
+ Math.min(deps.vectorTopK ?? DEFAULT_VECTOR_TOP_K, limit),
147
+ )
148
+ channelLists.push(vectorCandidates)
149
+ channelWeights.push(deps.vectorWeight ?? DEFAULT_VECTOR_WEIGHT)
150
+ vectorUsed = true
151
+ }
152
+ } catch {
153
+ // Vector 故障不阻塞 FTS(Plan §8.4)
154
+ }
155
+ }
156
+ const fusedChannels: ("lexical" | "vector")[] = [
157
+ ...(lexicalChannels > 0 ? (["lexical"] as const) : []),
158
+ ...(vectorUsed ? (["vector"] as const) : []),
159
+ ]
160
+ // rankingVersion:向量通道真正参与融合才记 v2;否则保持 v1(可被调用方显式覆盖)
161
+ const effectiveRankingVersion =
162
+ deps.rankingVersion ?? (vectorUsed ? RANKING_VERSION_HYBRID : RANKING_VERSION)
163
+
164
+ // Step 6:RRF 融合
165
+ const fused = rrfFuse(channelLists, deps.rrfK ?? DEFAULT_RRF_K, channelWeights)
166
+ const candidateIds = [...fused.keys()]
167
+ if (candidateIds.length === 0) {
168
+ const empty: RecallPipelineResult = {
169
+ items: [], recallId: null, degradedMode: deps.degradedMode ?? null,
170
+ fusedChannels,
171
+ excluded: [], candidateCount: 0, injectedTokens: 0,
172
+ latencyMs: Date.now() - start, rankingVersion: effectiveRankingVersion,
173
+ }
174
+ if (deps.audit) {
175
+ empty.recallId = await writeRecallAudit(deps.audit, {
176
+ repositoryRef: input.repositoryRef, query: input.query, stage: input.stage,
177
+ consumerRef: input.consumerRef, scopeContext: input.scopeCtx,
178
+ candidateIds: [], items: [], excluded: [],
179
+ rankingVersion: empty.rankingVersion, tokenBudget: input.maxTokens,
180
+ injectedTokens: 0, latencyMs: empty.latencyMs, degradedMode: empty.degradedMode ?? undefined,
181
+ })
182
+ }
183
+ return empty
184
+ }
185
+
186
+ // Step 2/3:取行 + lifecycle/scope 防御性复核(SQL 已过滤,此处兜底语义一致性)
187
+ const rows = await deps.fetchByIds(candidateIds)
188
+ const rowById = new Map(rows.map((r) => [r.id, r]))
189
+ const eligible = rows.filter((r) => {
190
+ if (r.repositoryRef !== input.repositoryRef) return false
191
+ if (!statuses.includes(r.status as MemoryStatus)) return false
192
+ if (r.validUntil && r.validUntil <= now) return false
193
+ return scopeApplies(
194
+ { type: r.scopeType as Parameters<typeof scopeApplies>[0]["type"], value: r.scopeValue },
195
+ input.scopeCtx,
196
+ )
197
+ })
198
+ const scopeExcluded = candidateIds
199
+ .filter((id) => rowById.has(id) && !eligible.some((r) => r.id === id))
200
+ .map((id) => ({ memoryId: id, reason: "lifecycle/scope/有效期过滤" }))
201
+
202
+ // Step 7/8:治理重排
203
+ const reranked = eligible.map((r) => ({
204
+ row: r,
205
+ rr: rerankOne({
206
+ memoryId: r.id,
207
+ // 相关性主导:RRF 放大到与治理 boost 可比量级(见 RRF_SCORE_SCALE 注释)
208
+ rrfScore: (fused.get(r.id) ?? 0) * RRF_SCORE_SCALE,
209
+ kind: r.kind,
210
+ status: r.status as MemoryStatus,
211
+ importance: r.importance,
212
+ confidence: r.confidence,
213
+ scopeType: r.scopeType,
214
+ scopeValue: r.scopeValue,
215
+ }, input.scopeCtx, deps.weights ?? DEFAULT_WEIGHTS),
216
+ }))
217
+
218
+ // Step 9:token 预算
219
+ const budgetItems = reranked.map(({ row, rr }) => ({
220
+ memoryId: row.id,
221
+ kind: row.kind,
222
+ finalScore: rr.finalScore,
223
+ tokens: estimateTokens(row.summary ?? row.content),
224
+ content: row.content,
225
+ sourceRefs: [] as string[],
226
+ _why: rr.whySelected,
227
+ _breakdown: rr.scoreBreakdown,
228
+ }))
229
+ const budget = buildContext(budgetItems, input.maxTokens)
230
+
231
+ const evidenceRefs = await deps.fetchEvidenceSourceRefs(budget.selected.map((s) => s.memoryId))
232
+ const items: RecalledMemory[] = budget.selected.map((s) => {
233
+ const row = rowById.get(s.memoryId)!
234
+ const extra = budgetItems.find((b) => b.memoryId === s.memoryId)!
235
+ return {
236
+ memoryId: s.memoryId,
237
+ kind: row.kind,
238
+ topic: row.topic,
239
+ content: row.content,
240
+ scope: { type: row.scopeType, value: row.scopeValue },
241
+ confidence: row.confidence,
242
+ importance: row.importance,
243
+ sourceRefs: evidenceRefs.get(s.memoryId) ?? s.sourceRefs,
244
+ whySelected: extra._why,
245
+ scoreBreakdown: extra._breakdown,
246
+ supersedes: (row.supersedes ?? []).map((x) => x.id),
247
+ }
248
+ })
249
+
250
+ const excluded = [...scopeExcluded, ...budget.excluded]
251
+ const latencyMs = Date.now() - start
252
+ const injectedTokens = budget.usedTokens
253
+
254
+ // Step 10:审计落库
255
+ let recallId: string | null = null
256
+ if (deps.audit) {
257
+ recallId = await writeRecallAudit(deps.audit, {
258
+ repositoryRef: input.repositoryRef,
259
+ query: input.query,
260
+ stage: input.stage,
261
+ consumerRef: input.consumerRef,
262
+ scopeContext: input.scopeCtx,
263
+ candidateIds,
264
+ items,
265
+ excluded,
266
+ rankingVersion: effectiveRankingVersion,
267
+ tokenBudget: input.maxTokens,
268
+ injectedTokens,
269
+ latencyMs,
270
+ degradedMode: deps.degradedMode,
271
+ })
272
+ }
273
+
274
+ return {
275
+ items,
276
+ recallId,
277
+ degradedMode: deps.degradedMode ?? null,
278
+ fusedChannels,
279
+ excluded,
280
+ candidateCount: candidateIds.length,
281
+ injectedTokens,
282
+ latencyMs,
283
+ rankingVersion: effectiveRankingVersion,
284
+ }
285
+ }
@@ -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
+ }