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,153 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Evidence 自动采集(Spec §10/§11,Plan §9.2 PostToolUse 行)
|
|
3
|
+
*
|
|
4
|
+
* 两段式:
|
|
5
|
+
* ① Hook 侧(同步 ≤200ms,无 DB):白名单判定 + 事件入队(evidence-queue.jsonl append-only)
|
|
6
|
+
* —— classifyEvidenceSource / buildEvidenceEvent 为纯函数,供 post-tool-router 直接引用
|
|
7
|
+
* ② Job 侧(异步,可重试):drainEvidenceQueue 消费队列 → upsert Evidence(幂等键
|
|
8
|
+
* repositoryRef+sourceType+sourceRef+contentHash,与 add.prisma @@unique 一一对应)
|
|
9
|
+
* 消费进度落 evidence-queue.offset(字节偏移),重放幂等:同偏移不重复处理,
|
|
10
|
+
* 同事件重复消费由 upsert 幂等吸收。
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto"
|
|
13
|
+
import { existsSync, mkdirSync, openSync, readFileSync, readSync, closeSync, statSync, writeFileSync, renameSync } from "node:fs"
|
|
14
|
+
import { dirname, join } from "node:path"
|
|
15
|
+
import type { AddMemoryEvidenceRow, TableDelegate } from "../../db-types.js"
|
|
16
|
+
import { EVIDENCE_OFFSET_FILE, EVIDENCE_QUEUE_FILE, MEMORY_DIR_NAME } from "../switches.js"
|
|
17
|
+
|
|
18
|
+
// ── ① Hook 侧纯函数(无 IO 之外的依赖,无 DB) ──
|
|
19
|
+
|
|
20
|
+
export type EvidenceSourceType = "PLAN" | "SPEC" | "HANDOFF" | "DEV_OPERATION"
|
|
21
|
+
|
|
22
|
+
export interface EvidenceEvent {
|
|
23
|
+
dedupKey: string
|
|
24
|
+
sourceType: EvidenceSourceType
|
|
25
|
+
sourceRef: string
|
|
26
|
+
excerpt: string
|
|
27
|
+
occurredAt: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 白名单分类:命中返回 sourceType,未命中返回 null(spec §10:按白名单采集) */
|
|
31
|
+
export function classifyEvidenceSource(filePath: string): EvidenceSourceType | null {
|
|
32
|
+
const p = filePath.replace(/\\/g, "/")
|
|
33
|
+
if (/(^|\/)plans\/[^/]*-plan-v\d+\.md$/.test(p)) return "PLAN"
|
|
34
|
+
if (/(^|\/)plans\/[^/]*-add-route-v\d+\.md$/.test(p)) return "PLAN"
|
|
35
|
+
if (/(^|\/)specs\/.+\.md$/.test(p)) return "SPEC"
|
|
36
|
+
if (/handoff[^/]*\.md$/i.test(p)) return "HANDOFF"
|
|
37
|
+
if (/(^|\/)reviews\/.+\.md$/.test(p)) return "DEV_OPERATION"
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 构造采证事件(幂等 key = sha256(sourceType|sourceRef|excerpt)):同文件同内容重复写入被吸收 */
|
|
42
|
+
export function buildEvidenceEvent(filePath: string, excerpt: string, occurredAt = new Date()): EvidenceEvent {
|
|
43
|
+
const sourceType = classifyEvidenceSource(filePath)
|
|
44
|
+
if (!sourceType) throw new Error(`非白名单路径: ${filePath}`)
|
|
45
|
+
const sourceRef = filePath
|
|
46
|
+
const dedupKey = createHash("sha256")
|
|
47
|
+
.update(`${sourceType}|${sourceRef}|${excerpt}`, "utf8")
|
|
48
|
+
.digest("hex")
|
|
49
|
+
.slice(0, 32)
|
|
50
|
+
return { dedupKey, sourceType, sourceRef, excerpt: excerpt.slice(0, 500), occurredAt: occurredAt.toISOString() }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── ② Job 侧消费(异步、可重试) ──
|
|
54
|
+
|
|
55
|
+
export interface DrainDeps {
|
|
56
|
+
projectDir: string
|
|
57
|
+
magicDir: string
|
|
58
|
+
repositoryRef: string
|
|
59
|
+
evidenceDb: Pick<TableDelegate<AddMemoryEvidenceRow>, "upsert">
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DrainResult {
|
|
63
|
+
processed: number
|
|
64
|
+
skipped: number
|
|
65
|
+
errors: string[]
|
|
66
|
+
newOffset: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 读 offset 标记(缺失/损坏 → 0,从头重放;upsert 幂等保证安全) */
|
|
70
|
+
export function readOffset(projectDir: string, magicDir: string): number {
|
|
71
|
+
try {
|
|
72
|
+
const f = join(projectDir, magicDir, MEMORY_DIR_NAME, EVIDENCE_OFFSET_FILE)
|
|
73
|
+
if (!existsSync(f)) return 0
|
|
74
|
+
const n = Number(readFileSync(f, "utf-8").trim())
|
|
75
|
+
return Number.isFinite(n) && n >= 0 ? n : 0
|
|
76
|
+
} catch {
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 消费 evidence 队列:从上次偏移继续,逐行 upsert Evidence。
|
|
83
|
+
* fail-open 粒度到行:坏行跳过并计入 errors,不阻塞后续。
|
|
84
|
+
*/
|
|
85
|
+
export async function drainEvidenceQueue(deps: DrainDeps): Promise<DrainResult> {
|
|
86
|
+
const queueFile = join(deps.projectDir, deps.magicDir, MEMORY_DIR_NAME, EVIDENCE_QUEUE_FILE)
|
|
87
|
+
const result: DrainResult = { processed: 0, skipped: 0, errors: [], newOffset: readOffset(deps.projectDir, deps.magicDir) }
|
|
88
|
+
if (!existsSync(queueFile)) return result
|
|
89
|
+
|
|
90
|
+
const size = (() => { try { return statSync(queueFile).size } catch { return 0 } })()
|
|
91
|
+
if (size <= result.newOffset) return result // 无增量
|
|
92
|
+
|
|
93
|
+
// 按偏移读取增量字节(避免大文件全量加载)
|
|
94
|
+
const fd = openSync(queueFile, "r")
|
|
95
|
+
let chunk: string
|
|
96
|
+
try {
|
|
97
|
+
const buf = Buffer.alloc(size - result.newOffset)
|
|
98
|
+
readSync(fd, buf, 0, buf.length, result.newOffset)
|
|
99
|
+
chunk = buf.toString("utf-8")
|
|
100
|
+
} finally {
|
|
101
|
+
closeSync(fd)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let cursor = result.newOffset
|
|
105
|
+
for (const line of chunk.split("\n")) {
|
|
106
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + 1 // + '\n'
|
|
107
|
+
cursor += lineBytes
|
|
108
|
+
const trimmed = line.trim()
|
|
109
|
+
if (!trimmed) continue
|
|
110
|
+
let ev: EvidenceEvent
|
|
111
|
+
try {
|
|
112
|
+
ev = JSON.parse(trimmed) as EvidenceEvent
|
|
113
|
+
} catch {
|
|
114
|
+
result.skipped++
|
|
115
|
+
result.errors.push(`坏行@${cursor - lineBytes}: JSON 解析失败`)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await deps.evidenceDb.upsert({
|
|
120
|
+
where: {
|
|
121
|
+
repositoryRef_sourceType_sourceRef_contentHash: {
|
|
122
|
+
repositoryRef: deps.repositoryRef,
|
|
123
|
+
sourceType: ev.sourceType,
|
|
124
|
+
sourceRef: ev.sourceRef,
|
|
125
|
+
contentHash: ev.dedupKey,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
create: {
|
|
129
|
+
repositoryRef: deps.repositoryRef,
|
|
130
|
+
sourceType: ev.sourceType,
|
|
131
|
+
sourceRef: ev.sourceRef,
|
|
132
|
+
excerpt: ev.excerpt,
|
|
133
|
+
contentHash: ev.dedupKey,
|
|
134
|
+
occurredAt: new Date(ev.occurredAt),
|
|
135
|
+
} as Partial<AddMemoryEvidenceRow>,
|
|
136
|
+
update: {},
|
|
137
|
+
})
|
|
138
|
+
result.processed++
|
|
139
|
+
} catch (e) {
|
|
140
|
+
result.skipped++
|
|
141
|
+
result.errors.push(`落库失败 ${ev.sourceRef}: ${e instanceof Error ? e.message : String(e)}`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 原子推进 offset(先写后改:offset 落后只会重放,upsert 幂等吸收)
|
|
146
|
+
result.newOffset = cursor
|
|
147
|
+
const offsetFile = join(deps.projectDir, deps.magicDir, MEMORY_DIR_NAME, EVIDENCE_OFFSET_FILE)
|
|
148
|
+
mkdirSync(dirname(offsetFile), { recursive: true })
|
|
149
|
+
const tmp = `${offsetFile}.tmp-${process.pid}`
|
|
150
|
+
writeFileSync(tmp, String(cursor), "utf-8")
|
|
151
|
+
renameSync(tmp, offsetFile)
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* L1/L2 召回快照生成(Spec §10:session-start 仅注入 repository 级 L1 小上下文)
|
|
3
|
+
*
|
|
4
|
+
* 设计约束(Plan §9.3):同步 Hook ≤200ms 且无 DB 依赖 → Hook 只读本模块预计算的快照文件;
|
|
5
|
+
* 快照由本 job 异步刷新(consolidation / 手动 CLI / Gate 后触发)。
|
|
6
|
+
* 原子写:tmp + rename,Hook 读侧永不遇到半文件。
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs"
|
|
9
|
+
import { dirname, join } from "node:path"
|
|
10
|
+
import { recallPipeline, type MemoryRowLike } from "../retrieval/pipeline.js"
|
|
11
|
+
import type { RecallAuditStore } from "../retrieval/recall-writer.js"
|
|
12
|
+
import type { LexicalSearchAdapter, RecalledMemory } from "../retrieval/types.js"
|
|
13
|
+
import { L1_SNAPSHOT_FILE, L2_SNAPSHOT_FILE, MEMORY_DIR_NAME, memoryMaxTokens } from "../switches.js"
|
|
14
|
+
|
|
15
|
+
export interface SnapshotDeps {
|
|
16
|
+
repositoryRef: string
|
|
17
|
+
projectDir: string
|
|
18
|
+
magicDir: string
|
|
19
|
+
lexical: LexicalSearchAdapter[]
|
|
20
|
+
fetchByIds(ids: string[]): Promise<MemoryRowLike[]>
|
|
21
|
+
fetchEvidenceSourceRefs(memoryIds: string[]): Promise<Map<string, string[]>>
|
|
22
|
+
audit: RecallAuditStore
|
|
23
|
+
now?: Date
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SnapshotResult {
|
|
27
|
+
path: string
|
|
28
|
+
itemCount: number
|
|
29
|
+
injectedTokens: number
|
|
30
|
+
recallId: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 渲染带来源边界标签的注入文本(Plan §9.3:Recall 注入采用明确的数据边界与来源标签) */
|
|
34
|
+
export function renderSnapshotMarkdown(level: "L1" | "L2", items: RecalledMemory[], generatedAt: Date): string {
|
|
35
|
+
const lines = [
|
|
36
|
+
`[Memory ${level} · 来源: AddMemory 治理库 · 生成于 ${generatedAt.toISOString()}]`,
|
|
37
|
+
`<agent-memory source="add-memory" trust="governed">`,
|
|
38
|
+
]
|
|
39
|
+
for (const it of items) {
|
|
40
|
+
lines.push(
|
|
41
|
+
`- [${it.kind}] ${it.topic}(置信 ${it.confidence.toFixed(2)})`,
|
|
42
|
+
` ${it.content.length > 120 ? it.content.slice(0, 120) + "…" : it.content}`,
|
|
43
|
+
)
|
|
44
|
+
if (it.sourceRefs.length > 0) lines.push(` 来源: ${it.sourceRefs.join(", ")}`)
|
|
45
|
+
}
|
|
46
|
+
lines.push(`</agent-memory>`)
|
|
47
|
+
return lines.join("\n") + "\n"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 刷新 L1 快照:repository 级核心约束/决策/陷阱/约定 */
|
|
51
|
+
export async function refreshL1Snapshot(deps: SnapshotDeps): Promise<SnapshotResult> {
|
|
52
|
+
const maxTokens = memoryMaxTokens()
|
|
53
|
+
const result = await recallPipeline(
|
|
54
|
+
{
|
|
55
|
+
query: "核心约束 关键决策 常见陷阱 项目约定",
|
|
56
|
+
stage: "session-start",
|
|
57
|
+
repositoryRef: deps.repositoryRef,
|
|
58
|
+
scopeCtx: { repository: deps.repositoryRef },
|
|
59
|
+
maxTokens,
|
|
60
|
+
kinds: ["CONSTRAINT", "DECISION", "PITFALL", "CONVENTION"],
|
|
61
|
+
limit: 20,
|
|
62
|
+
consumerRef: "memory-jobs:refresh-l1",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
lexical: deps.lexical,
|
|
66
|
+
fetchByIds: deps.fetchByIds,
|
|
67
|
+
fetchEvidenceSourceRefs: deps.fetchEvidenceSourceRefs,
|
|
68
|
+
audit: deps.audit,
|
|
69
|
+
degradedMode: "fts-only(snapshot-job)",
|
|
70
|
+
now: deps.now,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
const path = join(deps.projectDir, deps.magicDir, MEMORY_DIR_NAME, L1_SNAPSHOT_FILE)
|
|
74
|
+
atomicWrite(path, renderSnapshotMarkdown("L1", result.items, deps.now ?? new Date()))
|
|
75
|
+
return { path, itemCount: result.items.length, injectedTokens: result.injectedTokens, recallId: result.recallId }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 刷新 L2 快照:按给定 query/stage/scope(供 prompt-router 提示与人工查阅) */
|
|
79
|
+
export async function refreshL2Snapshot(
|
|
80
|
+
deps: SnapshotDeps,
|
|
81
|
+
query: string,
|
|
82
|
+
stage: string,
|
|
83
|
+
maxTokens = 1200,
|
|
84
|
+
): Promise<SnapshotResult> {
|
|
85
|
+
const result = await recallPipeline(
|
|
86
|
+
{
|
|
87
|
+
query,
|
|
88
|
+
stage,
|
|
89
|
+
repositoryRef: deps.repositoryRef,
|
|
90
|
+
scopeCtx: { repository: deps.repositoryRef },
|
|
91
|
+
maxTokens,
|
|
92
|
+
limit: 20,
|
|
93
|
+
consumerRef: "memory-jobs:refresh-l2",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
lexical: deps.lexical,
|
|
97
|
+
fetchByIds: deps.fetchByIds,
|
|
98
|
+
fetchEvidenceSourceRefs: deps.fetchEvidenceSourceRefs,
|
|
99
|
+
audit: deps.audit,
|
|
100
|
+
degradedMode: "fts-only(snapshot-job)",
|
|
101
|
+
now: deps.now,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
const path = join(deps.projectDir, deps.magicDir, MEMORY_DIR_NAME, L2_SNAPSHOT_FILE)
|
|
105
|
+
atomicWrite(path, renderSnapshotMarkdown("L2", result.items, deps.now ?? new Date()))
|
|
106
|
+
return { path, itemCount: result.items.length, injectedTokens: result.injectedTokens, recallId: result.recallId }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function atomicWrite(path: string, content: string): void {
|
|
110
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
111
|
+
const tmp = `${path}.tmp-${process.pid}`
|
|
112
|
+
writeFileSync(tmp, content, "utf-8")
|
|
113
|
+
renameSync(tmp, path)
|
|
114
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 工具侧阶段确定性召回(Plan §3.1 方案 B1 / Spec §2 §DeterministicRecall)
|
|
3
|
+
*
|
|
4
|
+
* 与 stage-words.ts 的分工:
|
|
5
|
+
* - stage-words.ts:纯函数,Hook 侧识别阶段词(无 DB)
|
|
6
|
+
* - 本模块:有 DB 的工具侧落点,复用既有 recallPipeline(不另起一套召回实现)
|
|
7
|
+
*
|
|
8
|
+
* 三条硬约束:
|
|
9
|
+
* 1. 阶段白名单命中才召回——未命中直接返回,不访问 DB、不写审计;
|
|
10
|
+
* 2. ADD_MEMORY_RECALL_MODE=off 时整体停用(与 recall_memory 工具同语义);
|
|
11
|
+
* 3. fail-open:管线异常只降级为 skippedReason="pipeline-error",绝不抛出,
|
|
12
|
+
* 不阻塞调用它的门禁/交接流程。
|
|
13
|
+
*/
|
|
14
|
+
import { recallPipeline, type RecallPipelineDeps, type RecallPipelineResult } from "../retrieval/pipeline.js"
|
|
15
|
+
import { buildContext, estimateTokens } from "../retrieval/context-builder.js"
|
|
16
|
+
import { recallMode, type RecallMode } from "../switches.js"
|
|
17
|
+
import type { ScopeContext } from "../domain/scope.js"
|
|
18
|
+
import { isRecallStage, RECALL_STAGES, type RecallStage } from "./stage-words.js"
|
|
19
|
+
|
|
20
|
+
/** 默认 token 预算(Spec §10:ADD_MEMORY_MAX_TOKENS 默认 600,此处与 Recall 通道对齐) */
|
|
21
|
+
export const DEFAULT_STAGE_RECALL_MAX_TOKENS = 600
|
|
22
|
+
|
|
23
|
+
export type StageRecallSkipReason = "stage-not-whitelisted" | "recall-off" | "pipeline-error"
|
|
24
|
+
|
|
25
|
+
export interface StageRecallInput {
|
|
26
|
+
/** 调用方声明的阶段;不在白名单内即不召回 */
|
|
27
|
+
stage: string
|
|
28
|
+
query: string
|
|
29
|
+
repositoryRef: string
|
|
30
|
+
scopeCtx?: Partial<ScopeContext>
|
|
31
|
+
planKeyword?: string
|
|
32
|
+
specRef?: string
|
|
33
|
+
maxTokens?: number
|
|
34
|
+
limit?: number
|
|
35
|
+
consumerRef?: string
|
|
36
|
+
diagnostic?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StageRecallResult {
|
|
40
|
+
injected: boolean
|
|
41
|
+
stage: RecallStage | null
|
|
42
|
+
context?: string
|
|
43
|
+
auditId?: string | null
|
|
44
|
+
degradedMode?: string | null
|
|
45
|
+
itemCount: number
|
|
46
|
+
usedTokens: number
|
|
47
|
+
latencyMs: number
|
|
48
|
+
skippedReason?: StageRecallSkipReason
|
|
49
|
+
error?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 按阶段执行确定性召回。deps 由调用方(MCP 工具)注入,便于测试与复用。
|
|
54
|
+
*/
|
|
55
|
+
export async function recallForStage(
|
|
56
|
+
input: StageRecallInput,
|
|
57
|
+
deps: RecallPipelineDeps,
|
|
58
|
+
options: { mode?: RecallMode } = {},
|
|
59
|
+
): Promise<StageRecallResult> {
|
|
60
|
+
const startedAt = Date.now()
|
|
61
|
+
const empty = (): Omit<StageRecallResult, "skippedReason" | "stage"> => ({
|
|
62
|
+
injected: false,
|
|
63
|
+
itemCount: 0,
|
|
64
|
+
usedTokens: 0,
|
|
65
|
+
latencyMs: Math.max(0, Date.now() - startedAt),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (!isRecallStage(input.stage)) {
|
|
69
|
+
return { ...empty(), stage: null, skippedReason: "stage-not-whitelisted" }
|
|
70
|
+
}
|
|
71
|
+
const mode = options.mode ?? recallMode()
|
|
72
|
+
if (mode === "off") {
|
|
73
|
+
return { ...empty(), stage: input.stage, skippedReason: "recall-off" }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const stage = input.stage
|
|
77
|
+
const maxTokens = input.maxTokens ?? DEFAULT_STAGE_RECALL_MAX_TOKENS
|
|
78
|
+
const scopeCtx: ScopeContext = {
|
|
79
|
+
repository: input.repositoryRef,
|
|
80
|
+
planKeyword: input.planKeyword,
|
|
81
|
+
specRef: input.specRef,
|
|
82
|
+
...input.scopeCtx,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result: RecallPipelineResult = await recallPipeline(
|
|
87
|
+
{
|
|
88
|
+
query: input.query,
|
|
89
|
+
stage,
|
|
90
|
+
repositoryRef: input.repositoryRef,
|
|
91
|
+
scopeCtx,
|
|
92
|
+
maxTokens,
|
|
93
|
+
limit: input.limit ?? 20,
|
|
94
|
+
consumerRef: input.consumerRef ?? `mcp:stage-recall:${stage}`,
|
|
95
|
+
diagnostic: input.diagnostic ?? false,
|
|
96
|
+
},
|
|
97
|
+
deps,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const budget = buildContext(
|
|
101
|
+
result.items.map((item) => ({
|
|
102
|
+
memoryId: item.memoryId,
|
|
103
|
+
kind: item.kind,
|
|
104
|
+
finalScore: item.scoreBreakdown?.final ?? item.confidence,
|
|
105
|
+
tokens: estimateTokens(item.content),
|
|
106
|
+
content: item.content,
|
|
107
|
+
sourceRefs: item.sourceRefs,
|
|
108
|
+
})),
|
|
109
|
+
maxTokens,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
injected: budget.selected.length > 0,
|
|
114
|
+
stage,
|
|
115
|
+
context: budget.selected.map((i) => i.content).join("\n\n"),
|
|
116
|
+
auditId: result.recallId,
|
|
117
|
+
degradedMode: result.degradedMode,
|
|
118
|
+
itemCount: budget.selected.length,
|
|
119
|
+
usedTokens: budget.usedTokens,
|
|
120
|
+
latencyMs: result.latencyMs,
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// fail-open:召回失败不阻塞门禁/交接;错误信息进审计与返回值
|
|
124
|
+
return {
|
|
125
|
+
...empty(),
|
|
126
|
+
stage,
|
|
127
|
+
skippedReason: "pipeline-error",
|
|
128
|
+
error: error instanceof Error ? error.message : String(error),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 供工具/文档展示的白名单(单一事实源来自 stage-words) */
|
|
134
|
+
export { RECALL_STAGES }
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Gate → MetricSnapshot 幂等采证(Plan §3.1 方案 A1 / Spec §1 §GateMetric)
|
|
3
|
+
*
|
|
4
|
+
* 设计要点:
|
|
5
|
+
* 1. 与评分解耦:评分逻辑保持只读,采证在评分完成后单独提交(Review P0 #1 / Plan §2.1);
|
|
6
|
+
* 2. 幂等键:sourceRef = `{gate}:{planKeyword}:{runId}`,配合表级唯一键
|
|
7
|
+
* (repositoryRef, metricType, sourceRef) —— 同 runId 重放不产生重复快照;
|
|
8
|
+
* 3. fail-open:任何异常都不向上抛,返回 outcome="bypassed" + degradedReason,
|
|
9
|
+
* 由调用方保证评分照常返回(验收项「采证失败旁路」);
|
|
10
|
+
* 4. 失败路径审计密度不低于成功路径(ADD-6):buildGateCaptureDetail 对三种 outcome
|
|
11
|
+
* 返回同构字段集。
|
|
12
|
+
*/
|
|
13
|
+
import type { AddMetricSnapshotRow, TableDelegate } from "../../db-types.js"
|
|
14
|
+
import { AddMetricSnapshotRowSchema, validatedDelegate } from "../../db-types.js"
|
|
15
|
+
import { MemoryError } from "../domain/errors.js"
|
|
16
|
+
import { createHash } from "node:crypto"
|
|
17
|
+
|
|
18
|
+
export type GateKind = "check_dps" | "check_rahs"
|
|
19
|
+
export type GateWriteOutcome = "written" | "skipped_duplicate" | "bypassed"
|
|
20
|
+
|
|
21
|
+
/** 门禁指标类型字面量(Spec §10 契约登记表) */
|
|
22
|
+
export const GATE_METRIC_TYPE = {
|
|
23
|
+
check_dps: "DPS_TOTAL",
|
|
24
|
+
check_rahs: "RAHS_TOTAL",
|
|
25
|
+
latency: "GATE_LATENCY_MS",
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
/** 采证耗时增量上限(ms)——验收项「工具响应耗时增量 ≤50ms」 */
|
|
29
|
+
export const GATE_CAPTURE_TOLERANCE_MS = 50
|
|
30
|
+
|
|
31
|
+
export interface GateMetricWriteInput {
|
|
32
|
+
gate: GateKind
|
|
33
|
+
planKeyword: string
|
|
34
|
+
runId: string
|
|
35
|
+
metricType?: string
|
|
36
|
+
score: number
|
|
37
|
+
repository: string
|
|
38
|
+
dimensionScores?: Record<string, number>
|
|
39
|
+
baseline?: number | null
|
|
40
|
+
unit?: string | null
|
|
41
|
+
specRef?: string | null
|
|
42
|
+
commitSha?: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface GateMetricWriteResult {
|
|
46
|
+
outcome: GateWriteOutcome
|
|
47
|
+
sourceRef: string
|
|
48
|
+
metricId?: string
|
|
49
|
+
degradedReason?: string
|
|
50
|
+
elapsedMs: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GateWriterDeps {
|
|
54
|
+
/** 采证目标表;测试可注入内存实现 */
|
|
55
|
+
metricDb: Pick<TableDelegate<AddMetricSnapshotRow>, "findUnique" | "upsert">
|
|
56
|
+
/** 时钟注入(测试用),默认 Date.now */
|
|
57
|
+
now?: () => number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 幂等键构造。空值归一为 "-",避免出现 `::` 与前缀碰撞。
|
|
62
|
+
*/
|
|
63
|
+
export function buildGateSourceRef(gate: GateKind, planKeyword: string, runId: string): string {
|
|
64
|
+
const norm = (v: string | undefined | null): string => {
|
|
65
|
+
const t = (v ?? "").trim()
|
|
66
|
+
return t.length > 0 ? t : "-"
|
|
67
|
+
}
|
|
68
|
+
return `${gate}:${norm(planKeyword)}:${norm(runId)}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 由被评分产物的内容派生 runId。
|
|
73
|
+
*
|
|
74
|
+
* 语义:同一份文档重复跑同一门禁 → 同一幂等键 → 判定为重复采证(skipped_duplicate);
|
|
75
|
+
* 文档内容变化 → 新幂等键 → 产生新的证据快照。这样「重放」不会因为时间戳不同而假性去重,
|
|
76
|
+
* 也不需要调用方额外传参(门禁工具入参契约保持不变)。
|
|
77
|
+
*/
|
|
78
|
+
export function deriveGateRunId(gate: GateKind, planKeyword: string, seed: string): string {
|
|
79
|
+
const digest = createHash("sha256")
|
|
80
|
+
.update(`${gate}|${planKeyword}|${seed}`)
|
|
81
|
+
.digest("hex")
|
|
82
|
+
return `auto-${digest.slice(0, 12)}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 唯一键参数(与 prisma @@unique([repositoryRef, metricType, sourceRef]) 对应) */
|
|
86
|
+
function uniqueWhere(repositoryRef: string, metricType: string, sourceRef: string) {
|
|
87
|
+
return { repositoryRef_metricType_sourceRef: { repositoryRef, metricType, sourceRef } }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isUniqueViolation(error: unknown): boolean {
|
|
91
|
+
const code = (error as { code?: string } | null)?.code
|
|
92
|
+
return code === "P2002"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 写入一次门禁指标快照。**不抛异常**:失败返回 bypassed,由调用方继续返回评分。
|
|
97
|
+
*/
|
|
98
|
+
export async function writeGateMetric(
|
|
99
|
+
input: GateMetricWriteInput,
|
|
100
|
+
deps: GateWriterDeps,
|
|
101
|
+
): Promise<GateMetricWriteResult> {
|
|
102
|
+
const now = deps.now ?? Date.now
|
|
103
|
+
const startedAt = now()
|
|
104
|
+
const metricType = input.metricType ?? GATE_METRIC_TYPE[input.gate]
|
|
105
|
+
const sourceRef = buildGateSourceRef(input.gate, input.planKeyword, input.runId)
|
|
106
|
+
const elapsed = () => Math.max(0, now() - startedAt)
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (!input.repository || input.repository.trim().length === 0) {
|
|
110
|
+
throw new MemoryError("ERR_REPOSITORY_MISMATCH", "repositoryRef 为空,拒绝采证")
|
|
111
|
+
}
|
|
112
|
+
if (!Number.isFinite(input.score)) {
|
|
113
|
+
throw new MemoryError("ERR_INVARIANT", "score 非有限数值,拒绝采证")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const existing = await deps.metricDb.findUnique({
|
|
117
|
+
where: uniqueWhere(input.repository, metricType, sourceRef),
|
|
118
|
+
})
|
|
119
|
+
if (existing) {
|
|
120
|
+
// 同 runId 重放:既有快照是证据,不覆写
|
|
121
|
+
return { outcome: "skipped_duplicate", metricId: existing.id, sourceRef, elapsedMs: elapsed() }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const created = await deps.metricDb.upsert({
|
|
125
|
+
where: uniqueWhere(input.repository, metricType, sourceRef),
|
|
126
|
+
create: {
|
|
127
|
+
repositoryRef: input.repository,
|
|
128
|
+
metricType,
|
|
129
|
+
value: input.score,
|
|
130
|
+
baseline: input.baseline ?? null,
|
|
131
|
+
delta: input.baseline == null ? null : input.score - input.baseline,
|
|
132
|
+
unit: input.unit ?? null,
|
|
133
|
+
planKeyword: input.planKeyword || null,
|
|
134
|
+
specRef: input.specRef ?? null,
|
|
135
|
+
commitSha: input.commitSha ?? null,
|
|
136
|
+
sourceRef,
|
|
137
|
+
metadata: {
|
|
138
|
+
gate: input.gate,
|
|
139
|
+
runId: input.runId,
|
|
140
|
+
dimensionScores: input.dimensionScores ?? null,
|
|
141
|
+
capture: "gate-writer",
|
|
142
|
+
},
|
|
143
|
+
measuredAt: new Date(now()),
|
|
144
|
+
},
|
|
145
|
+
// 幂等:并发下由唯一键裁定,update 不改写既有证据
|
|
146
|
+
update: {},
|
|
147
|
+
})
|
|
148
|
+
return { outcome: "written", metricId: created.id, sourceRef, elapsedMs: elapsed() }
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (isUniqueViolation(error)) {
|
|
151
|
+
return { outcome: "skipped_duplicate", sourceRef, elapsedMs: elapsed() }
|
|
152
|
+
}
|
|
153
|
+
// fail-open:采证失败绝不阻塞门禁返回评分
|
|
154
|
+
return {
|
|
155
|
+
outcome: "bypassed",
|
|
156
|
+
sourceRef,
|
|
157
|
+
degradedReason: error instanceof Error ? error.message : String(error),
|
|
158
|
+
elapsedMs: elapsed(),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 审计明细(ADD-6:成功/去重/旁路三态返回同构字段集,失败路径不得更稀疏)。
|
|
165
|
+
*/
|
|
166
|
+
export function buildGateCaptureDetail(
|
|
167
|
+
input: GateMetricWriteInput,
|
|
168
|
+
result: GateMetricWriteResult,
|
|
169
|
+
): Record<string, unknown> {
|
|
170
|
+
return {
|
|
171
|
+
gate: input.gate,
|
|
172
|
+
planKeyword: input.planKeyword,
|
|
173
|
+
runId: input.runId,
|
|
174
|
+
metricType: input.metricType ?? GATE_METRIC_TYPE[input.gate],
|
|
175
|
+
sourceRef: result.sourceRef,
|
|
176
|
+
outcome: result.outcome,
|
|
177
|
+
metricId: result.metricId ?? null,
|
|
178
|
+
score: input.score,
|
|
179
|
+
dimensionCount: input.dimensionScores ? Object.keys(input.dimensionScores).length : 0,
|
|
180
|
+
baseline: input.baseline ?? null,
|
|
181
|
+
degradedReason: result.degradedReason ?? null,
|
|
182
|
+
elapsedMs: result.elapsedMs,
|
|
183
|
+
overTolerance: result.elapsedMs > GATE_CAPTURE_TOLERANCE_MS,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 单行摘要:供 MCP 工具在响应末尾追加(三态均返回非空文本) */
|
|
188
|
+
export function formatGateCaptureSummary(detail: Record<string, unknown>): string {
|
|
189
|
+
const bits = [
|
|
190
|
+
`结果: ${String(detail.outcome)}`,
|
|
191
|
+
`幂等键: ${String(detail.sourceRef)}`,
|
|
192
|
+
`耗时: ${String(detail.elapsedMs)}ms`,
|
|
193
|
+
]
|
|
194
|
+
if (typeof detail.metricId === "string" && detail.metricId.length > 0) {
|
|
195
|
+
bits.push(`metricId: ${detail.metricId}`)
|
|
196
|
+
}
|
|
197
|
+
if (typeof detail.degradedReason === "string" && detail.degradedReason.length > 0) {
|
|
198
|
+
bits.push(`旁路原因: ${detail.degradedReason}(评分不受影响)`)
|
|
199
|
+
}
|
|
200
|
+
if (detail.overTolerance === true) {
|
|
201
|
+
bits.push(`⚠️ 超出 ${GATE_CAPTURE_TOLERANCE_MS}ms 容差`)
|
|
202
|
+
}
|
|
203
|
+
return bits.join(" | ")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 便捷装配:把 MCP 工具侧的 prisma delegate 包装为通过行校验的采证依赖。
|
|
208
|
+
*/
|
|
209
|
+
export function createGateWriterDeps(rawMetricDelegate: unknown): GateWriterDeps {
|
|
210
|
+
return {
|
|
211
|
+
metricDb: validatedDelegate<AddMetricSnapshotRow>(
|
|
212
|
+
rawMetricDelegate,
|
|
213
|
+
AddMetricSnapshotRowSchema,
|
|
214
|
+
"AddMetricSnapshot",
|
|
215
|
+
),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 阶段词识别(Plan §9.1 / Spec §2 §DeterministicRecall)
|
|
3
|
+
*
|
|
4
|
+
* 本模块是**纯函数**:零运行时依赖、零 IO、零 DB。
|
|
5
|
+
* 存在意义:governance Hook(同步 spawn、≤200ms 预算、无 DB 访问权限)需要识别
|
|
6
|
+
* 「现在处于哪个 ADD 阶段」,以便产出确定性召回提示;而真正的召回落点在有 DB 的
|
|
7
|
+
* MCP 工具侧(gate-recall.ts)。两者共用同一份阶段白名单,避免 Hook 与工具侧漂移。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const RECALL_STAGES = ["plan-start", "spec-start", "dps", "rahs", "handoff"] as const
|
|
11
|
+
export type RecallStage = (typeof RECALL_STAGES)[number]
|
|
12
|
+
|
|
13
|
+
export function isRecallStage(value: string): value is RecallStage {
|
|
14
|
+
return (RECALL_STAGES as readonly string[]).includes(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const STAGE_LABEL: Record<RecallStage, string> = {
|
|
18
|
+
"plan-start": "Plan 起草",
|
|
19
|
+
"spec-start": "Spec 起草",
|
|
20
|
+
dps: "DPS 门禁",
|
|
21
|
+
rahs: "RAHS 门禁",
|
|
22
|
+
handoff: "Handoff 交接",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 阶段词模式。按特异性从高到低排列——先命中者为准(handoff/rahs/dps 比 plan 更具体,
|
|
27
|
+
* 避免「交接 plan」被判成 plan-start)。
|
|
28
|
+
*/
|
|
29
|
+
const STAGE_PATTERNS: ReadonlyArray<{ stage: RecallStage; re: RegExp }> = [
|
|
30
|
+
{ stage: "handoff", re: /handoff|交接(手册|文档|说明)?|交接给/i },
|
|
31
|
+
{ stage: "rahs", re: /rahs|注意力漂移|执行健康度/i },
|
|
32
|
+
{ stage: "dps", re: /dps|文档质量闸门|质量闸门|门禁(评分|检查)?/i },
|
|
33
|
+
{ stage: "spec-start", re: /(生成|写|新建|起草|补)\s*(spec|规格|三元组)|specs?\s*三元组|WHEN-?THEN/i },
|
|
34
|
+
{ stage: "plan-start", re: /(生成|写|新建|起草|补)\s*(plan|计划|方案)|plan\s*阶段|规划阶段/i },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/** 识别提示词所属的 ADD 阶段;无法判定返回 null(不猜测) */
|
|
38
|
+
export function detectRecallStage(prompt: string): RecallStage | null {
|
|
39
|
+
if (!prompt) return null
|
|
40
|
+
for (const { stage, re } of STAGE_PATTERNS) {
|
|
41
|
+
if (re.test(prompt)) return stage
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type RecallModeLike = "off" | "shadow" | "inject"
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 阶段召回提示文本(Hook 侧输出)。
|
|
50
|
+
* - off:返回 null(不提示、不召回)
|
|
51
|
+
* - shadow:提示可显式调用,并说明 Hook 不注入
|
|
52
|
+
* - inject:提示调用后将注入上下文
|
|
53
|
+
*/
|
|
54
|
+
export function buildStageRecallHint(
|
|
55
|
+
stage: RecallStage,
|
|
56
|
+
mode: RecallModeLike,
|
|
57
|
+
): string | null {
|
|
58
|
+
if (mode === "off") return null
|
|
59
|
+
const label = STAGE_LABEL[stage]
|
|
60
|
+
const tail =
|
|
61
|
+
mode === "shadow"
|
|
62
|
+
? "(当前 shadow 模式:Hook 不注入上下文,召回结果需显式消费)"
|
|
63
|
+
: "(当前 inject 模式:调用后上下文将被注入)"
|
|
64
|
+
return (
|
|
65
|
+
`[Memory] 检测到${label}阶段 → 建议调用 ` +
|
|
66
|
+
`recall_memory({ stage: "${stage}", query: <本阶段意图>, planKeyword: <Plan 关键词> }) ` +
|
|
67
|
+
`获取受治理的历史上下文(含来源与评分)。${tail}\n`
|
|
68
|
+
)
|
|
69
|
+
}
|