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,654 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Memory 治理面 MCP 工具(Spec §8,Plan §6.1/§6.2)
|
|
3
|
+
*
|
|
4
|
+
* 8 个 MVP 工具:
|
|
5
|
+
* propose_memory / recall_memory / get_memory / list_memories
|
|
6
|
+
* review_memory / resolve_memory / feedback_memory / get_memory_health
|
|
7
|
+
*
|
|
8
|
+
* 横切契约:
|
|
9
|
+
* - 所有写工具与读工具均强制 repositoryRef === runtimeContext.projectKey(ERR_REPOSITORY_MISMATCH)
|
|
10
|
+
* - 错误一律稳定错误码(domain/errors.ts),不返回自由文本堆栈
|
|
11
|
+
* - 召回降级必须 degradedMode 明示
|
|
12
|
+
* - 状态迁移走领域状态机 assertTransition + AuditLog 打点(ADD-5)
|
|
13
|
+
*/
|
|
14
|
+
import * as z from "zod/v4"
|
|
15
|
+
import type { ToolRegistrar } from "./registrar.js"
|
|
16
|
+
import { textResponse, errorResponse } from "../shared/response.js"
|
|
17
|
+
import { prisma } from "../shared/prisma.js"
|
|
18
|
+
import { DATABASE_URL, getRuntimeContext } from "../shared/env.js"
|
|
19
|
+
import {
|
|
20
|
+
validatedDelegate,
|
|
21
|
+
AddMemoryRowSchema,
|
|
22
|
+
AddMemoryEvidenceRowSchema,
|
|
23
|
+
AddMemoryEvidenceLinkRowSchema,
|
|
24
|
+
AddMemoryRecallRowSchema,
|
|
25
|
+
AddMemoryRecallItemRowSchema,
|
|
26
|
+
AuditLogRowSchema,
|
|
27
|
+
AddUserRowSchema,
|
|
28
|
+
type AddMemoryRow,
|
|
29
|
+
type AddMemoryEvidenceRow,
|
|
30
|
+
type AddMemoryEvidenceLinkRow,
|
|
31
|
+
type AddMemoryRecallRow,
|
|
32
|
+
type AddMemoryRecallItemRow,
|
|
33
|
+
type AuditLogRow,
|
|
34
|
+
type AddUserRow,
|
|
35
|
+
} from "../shared/db-types.js"
|
|
36
|
+
import { MemoryError, isMemoryError } from "../shared/memory/domain/errors.js"
|
|
37
|
+
import { assertTransition, type MemoryAction, type MemoryStatus } from "../shared/memory/domain/state-machine.js"
|
|
38
|
+
import { assertScopeWritable, scopesCompatible, type ScopeContext, type MemoryScopeType } from "../shared/memory/domain/scope.js"
|
|
39
|
+
import { contentHash } from "../shared/memory/domain/dedup.js"
|
|
40
|
+
import { detectConflicts } from "../shared/memory/domain/conflicts.js"
|
|
41
|
+
import { assertNoSecrets } from "../shared/memory/domain/secrets.js"
|
|
42
|
+
import { recallPipeline } from "../shared/memory/retrieval/pipeline.js"
|
|
43
|
+
import type { LexicalSearchAdapter, RawQuerier, RecallFilter } from "../shared/memory/retrieval/types.js"
|
|
44
|
+
import { createPgFtsAdapter } from "../shared/memory/retrieval/fts/pg.js"
|
|
45
|
+
import { createSqliteFtsAdapter } from "../shared/memory/retrieval/fts/sqlite.js"
|
|
46
|
+
import {
|
|
47
|
+
createNoneEmbeddingProvider,
|
|
48
|
+
createEmbeddingProviderFromConfig,
|
|
49
|
+
parseEmbeddingEnv,
|
|
50
|
+
type EmbeddingProvider,
|
|
51
|
+
type VectorSearchAdapter,
|
|
52
|
+
} from "../shared/memory/embedding/index.js"
|
|
53
|
+
import { createPgVectorAdapter } from "../shared/memory/retrieval/vector/pgvector.js"
|
|
54
|
+
import { createSqliteVecAdapter } from "../shared/memory/retrieval/vector/sqlite-vec.js"
|
|
55
|
+
|
|
56
|
+
/** 测试可注入依赖(生产默认从 prisma/DATABASE_URL 构建) */
|
|
57
|
+
export interface MemoryToolDeps {
|
|
58
|
+
lexical?: LexicalSearchAdapter[]
|
|
59
|
+
rawQuerier?: RawQuerier
|
|
60
|
+
embedding?: EmbeddingProvider
|
|
61
|
+
vector?: VectorSearchAdapter | null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const MEMORY_KINDS = ["DECISION", "CONSTRAINT", "PITFALL", "FAILURE", "LESSON", "PATTERN", "CONVENTION", "FACT", "HANDOFF_DIGEST", "HYPOTHESIS"] as const
|
|
65
|
+
const SCOPE_TYPES = ["ORGANIZATION", "REPOSITORY", "BRANCH", "MODULE", "PATH", "SYMBOL", "PLAN", "SPEC"] as const
|
|
66
|
+
const RECALL_OUTCOMES = ["UNKNOWN", "USED", "USEFUL", "IRRELEVANT", "OUTDATED", "CONTRADICTED", "HARMFUL"] as const
|
|
67
|
+
|
|
68
|
+
export function registerMemoryTools(server: ToolRegistrar, deps: MemoryToolDeps = {}) {
|
|
69
|
+
const runtimeContext = getRuntimeContext()
|
|
70
|
+
|
|
71
|
+
// 无类型边界单点(zod 托管):动态 client → 运行期校验的泛型委托
|
|
72
|
+
const memoryDb = validatedDelegate<AddMemoryRow>(prisma.addMemory, AddMemoryRowSchema, "AddMemory")
|
|
73
|
+
const evidenceDb = validatedDelegate<AddMemoryEvidenceRow>(prisma.addMemoryEvidence, AddMemoryEvidenceRowSchema, "AddMemoryEvidence")
|
|
74
|
+
const linkDb = validatedDelegate<AddMemoryEvidenceLinkRow>(prisma.addMemoryEvidenceLink, AddMemoryEvidenceLinkRowSchema, "AddMemoryEvidenceLink")
|
|
75
|
+
const recallDb = validatedDelegate<AddMemoryRecallRow>(prisma.addMemoryRecall, AddMemoryRecallRowSchema, "AddMemoryRecall")
|
|
76
|
+
const recallItemDb = validatedDelegate<AddMemoryRecallItemRow>(prisma.addMemoryRecallItem, AddMemoryRecallItemRowSchema, "AddMemoryRecallItem")
|
|
77
|
+
const auditDb = validatedDelegate<AuditLogRow>(prisma.auditLog, AuditLogRowSchema, "AuditLog")
|
|
78
|
+
const userDb = validatedDelegate<AddUserRow>(prisma.addUser, AddUserRowSchema, "AddUser")
|
|
79
|
+
|
|
80
|
+
const rawQuerier: RawQuerier = deps.rawQuerier ?? {
|
|
81
|
+
query: <T = Record<string, unknown>>(sql: string, params: unknown[]): Promise<T[]> =>
|
|
82
|
+
(prisma.$queryRawUnsafe as unknown as (s: string, ...p: unknown[]) => Promise<unknown>)(sql, ...params) as Promise<T[]>,
|
|
83
|
+
}
|
|
84
|
+
const lexical: LexicalSearchAdapter[] = deps.lexical ?? [
|
|
85
|
+
DATABASE_URL.startsWith("postgres") ? createPgFtsAdapter(rawQuerier) : createSqliteFtsAdapter(rawQuerier),
|
|
86
|
+
]
|
|
87
|
+
// ── 轮 3:按配置装配 provider / vector adapter(默认 none + 不启用向量 = 首版行为)──
|
|
88
|
+
// 惰性解析:只在真正召回时构建,避免启动期加载 ONNX/网络依赖;任一环节不可用即降级。
|
|
89
|
+
interface RuntimePair { embedding: EmbeddingProvider; vector: VectorSearchAdapter | null }
|
|
90
|
+
let cachedRuntime: Promise<RuntimePair> | null = null
|
|
91
|
+
|
|
92
|
+
function resolveRuntimePair(): Promise<RuntimePair> {
|
|
93
|
+
if (deps.embedding || deps.vector !== undefined) {
|
|
94
|
+
return Promise.resolve({
|
|
95
|
+
embedding: deps.embedding ?? createNoneEmbeddingProvider(),
|
|
96
|
+
vector: deps.vector ?? null,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
if (!cachedRuntime) cachedRuntime = buildRuntimePairFromEnv()
|
|
100
|
+
return cachedRuntime
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function buildRuntimePairFromEnv(): Promise<RuntimePair> {
|
|
104
|
+
const cfg = parseEmbeddingEnv()
|
|
105
|
+
let embedding: EmbeddingProvider
|
|
106
|
+
try {
|
|
107
|
+
embedding = await createEmbeddingProviderFromConfig(cfg)
|
|
108
|
+
} catch {
|
|
109
|
+
// 配置要求向量但 provider 不可用 → 合法降级为 none(degradedMode 明示)
|
|
110
|
+
embedding = createNoneEmbeddingProvider()
|
|
111
|
+
}
|
|
112
|
+
const mode = (process.env.ADD_MEMORY_VECTOR ?? "off").toLowerCase()
|
|
113
|
+
if (mode === "off" || embedding.id === "none") return { embedding, vector: null }
|
|
114
|
+
|
|
115
|
+
const model = cfg.model ?? (embedding.id === "local-onnx" ? "Xenova/bge-small-zh-v1.5" : "text-embedding-3-small")
|
|
116
|
+
const dimension = embedding.dimension || cfg.dimension || 0
|
|
117
|
+
if (dimension <= 0) return { embedding, vector: null }
|
|
118
|
+
|
|
119
|
+
const adapter = DATABASE_URL.startsWith("postgres")
|
|
120
|
+
? createPgVectorAdapter({ querier: rawQuerier, dimension, model })
|
|
121
|
+
: createSqliteVecAdapter({ querier: rawQuerier, dimension, model })
|
|
122
|
+
const health = await adapter.health()
|
|
123
|
+
// 能力不足(扩展缺失/表缺失)→ 不启用向量,召回侧以 degradedMode 明示
|
|
124
|
+
return { embedding, vector: health.status === "ok" ? adapter : null }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 横切守卫 ──
|
|
128
|
+
|
|
129
|
+
/** repository 边界:不一致即拒(越权测试的唯一判定) */
|
|
130
|
+
function assertRepository(repositoryRef: string): void {
|
|
131
|
+
if (repositoryRef !== runtimeContext.projectKey) {
|
|
132
|
+
throw new MemoryError("ERR_REPOSITORY_MISMATCH", `期望 ${runtimeContext.projectKey},实收 ${repositoryRef}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertInvariant01(field: string, v: number | undefined): void {
|
|
137
|
+
if (v !== undefined && (Number.isNaN(v) || v < 0 || v > 1)) {
|
|
138
|
+
throw new MemoryError("ERR_INVARIANT", `${field}=${v} 越出 [0,1]`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 统一错误出口:MemoryError 透传稳定码;其余兜底为 ERR_INVARIANT 之外的通用包装 */
|
|
143
|
+
function fail(e: unknown) {
|
|
144
|
+
if (isMemoryError(e)) return errorResponse(e.message)
|
|
145
|
+
return errorResponse(`ERR_INTERNAL: ${e instanceof Error ? e.message : String(e)}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function ensureSystemUser(): Promise<string> {
|
|
149
|
+
let u = await userDb.findUnique({ where: { username: "ai-assistant" }, select: { id: true } })
|
|
150
|
+
if (!u) u = await userDb.create({ data: { id: "ai-assistant", username: "ai-assistant", email: "ai-assistant@internal" } })
|
|
151
|
+
return u.id
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 状态迁移审计打点(ADD-5:不落库的状态迁移视为未发生) */
|
|
155
|
+
async function writeTransitionAudit(input: {
|
|
156
|
+
memoryId: string; action: string; from: MemoryStatus; to: MemoryStatus; reason?: string; actor?: string
|
|
157
|
+
}): Promise<string> {
|
|
158
|
+
const userId = await ensureSystemUser()
|
|
159
|
+
const log = await auditDb.create({
|
|
160
|
+
data: {
|
|
161
|
+
userId,
|
|
162
|
+
projectKey: runtimeContext.projectKey,
|
|
163
|
+
producerAdapterKey: runtimeContext.adapterKey,
|
|
164
|
+
contextId: runtimeContext.contextId,
|
|
165
|
+
action: `MEMORY_${input.action.toUpperCase()}`,
|
|
166
|
+
targetType: "AddMemory",
|
|
167
|
+
targetId: input.memoryId,
|
|
168
|
+
beforeState: { status: input.from },
|
|
169
|
+
afterState: { status: input.to, actor: input.actor ?? null },
|
|
170
|
+
reason: input.reason ?? null,
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
return log.id
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 取行 + 边界校验(找不到/越库 → 稳定错误码) */
|
|
177
|
+
async function getScopedMemory(memoryId: string): Promise<AddMemoryRow> {
|
|
178
|
+
const row = await memoryDb.findUnique({ where: { id: memoryId } })
|
|
179
|
+
if (!row) throw new MemoryError("ERR_NOT_FOUND", `memoryId=${memoryId}`)
|
|
180
|
+
if (row.repositoryRef !== runtimeContext.projectKey) {
|
|
181
|
+
throw new MemoryError("ERR_REPOSITORY_MISMATCH", `memoryId=${memoryId} 属于 ${row.repositoryRef}`)
|
|
182
|
+
}
|
|
183
|
+
return row
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ===== 1. propose_memory =====
|
|
187
|
+
server.registerTool("propose_memory", {
|
|
188
|
+
description: "提出记忆候选(CANDIDATE,绝不直接 ACTIVE)。执行去重(幂等键 repositoryRef+contentHash+scope)、密钥扫描(命中拒写)、同 scope 冲突检测(返回 conflicts 由人工裁决)。可关联证据引用。",
|
|
189
|
+
inputSchema: z.object({
|
|
190
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
191
|
+
kind: z.enum(MEMORY_KINDS).describe("记忆类型"),
|
|
192
|
+
topic: z.string().describe("主题(短句)"),
|
|
193
|
+
content: z.string().describe("记忆正文(可验证结论,禁止只存数值)"),
|
|
194
|
+
scopeType: z.enum(SCOPE_TYPES).describe("scope 类型"),
|
|
195
|
+
scopeValue: z.string().describe("scope 值(如仓库名/分支/路径/符号)"),
|
|
196
|
+
summary: z.string().optional().describe("可选摘要(注入上下文时优先使用)"),
|
|
197
|
+
importance: z.number().optional().describe("重要性 [0,1],默认 0.5"),
|
|
198
|
+
confidence: z.number().optional().describe("置信度 [0,1],默认 0.5"),
|
|
199
|
+
evidenceRefs: z.array(z.string()).optional().describe("证据引用列表(sourceRef,如 plan 路径/审计 ID/文件路径)"),
|
|
200
|
+
sourceType: z.enum(["PLAN", "SPEC", "DPS_GATE", "DEV_OPERATION", "RAHS_GATE", "HANDOFF", "MANUAL", "IMPORT"]).optional().default("MANUAL"),
|
|
201
|
+
createdBy: z.string().optional().describe("提议者标识"),
|
|
202
|
+
}),
|
|
203
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
204
|
+
try {
|
|
205
|
+
const repositoryRef = args.repositoryRef as string
|
|
206
|
+
assertRepository(repositoryRef)
|
|
207
|
+
const kind = args.kind as string
|
|
208
|
+
const topic = args.topic as string
|
|
209
|
+
const content = args.content as string
|
|
210
|
+
const scopeType = args.scopeType as MemoryScopeType
|
|
211
|
+
const scopeValue = args.scopeValue as string
|
|
212
|
+
assertScopeWritable({ type: scopeType, value: scopeValue })
|
|
213
|
+
assertInvariant01("importance", args.importance as number | undefined)
|
|
214
|
+
assertInvariant01("confidence", args.confidence as number | undefined)
|
|
215
|
+
assertNoSecrets(`${topic}\n${content}`)
|
|
216
|
+
|
|
217
|
+
const hash = contentHash(content)
|
|
218
|
+
// 幂等去重:同幂等键且未被拒绝/归档 → 合并语义,返回已有 id
|
|
219
|
+
const existing = await memoryDb.findFirst({
|
|
220
|
+
where: { repositoryRef, contentHash: hash, scopeType, scopeValue, status: { notIn: ["REJECTED", "ARCHIVED"] } },
|
|
221
|
+
})
|
|
222
|
+
if (existing) {
|
|
223
|
+
return textResponse(JSON.stringify({ memoryId: existing.id, merged: true, status: existing.status, conflicts: [] }))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 冲突检测:同 scope 的 ACTIVE 高相似 CONSTRAINT/DECISION/CONVENTION
|
|
227
|
+
const actives = await memoryDb.findMany({
|
|
228
|
+
where: { repositoryRef, status: "ACTIVE", scopeType, scopeValue },
|
|
229
|
+
take: 200,
|
|
230
|
+
})
|
|
231
|
+
const conflicts = detectConflicts({ kind, topic, content, scopeType, scopeValue }, actives)
|
|
232
|
+
|
|
233
|
+
const created = await memoryDb.create({
|
|
234
|
+
data: {
|
|
235
|
+
kind, topic, content,
|
|
236
|
+
summary: (args.summary as string | undefined) ?? null,
|
|
237
|
+
scopeType, scopeValue, repositoryRef,
|
|
238
|
+
importance: (args.importance as number | undefined) ?? 0.5,
|
|
239
|
+
confidence: (args.confidence as number | undefined) ?? 0.5,
|
|
240
|
+
contentHash: hash,
|
|
241
|
+
createdBy: (args.createdBy as string | undefined) ?? null,
|
|
242
|
+
} as Partial<AddMemoryRow>,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// 证据关联(幂等:repositoryRef+sourceType+sourceRef+contentHash 唯一)
|
|
246
|
+
const evidenceRefs = (args.evidenceRefs as string[] | undefined) ?? []
|
|
247
|
+
const sourceType = (args.sourceType as string | undefined) ?? "MANUAL"
|
|
248
|
+
let evidenceCount = 0
|
|
249
|
+
for (const ref of evidenceRefs) {
|
|
250
|
+
assertNoSecrets(ref)
|
|
251
|
+
const ev = await evidenceDb.upsert({
|
|
252
|
+
where: {
|
|
253
|
+
repositoryRef_sourceType_sourceRef_contentHash: {
|
|
254
|
+
repositoryRef, sourceType, sourceRef: ref, contentHash: contentHash(ref),
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
create: {
|
|
258
|
+
repositoryRef, sourceType, sourceRef: ref,
|
|
259
|
+
excerpt: ref.slice(0, 500),
|
|
260
|
+
contentHash: contentHash(ref),
|
|
261
|
+
} as Partial<AddMemoryEvidenceRow>,
|
|
262
|
+
update: {},
|
|
263
|
+
})
|
|
264
|
+
const linkKey = { memoryId: created.id, evidenceId: ev.id }
|
|
265
|
+
const linked = await linkDb.findFirst({ where: linkKey })
|
|
266
|
+
if (!linked) await linkDb.create({ data: linkKey })
|
|
267
|
+
evidenceCount++
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return textResponse(JSON.stringify({
|
|
271
|
+
memoryId: created.id, merged: false, status: created.status, evidenceCount, conflicts,
|
|
272
|
+
}))
|
|
273
|
+
} catch (e) { return fail(e) }
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// ===== 2. recall_memory =====
|
|
277
|
+
server.registerTool("recall_memory", {
|
|
278
|
+
description: "受约束的混合召回(FTS-first,RRF 融合 + 治理重排 + token 预算裁剪)。返回结构化 items(含 whySelected/scoreBreakdown/supersedes)+ recallId(审计可重放)+ degradedMode(降级明示)。",
|
|
279
|
+
inputSchema: z.object({
|
|
280
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
281
|
+
query: z.string().describe("召回意图(自然语言查询)"),
|
|
282
|
+
stage: z.string().describe("召回阶段(如 session-start/plan-start/dps/rah/handoff/prompt)"),
|
|
283
|
+
branch: z.string().optional(),
|
|
284
|
+
module: z.string().optional(),
|
|
285
|
+
paths: z.array(z.string()).optional().describe("当前上下文路径集合"),
|
|
286
|
+
symbols: z.array(z.string()).optional().describe("当前上下文符号集合"),
|
|
287
|
+
planKeyword: z.string().optional(),
|
|
288
|
+
specRef: z.string().optional(),
|
|
289
|
+
includePlanScope: z.boolean().optional().describe("显式历史查询时放行 PLAN/SPEC scope"),
|
|
290
|
+
kinds: z.array(z.enum(MEMORY_KINDS)).optional().describe("限定记忆类型"),
|
|
291
|
+
maxTokens: z.number().optional().default(1200).describe("注入 token 预算"),
|
|
292
|
+
limit: z.number().optional().default(20).describe("候选上限"),
|
|
293
|
+
consumerRef: z.string().optional().describe("消费方标识(审计用)"),
|
|
294
|
+
diagnostic: z.boolean().optional().describe("诊断模式:放行 STALE(带警告)"),
|
|
295
|
+
}),
|
|
296
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
297
|
+
try {
|
|
298
|
+
const repositoryRef = args.repositoryRef as string
|
|
299
|
+
assertRepository(repositoryRef)
|
|
300
|
+
|
|
301
|
+
// 轮 3:按配置解析 provider / vector adapter(惰性;默认 none + 不启用向量 = 首版行为)
|
|
302
|
+
const { embedding, vector } = await resolveRuntimePair()
|
|
303
|
+
|
|
304
|
+
const scopeCtx: ScopeContext = {
|
|
305
|
+
repository: repositoryRef,
|
|
306
|
+
branch: args.branch as string | undefined,
|
|
307
|
+
module: args.module as string | undefined,
|
|
308
|
+
paths: args.paths as string[] | undefined,
|
|
309
|
+
symbols: args.symbols as string[] | undefined,
|
|
310
|
+
planKeyword: args.planKeyword as string | undefined,
|
|
311
|
+
specRef: args.specRef as string | undefined,
|
|
312
|
+
includePlanScope: (args.includePlanScope as boolean | undefined) ?? false,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// degradedMode 明示:向量能力缺失 → FTS-only(合法降级,非故障)
|
|
316
|
+
const vectorHealth = vector ? await vector.health() : null
|
|
317
|
+
const degradedMode = vectorHealth && vectorHealth.status === "ok"
|
|
318
|
+
? null
|
|
319
|
+
: `fts-only(embedding=${embedding.id}${vector ? `,vector=${vectorHealth?.status ?? "unknown"}` : ""})`
|
|
320
|
+
|
|
321
|
+
// 组合式 vector 候选:query → embedding → 向量索引(仅当 provider+adapter 均健康)
|
|
322
|
+
const vectorForPipeline =
|
|
323
|
+
vector && vectorHealth && vectorHealth.status === "ok"
|
|
324
|
+
? {
|
|
325
|
+
search: async (query: string, filter: RecallFilter, limit: number) => {
|
|
326
|
+
const [vec] = await embedding.embed([query])
|
|
327
|
+
return vector.search(vec, filter, limit)
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
: null
|
|
331
|
+
|
|
332
|
+
const result = await recallPipeline(
|
|
333
|
+
{
|
|
334
|
+
query: args.query as string,
|
|
335
|
+
stage: args.stage as string,
|
|
336
|
+
repositoryRef,
|
|
337
|
+
scopeCtx,
|
|
338
|
+
maxTokens: (args.maxTokens as number | undefined) ?? 1200,
|
|
339
|
+
kinds: args.kinds as string[] | undefined,
|
|
340
|
+
limit: (args.limit as number | undefined) ?? 20,
|
|
341
|
+
consumerRef: (args.consumerRef as string | undefined) ?? "mcp:recall_memory",
|
|
342
|
+
diagnostic: (args.diagnostic as boolean | undefined) ?? false,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
lexical,
|
|
346
|
+
vector: vectorForPipeline,
|
|
347
|
+
fetchByIds: async (ids) =>
|
|
348
|
+
await memoryDb.findMany({
|
|
349
|
+
where: { id: { in: ids } },
|
|
350
|
+
include: { supersedes: { select: { id: true } } },
|
|
351
|
+
}),
|
|
352
|
+
fetchEvidenceSourceRefs: async (memoryIds) => {
|
|
353
|
+
const links = await linkDb.findMany({ where: { memoryId: { in: memoryIds } } })
|
|
354
|
+
const evIds = [...new Set(links.map((l) => l.evidenceId))]
|
|
355
|
+
const evs = evIds.length > 0 ? await evidenceDb.findMany({ where: { id: { in: evIds } } }) : []
|
|
356
|
+
const refById = new Map(evs.map((e) => [e.id, e.sourceRef]))
|
|
357
|
+
const out = new Map<string, string[]>()
|
|
358
|
+
for (const l of links) {
|
|
359
|
+
const ref = refById.get(l.evidenceId)
|
|
360
|
+
if (!ref) continue
|
|
361
|
+
const arr = out.get(l.memoryId) ?? []
|
|
362
|
+
arr.push(ref)
|
|
363
|
+
out.set(l.memoryId, arr)
|
|
364
|
+
}
|
|
365
|
+
return out
|
|
366
|
+
},
|
|
367
|
+
audit: {
|
|
368
|
+
createRecall: (data) => recallDb.create({ data: data }),
|
|
369
|
+
createRecallItem: (data) => recallItemDb.create({ data: data }),
|
|
370
|
+
},
|
|
371
|
+
degradedMode: degradedMode ?? undefined,
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return textResponse(JSON.stringify(result))
|
|
376
|
+
} catch (e) { return fail(e) }
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// ===== 3. get_memory =====
|
|
380
|
+
server.registerTool("get_memory", {
|
|
381
|
+
description: "查看单条记忆详情与 provenance:evidence 列表、supersession 链(前驱/后继)、最近召回使用情况。",
|
|
382
|
+
inputSchema: z.object({
|
|
383
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
384
|
+
memoryId: z.string(),
|
|
385
|
+
}),
|
|
386
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
387
|
+
try {
|
|
388
|
+
assertRepository(args.repositoryRef as string)
|
|
389
|
+
const row = await getScopedMemory(args.memoryId as string)
|
|
390
|
+
|
|
391
|
+
const links = await linkDb.findMany({ where: { memoryId: row.id } })
|
|
392
|
+
const evIds = links.map((l) => l.evidenceId)
|
|
393
|
+
const evidence = evIds.length > 0 ? await evidenceDb.findMany({ where: { id: { in: evIds } } }) : []
|
|
394
|
+
|
|
395
|
+
// supersession 链:向上(supersededById)+ 向下(谁 supersede 了我)
|
|
396
|
+
// 注意:validatedDelegate 会按行 schema 校验返回值,不能用 select 裁剪字段
|
|
397
|
+
const supersededBy = row.supersededById
|
|
398
|
+
? await memoryDb.findUnique({ where: { id: row.supersededById } })
|
|
399
|
+
: null
|
|
400
|
+
const supersedes = await memoryDb.findMany({ where: { supersededById: row.id } })
|
|
401
|
+
|
|
402
|
+
// 最近召回使用(RecallItem join Recall 元信息)
|
|
403
|
+
const items = await recallItemDb.findMany({
|
|
404
|
+
where: { memoryId: row.id }, orderBy: { updatedAt: "desc" }, take: 10,
|
|
405
|
+
})
|
|
406
|
+
const recallIds = [...new Set(items.map((i) => i.recallId))]
|
|
407
|
+
const recalls = recallIds.length > 0 ? await recallDb.findMany({ where: { id: { in: recallIds } } }) : []
|
|
408
|
+
const recallById = new Map(recalls.map((r) => [r.id, r]))
|
|
409
|
+
const recallUsage = items.map((i) => ({
|
|
410
|
+
recallId: i.recallId, selected: i.selected, rank: i.rank, outcome: i.outcome,
|
|
411
|
+
query: recallById.get(i.recallId)?.query ?? null,
|
|
412
|
+
stage: recallById.get(i.recallId)?.stage ?? null,
|
|
413
|
+
at: recallById.get(i.recallId)?.createdAt ?? null,
|
|
414
|
+
}))
|
|
415
|
+
|
|
416
|
+
return textResponse(JSON.stringify({
|
|
417
|
+
memory: row,
|
|
418
|
+
evidence: evidence.map((e) => ({ id: e.id, sourceType: e.sourceType, sourceRef: e.sourceRef, excerpt: e.excerpt, occurredAt: e.occurredAt })),
|
|
419
|
+
supersession: {
|
|
420
|
+
supersededBy: supersededBy?.id ?? null,
|
|
421
|
+
supersedes: supersedes.map((s) => s.id),
|
|
422
|
+
},
|
|
423
|
+
recallUsage,
|
|
424
|
+
}))
|
|
425
|
+
} catch (e) { return fail(e) }
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// ===== 4. list_memories =====
|
|
429
|
+
server.registerTool("list_memories", {
|
|
430
|
+
description: "按状态/scope/kind 列表查询(cursor 分页,createdAt 倒序)。",
|
|
431
|
+
inputSchema: z.object({
|
|
432
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
433
|
+
status: z.enum(["CANDIDATE", "PENDING", "ACTIVE", "STALE", "SUPERSEDED", "REJECTED", "ARCHIVED"]).optional(),
|
|
434
|
+
kind: z.enum(MEMORY_KINDS).optional(),
|
|
435
|
+
scopeType: z.enum(SCOPE_TYPES).optional(),
|
|
436
|
+
scopeValue: z.string().optional(),
|
|
437
|
+
cursor: z.string().optional().describe("上一页最后一行的 id"),
|
|
438
|
+
limit: z.number().optional().default(20).describe("每页条数,默认 20,最大 100"),
|
|
439
|
+
}),
|
|
440
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
441
|
+
try {
|
|
442
|
+
const repositoryRef = args.repositoryRef as string
|
|
443
|
+
assertRepository(repositoryRef)
|
|
444
|
+
const limit = Math.min((args.limit as number | undefined) ?? 20, 100)
|
|
445
|
+
const cursor = args.cursor as string | undefined
|
|
446
|
+
|
|
447
|
+
const where: Record<string, unknown> = { repositoryRef }
|
|
448
|
+
if (args.status) where.status = args.status
|
|
449
|
+
if (args.kind) where.kind = args.kind
|
|
450
|
+
if (args.scopeType) where.scopeType = args.scopeType
|
|
451
|
+
if (args.scopeValue) where.scopeValue = args.scopeValue
|
|
452
|
+
|
|
453
|
+
const rows = await memoryDb.findMany({
|
|
454
|
+
where,
|
|
455
|
+
orderBy: { createdAt: "desc" },
|
|
456
|
+
take: limit,
|
|
457
|
+
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
|
|
458
|
+
})
|
|
459
|
+
const nextCursor = rows.length === limit ? rows[rows.length - 1].id : null
|
|
460
|
+
return textResponse(JSON.stringify({
|
|
461
|
+
items: rows.map((r) => ({
|
|
462
|
+
id: r.id, kind: r.kind, status: r.status, topic: r.topic,
|
|
463
|
+
scope: { type: r.scopeType, value: r.scopeValue },
|
|
464
|
+
importance: r.importance, confidence: r.confidence,
|
|
465
|
+
validUntil: r.validUntil, createdAt: r.createdAt,
|
|
466
|
+
})),
|
|
467
|
+
nextCursor,
|
|
468
|
+
}))
|
|
469
|
+
} catch (e) { return fail(e) }
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// ===== 5. review_memory =====
|
|
473
|
+
server.registerTool("review_memory", {
|
|
474
|
+
description: "获取待审核治理队列:CANDIDATE/PENDING 候选 + 各自证据 + 重复项(同 contentHash)+ 疑似冲突(同 scope 高相似 ACTIVE)。",
|
|
475
|
+
inputSchema: z.object({
|
|
476
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
477
|
+
kind: z.enum(MEMORY_KINDS).optional(),
|
|
478
|
+
limit: z.number().optional().default(50),
|
|
479
|
+
}),
|
|
480
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
481
|
+
try {
|
|
482
|
+
const repositoryRef = args.repositoryRef as string
|
|
483
|
+
assertRepository(repositoryRef)
|
|
484
|
+
const limit = Math.min((args.limit as number | undefined) ?? 50, 200)
|
|
485
|
+
|
|
486
|
+
const where: Record<string, unknown> = { repositoryRef, status: { in: ["CANDIDATE", "PENDING"] } }
|
|
487
|
+
if (args.kind) where.kind = args.kind
|
|
488
|
+
const candidates = await memoryDb.findMany({ where, orderBy: { createdAt: "asc" }, take: limit })
|
|
489
|
+
|
|
490
|
+
const actives = await memoryDb.findMany({ where: { repositoryRef, status: "ACTIVE" }, take: 500 })
|
|
491
|
+
const result = []
|
|
492
|
+
for (const c of candidates) {
|
|
493
|
+
const links = await linkDb.findMany({ where: { memoryId: c.id } })
|
|
494
|
+
const evIds = links.map((l) => l.evidenceId)
|
|
495
|
+
const evidence = evIds.length > 0 ? await evidenceDb.findMany({ where: { id: { in: evIds } } }) : []
|
|
496
|
+
const duplicates = await memoryDb.findMany({
|
|
497
|
+
where: { repositoryRef, contentHash: c.contentHash, id: { not: c.id }, status: { notIn: ["REJECTED", "ARCHIVED"] } },
|
|
498
|
+
take: 10,
|
|
499
|
+
})
|
|
500
|
+
const conflicts = detectConflicts(c, actives)
|
|
501
|
+
result.push({
|
|
502
|
+
id: c.id, kind: c.kind, status: c.status, topic: c.topic,
|
|
503
|
+
scope: { type: c.scopeType, value: c.scopeValue },
|
|
504
|
+
evidence: evidence.map((e) => ({ sourceType: e.sourceType, sourceRef: e.sourceRef })),
|
|
505
|
+
duplicates: duplicates.map((d) => ({ id: d.id, status: d.status, topic: d.topic })),
|
|
506
|
+
conflicts,
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
return textResponse(JSON.stringify({ backlog: result.length, candidates: result }))
|
|
510
|
+
} catch (e) { return fail(e) }
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// ===== 6. resolve_memory =====
|
|
514
|
+
server.registerTool("resolve_memory", {
|
|
515
|
+
description: "执行治理状态迁移(状态机强制校验 + AuditLog 打点)。action: submit_review|approve|reject|stale|supersede|archive|restore。approve 必须已有 ≥1 证据且提供 actor;supersede 必须提供 supersededById(新记忆 id)且 scope 兼容。",
|
|
516
|
+
inputSchema: z.object({
|
|
517
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
518
|
+
memoryId: z.string(),
|
|
519
|
+
action: z.enum(["submit_review", "approve", "reject", "stale", "supersede", "archive", "restore"]),
|
|
520
|
+
reason: z.string().optional(),
|
|
521
|
+
actor: z.string().optional().describe("操作者(approve 时作为 approvedBy,必填)"),
|
|
522
|
+
supersededById: z.string().optional().describe("supersede 专用:新记忆 id"),
|
|
523
|
+
}),
|
|
524
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
525
|
+
try {
|
|
526
|
+
assertRepository(args.repositoryRef as string)
|
|
527
|
+
const memoryId = args.memoryId as string
|
|
528
|
+
const action = args.action as string
|
|
529
|
+
const actor = args.actor as string | undefined
|
|
530
|
+
const row = await getScopedMemory(memoryId)
|
|
531
|
+
const current = row.status
|
|
532
|
+
const domainAction: MemoryAction = action === "stale" ? "mark_stale" : (action as MemoryAction)
|
|
533
|
+
|
|
534
|
+
// 迁移上下文:按动作装配不变量证据
|
|
535
|
+
const tctx: Parameters<typeof assertTransition>[2] = {}
|
|
536
|
+
if (domainAction === "approve") {
|
|
537
|
+
const links = await linkDb.findMany({ where: { memoryId } })
|
|
538
|
+
tctx.evidenceCount = links.length
|
|
539
|
+
tctx.approvedBy = actor ?? null
|
|
540
|
+
tctx.approvedAt = actor ? new Date() : null
|
|
541
|
+
}
|
|
542
|
+
let superseding: AddMemoryRow | null = null
|
|
543
|
+
if (domainAction === "supersede") {
|
|
544
|
+
const newId = args.supersededById as string | undefined
|
|
545
|
+
if (newId) {
|
|
546
|
+
superseding = await memoryDb.findUnique({ where: { id: newId } })
|
|
547
|
+
tctx.supersededById = newId
|
|
548
|
+
tctx.supersessionCompatible = !!superseding &&
|
|
549
|
+
superseding.repositoryRef === row.repositoryRef &&
|
|
550
|
+
scopesCompatible(
|
|
551
|
+
{ type: row.scopeType, value: row.scopeValue },
|
|
552
|
+
{ type: superseding.scopeType, value: superseding.scopeValue },
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const t = assertTransition(current, domainAction, tctx)
|
|
558
|
+
|
|
559
|
+
const data: Record<string, unknown> = { status: t.to }
|
|
560
|
+
if (domainAction === "approve") { data.approvedBy = actor; data.approvedAt = new Date() }
|
|
561
|
+
if (domainAction === "supersede") data.supersededById = args.supersededById
|
|
562
|
+
await memoryDb.update({ where: { id: memoryId }, data: data })
|
|
563
|
+
|
|
564
|
+
const auditRef = await writeTransitionAudit({
|
|
565
|
+
memoryId, action: `TRANSITION_${action.toUpperCase()}`,
|
|
566
|
+
from: t.from, to: t.to,
|
|
567
|
+
reason: args.reason as string | undefined, actor,
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
return textResponse(JSON.stringify({ memoryId, oldStatus: t.from, newStatus: t.to, auditRef }))
|
|
571
|
+
} catch (e) { return fail(e) }
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
// ===== 7. feedback_memory =====
|
|
575
|
+
server.registerTool("feedback_memory", {
|
|
576
|
+
description: "记录召回结果反馈(幂等 upsert RecallItem.outcome)。outcome: UNKNOWN|USED|USEFUL|IRRELEVANT|OUTDATED|CONTRADICTED|HARMFUL。",
|
|
577
|
+
inputSchema: z.object({
|
|
578
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
579
|
+
recallId: z.string(),
|
|
580
|
+
memoryId: z.string(),
|
|
581
|
+
outcome: z.enum(RECALL_OUTCOMES),
|
|
582
|
+
feedback: z.string().optional(),
|
|
583
|
+
}),
|
|
584
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
585
|
+
try {
|
|
586
|
+
assertRepository(args.repositoryRef as string)
|
|
587
|
+
const recallId = args.recallId as string
|
|
588
|
+
const memoryId = args.memoryId as string
|
|
589
|
+
|
|
590
|
+
const recall = await recallDb.findUnique({ where: { id: recallId } })
|
|
591
|
+
if (!recall) throw new MemoryError("ERR_NOT_FOUND", `recallId=${recallId}`)
|
|
592
|
+
if (recall.repositoryRef !== runtimeContext.projectKey) {
|
|
593
|
+
throw new MemoryError("ERR_REPOSITORY_MISMATCH", `recallId=${recallId} 属于 ${recall.repositoryRef}`)
|
|
594
|
+
}
|
|
595
|
+
const mem = await memoryDb.findUnique({ where: { id: memoryId } })
|
|
596
|
+
if (!mem) throw new MemoryError("ERR_NOT_FOUND", `memoryId=${memoryId}`)
|
|
597
|
+
|
|
598
|
+
const outcome = args.outcome as string
|
|
599
|
+
const feedback = (args.feedback as string | undefined) ?? null
|
|
600
|
+
await recallItemDb.upsert({
|
|
601
|
+
where: { recallId_memoryId: { recallId, memoryId } },
|
|
602
|
+
create: { recallId, memoryId, selected: false, rank: null, outcome, feedback } as Partial<AddMemoryRecallItemRow>,
|
|
603
|
+
update: { outcome, feedback } as Partial<AddMemoryRecallItemRow>,
|
|
604
|
+
})
|
|
605
|
+
return textResponse(JSON.stringify({ updated: true, recallId, memoryId, outcome }))
|
|
606
|
+
} catch (e) { return fail(e) }
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
// ===== 8. get_memory_health =====
|
|
610
|
+
server.registerTool("get_memory_health", {
|
|
611
|
+
description: "运行与治理健康度:backlog(CANDIDATE/PENDING 计数)、leakage 抽查(近 20 次召回选中项的越库计数)、embedding provider 与 FTS 索引健康。",
|
|
612
|
+
inputSchema: z.object({
|
|
613
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
614
|
+
}),
|
|
615
|
+
}, async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
616
|
+
try {
|
|
617
|
+
const repositoryRef = args.repositoryRef as string
|
|
618
|
+
assertRepository(repositoryRef)
|
|
619
|
+
const { embedding } = await resolveRuntimePair()
|
|
620
|
+
|
|
621
|
+
// backlog:raw count(validatedDelegate 不支持 count/select 裁剪)
|
|
622
|
+
const backlogRows = await rawQuerier.query<{ status: string; count: number | string }>(
|
|
623
|
+
`SELECT "status"::text AS status, COUNT(*)::int AS count FROM "AddMemory" WHERE "repositoryRef" = $1 GROUP BY "status"`,
|
|
624
|
+
[repositoryRef],
|
|
625
|
+
)
|
|
626
|
+
const backlog: Record<string, number> = {}
|
|
627
|
+
for (const r of backlogRows) backlog[r.status] = Number(r.count)
|
|
628
|
+
|
|
629
|
+
// leakage 抽查:最近 20 次召回的 selectedIds 是否全部属于本仓库
|
|
630
|
+
const recentRecalls = await recallDb.findMany({
|
|
631
|
+
where: { repositoryRef }, orderBy: { createdAt: "desc" }, take: 20,
|
|
632
|
+
})
|
|
633
|
+
const selectedIds = [...new Set(recentRecalls.flatMap((r) => (r.selectedIds as string[] | null) ?? []))]
|
|
634
|
+
let leakage = 0
|
|
635
|
+
if (selectedIds.length > 0) {
|
|
636
|
+
const memRows = await memoryDb.findMany({ where: { id: { in: selectedIds } } })
|
|
637
|
+
leakage = memRows.filter((m) => m.repositoryRef !== repositoryRef).length
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const [embeddingHealth, ftsHealth] = await Promise.all([
|
|
641
|
+
embedding.health(),
|
|
642
|
+
...lexical.map((l) => l.health()),
|
|
643
|
+
])
|
|
644
|
+
|
|
645
|
+
return textResponse(JSON.stringify({
|
|
646
|
+
repositoryRef,
|
|
647
|
+
backlog: { candidate: backlog.CANDIDATE ?? 0, pending: backlog.PENDING ?? 0, byStatus: backlog },
|
|
648
|
+
leakage: { checkedRecalls: recentRecalls.length, crossRepositorySelections: leakage },
|
|
649
|
+
providers: { embedding: embeddingHealth },
|
|
650
|
+
index: ftsHealth ?? { component: "fts", status: "unavailable", detail: "无 lexical adapter" },
|
|
651
|
+
}))
|
|
652
|
+
} catch (e) { return fail(e) }
|
|
653
|
+
})
|
|
654
|
+
}
|
|
@@ -8,6 +8,8 @@ import { prisma } from "../shared/prisma.js"
|
|
|
8
8
|
import type { PlanRow } from "../shared/db-types.js"
|
|
9
9
|
import { PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
|
|
10
10
|
import { getRuntimeContext } from "../shared/env.js"
|
|
11
|
+
// 口径单一真源:checklist 的 [T]/[R] 统计直接复用校验层,避免 tracker 与校验器两套口径漂移
|
|
12
|
+
import { checklistStats } from "../../../validation/validators/checklist.js"
|
|
11
13
|
import { assertPathInRuntimeScope } from "../shared/runtime-context.js"
|
|
12
14
|
import { resolvePlanStatus } from "../shared/plan-lifecycle.js"
|
|
13
15
|
import { createPrismaPlanStatusStore } from "../shared/plan-status-store.js"
|
|
@@ -104,9 +106,12 @@ export function registerPlanTools(server: ToolRegistrar) {
|
|
|
104
106
|
let checklistT = 0, checklistTDone = 0, checklistR = 0
|
|
105
107
|
const checklistContent = await readFileSafe(checklistPath)
|
|
106
108
|
if (checklistContent) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
// 单一真源:复用校验层 checklistStats(旧实现按全文出现次数统计,把正文里提到的
|
|
110
|
+
// `[T]` 也计入分母 —— 实测 16/16 被报成 17/18,2026-09-14 修正)
|
|
111
|
+
const stats = checklistStats(checklistContent)
|
|
112
|
+
checklistT = stats.tTotal
|
|
113
|
+
checklistTDone = stats.tDone
|
|
114
|
+
checklistR = stats.rTotal
|
|
110
115
|
}
|
|
111
116
|
// 定位 add-route
|
|
112
117
|
const planPrefix = basename(t.name).replace(/-plan-v\d+$/, "")
|