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.
- package/dist/index.js +21 -3
- package/package.json +3 -3
- package/templates/.add-coder-src-hash.json +95 -41
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/claude/hooks/session-start.mjs +65 -1
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/codex/hooks/session-start.mjs +65 -1
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/trae/hooks/session-start.mjs +65 -1
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
- package/templates/core/governance/doc-format-guard.ts +29 -112
- package/templates/core/governance/post-tool-router.ts +33 -1
- package/templates/core/governance/prompt-router.ts +47 -0
- package/templates/core/governance/session-start-guard.ts +48 -1
- package/templates/core/prisma/add.prisma +203 -0
- package/templates/core/scripts/db-ensure.sh +61 -2
- package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
- package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
- package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
- package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
- package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
- package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
- package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
- package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
- package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
- package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
- package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
- package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
- package/templates/core/scripts/mcp-server.ts +36 -0
- package/templates/core/templates/checklist-template.md +13 -0
- package/templates/core/templates/review-implementation-template.md +24 -0
- package/templates/core/templates/review-template.md +16 -0
- package/templates/core/validation/index.ts +136 -0
- package/templates/core/validation/policy.ts +91 -0
- package/templates/core/validation/registry.ts +61 -0
- package/templates/core/validation/schema-validator.ts +277 -0
- package/templates/core/validation/validators/add-route.ts +32 -0
- package/templates/core/validation/validators/checklist.ts +48 -0
- package/templates/core/validation/validators/handoff.ts +46 -0
- package/templates/core/validation/validators/hitl.ts +22 -0
- package/templates/core/validation/validators/index.ts +52 -0
- package/templates/core/validation/validators/plan.ts +20 -0
- package/templates/core/validation/validators/report.ts +16 -0
- package/templates/core/validation/validators/review.ts +30 -0
- package/templates/core/validation/validators/spec.ts +25 -0
- package/templates/core/validation/validators/tasks.ts +40 -0
- package/templates/core/validation/validators/types.ts +32 -0
- package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
- 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
|
+
}
|