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.
- package/dist/index.js +21 -3
- package/package.json +3 -3
- package/templates/.add-coder-src-hash.json +144 -90
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/claude/hooks/notification.mjs +0 -4
- package/templates/adapters/claude/hooks/permission-denied.mjs +0 -4
- package/templates/adapters/claude/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/claude/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/claude/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/claude/hooks/session-end.mjs +0 -4
- package/templates/adapters/claude/hooks/session-start.mjs +65 -5
- package/templates/adapters/claude/hooks/stop-check.mjs +6 -87
- package/templates/adapters/claude/hooks/stop-failure.mjs +0 -4
- package/templates/adapters/claude/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/claude/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/codex/hooks/notification.mjs +0 -4
- package/templates/adapters/codex/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/codex/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/codex/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/codex/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/codex/hooks/session-end.mjs +0 -4
- package/templates/adapters/codex/hooks/session-start.mjs +65 -5
- package/templates/adapters/codex/hooks/stop-check.mjs +6 -87
- package/templates/adapters/codex/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/codex/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/qoder/hooks/notification.mjs +0 -4
- package/templates/adapters/qoder/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/qoder/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/qoder/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/qoder/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/qoder/hooks/review-checklist.mjs +0 -4
- package/templates/adapters/qoder/hooks/session-end.mjs +0 -4
- package/templates/adapters/qoder/hooks/session-start.mjs +67 -5
- package/templates/adapters/qoder/hooks/stop-check.mjs +6 -87
- package/templates/adapters/qoder/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/qoder/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/trae/hooks/notification.mjs +0 -4
- package/templates/adapters/trae/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/trae/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/trae/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/trae/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/trae/hooks/session-end.mjs +0 -4
- package/templates/adapters/trae/hooks/session-start.mjs +65 -5
- package/templates/adapters/trae/hooks/stop-check.mjs +6 -87
- package/templates/adapters/trae/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/trae/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/vscode/hooks/notification.mjs +0 -4
- package/templates/adapters/vscode/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/vscode/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/vscode/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/vscode/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/vscode/hooks/session-end.mjs +0 -4
- package/templates/adapters/vscode/hooks/session-start.mjs +65 -5
- package/templates/adapters/vscode/hooks/stop-check.mjs +6 -87
- package/templates/adapters/vscode/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/vscode/hooks/subagent-stop.mjs +0 -4
- 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/rules.ts +0 -4
- package/templates/core/governance/session-start-guard.ts +48 -1
- package/templates/core/governance/stop-router.ts +7 -101
- 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,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
|
+
}
|