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.
Files changed (147) hide show
  1. package/dist/index.js +21 -3
  2. package/package.json +3 -3
  3. package/templates/.add-coder-src-hash.json +144 -90
  4. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -88
  5. package/templates/adapters/claude/hooks/notification.mjs +0 -4
  6. package/templates/adapters/claude/hooks/permission-denied.mjs +0 -4
  7. package/templates/adapters/claude/hooks/permission-gate.mjs +0 -4
  8. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -5
  9. package/templates/adapters/claude/hooks/pre-compact.mjs +0 -4
  10. package/templates/adapters/claude/hooks/pre-tool-use.mjs +0 -4
  11. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -4
  12. package/templates/adapters/claude/hooks/session-end.mjs +0 -4
  13. package/templates/adapters/claude/hooks/session-start.mjs +65 -5
  14. package/templates/adapters/claude/hooks/stop-check.mjs +6 -87
  15. package/templates/adapters/claude/hooks/stop-failure.mjs +0 -4
  16. package/templates/adapters/claude/hooks/subagent-guard.mjs +0 -4
  17. package/templates/adapters/claude/hooks/subagent-stop.mjs +0 -4
  18. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -88
  19. package/templates/adapters/codex/hooks/notification.mjs +0 -4
  20. package/templates/adapters/codex/hooks/permission-gate.mjs +0 -4
  21. package/templates/adapters/codex/hooks/post-tool-failure.mjs +0 -4
  22. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -5
  23. package/templates/adapters/codex/hooks/pre-compact.mjs +0 -4
  24. package/templates/adapters/codex/hooks/pre-tool-use.mjs +0 -4
  25. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -4
  26. package/templates/adapters/codex/hooks/session-end.mjs +0 -4
  27. package/templates/adapters/codex/hooks/session-start.mjs +65 -5
  28. package/templates/adapters/codex/hooks/stop-check.mjs +6 -87
  29. package/templates/adapters/codex/hooks/subagent-guard.mjs +0 -4
  30. package/templates/adapters/codex/hooks/subagent-stop.mjs +0 -4
  31. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -88
  32. package/templates/adapters/qoder/hooks/notification.mjs +0 -4
  33. package/templates/adapters/qoder/hooks/permission-gate.mjs +0 -4
  34. package/templates/adapters/qoder/hooks/post-tool-failure.mjs +0 -4
  35. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -5
  36. package/templates/adapters/qoder/hooks/pre-compact.mjs +0 -4
  37. package/templates/adapters/qoder/hooks/pre-tool-use.mjs +0 -4
  38. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -4
  39. package/templates/adapters/qoder/hooks/review-checklist.mjs +0 -4
  40. package/templates/adapters/qoder/hooks/session-end.mjs +0 -4
  41. package/templates/adapters/qoder/hooks/session-start.mjs +67 -5
  42. package/templates/adapters/qoder/hooks/stop-check.mjs +6 -87
  43. package/templates/adapters/qoder/hooks/subagent-guard.mjs +0 -4
  44. package/templates/adapters/qoder/hooks/subagent-stop.mjs +0 -4
  45. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -88
  46. package/templates/adapters/trae/hooks/notification.mjs +0 -4
  47. package/templates/adapters/trae/hooks/permission-gate.mjs +0 -4
  48. package/templates/adapters/trae/hooks/post-tool-failure.mjs +0 -4
  49. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -5
  50. package/templates/adapters/trae/hooks/pre-compact.mjs +0 -4
  51. package/templates/adapters/trae/hooks/pre-tool-use.mjs +0 -4
  52. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -4
  53. package/templates/adapters/trae/hooks/session-end.mjs +0 -4
  54. package/templates/adapters/trae/hooks/session-start.mjs +65 -5
  55. package/templates/adapters/trae/hooks/stop-check.mjs +6 -87
  56. package/templates/adapters/trae/hooks/subagent-guard.mjs +0 -4
  57. package/templates/adapters/trae/hooks/subagent-stop.mjs +0 -4
  58. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -88
  59. package/templates/adapters/vscode/hooks/notification.mjs +0 -4
  60. package/templates/adapters/vscode/hooks/permission-gate.mjs +0 -4
  61. package/templates/adapters/vscode/hooks/post-tool-failure.mjs +0 -4
  62. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -5
  63. package/templates/adapters/vscode/hooks/pre-compact.mjs +0 -4
  64. package/templates/adapters/vscode/hooks/pre-tool-use.mjs +0 -4
  65. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -4
  66. package/templates/adapters/vscode/hooks/session-end.mjs +0 -4
  67. package/templates/adapters/vscode/hooks/session-start.mjs +65 -5
  68. package/templates/adapters/vscode/hooks/stop-check.mjs +6 -87
  69. package/templates/adapters/vscode/hooks/subagent-guard.mjs +0 -4
  70. package/templates/adapters/vscode/hooks/subagent-stop.mjs +0 -4
  71. package/templates/core/governance/doc-format-guard.ts +29 -112
  72. package/templates/core/governance/post-tool-router.ts +33 -1
  73. package/templates/core/governance/prompt-router.ts +47 -0
  74. package/templates/core/governance/rules.ts +0 -4
  75. package/templates/core/governance/session-start-guard.ts +48 -1
  76. package/templates/core/governance/stop-router.ts +7 -101
  77. package/templates/core/prisma/add.prisma +203 -0
  78. package/templates/core/scripts/db-ensure.sh +61 -2
  79. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  80. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  81. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  82. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  83. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  84. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  85. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  86. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  87. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  88. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  89. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  90. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  91. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  92. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  93. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  94. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  95. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  96. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  97. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  98. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  99. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  100. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  101. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  102. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  103. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  104. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  105. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  106. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  107. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  108. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  109. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  110. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  111. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  112. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  113. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  114. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  115. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  116. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  117. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  118. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
  119. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  120. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  121. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  122. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  123. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  124. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  125. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  126. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  127. package/templates/core/scripts/mcp-server.ts +36 -0
  128. package/templates/core/templates/checklist-template.md +13 -0
  129. package/templates/core/templates/review-implementation-template.md +24 -0
  130. package/templates/core/templates/review-template.md +16 -0
  131. package/templates/core/validation/index.ts +136 -0
  132. package/templates/core/validation/policy.ts +91 -0
  133. package/templates/core/validation/registry.ts +61 -0
  134. package/templates/core/validation/schema-validator.ts +277 -0
  135. package/templates/core/validation/validators/add-route.ts +32 -0
  136. package/templates/core/validation/validators/checklist.ts +48 -0
  137. package/templates/core/validation/validators/handoff.ts +46 -0
  138. package/templates/core/validation/validators/hitl.ts +22 -0
  139. package/templates/core/validation/validators/index.ts +52 -0
  140. package/templates/core/validation/validators/plan.ts +20 -0
  141. package/templates/core/validation/validators/report.ts +16 -0
  142. package/templates/core/validation/validators/review.ts +30 -0
  143. package/templates/core/validation/validators/spec.ts +25 -0
  144. package/templates/core/validation/validators/tasks.ts +40 -0
  145. package/templates/core/validation/validators/types.ts +32 -0
  146. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
  147. package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Memory 生命周期状态机(Plan §5)
3
+ *
4
+ * CANDIDATE → PENDING → ACTIVE → STALE → SUPERSEDED → ARCHIVED
5
+ * │ │ │ └──────────────→ ARCHIVED
6
+ * └──────────┴─────────┴──────────────────────→ REJECTED
7
+ *
8
+ * 首版默认只允许人工或显式治理调用激活(approve 的强制校验在此表达)。
9
+ */
10
+ import { MemoryError } from "./errors.js"
11
+
12
+ export type MemoryStatus =
13
+ | "CANDIDATE" | "PENDING" | "ACTIVE" | "STALE"
14
+ | "SUPERSEDED" | "REJECTED" | "ARCHIVED"
15
+
16
+ export type MemoryAction =
17
+ | "propose" | "submit_review" | "approve" | "reject"
18
+ | "mark_stale" | "supersede" | "archive" | "restore"
19
+
20
+ interface Transition {
21
+ from: MemoryStatus[]
22
+ to: MemoryStatus
23
+ }
24
+
25
+ /** 迁移表:propose 是创建语义(无 from),其余均为状态到状态 */
26
+ export const TRANSITIONS: Record<MemoryAction, Transition> = {
27
+ propose: { from: [], to: "CANDIDATE" },
28
+ submit_review: { from: ["CANDIDATE"], to: "PENDING" },
29
+ approve: { from: ["PENDING", "CANDIDATE"], to: "ACTIVE" },
30
+ reject: { from: ["CANDIDATE", "PENDING"], to: "REJECTED" },
31
+ mark_stale: { from: ["ACTIVE"], to: "STALE" },
32
+ supersede: { from: ["ACTIVE", "STALE"], to: "SUPERSEDED" },
33
+ archive: { from: ["ACTIVE", "STALE", "SUPERSEDED"], to: "ARCHIVED" },
34
+ restore: { from: ["ARCHIVED", "REJECTED"], to: "CANDIDATE" },
35
+ }
36
+
37
+ /** approve / supersede 的不变量校验输入(DB 查询结果由调用方传入,领域层保持纯函数) */
38
+ export interface TransitionContext {
39
+ evidenceCount?: number
40
+ approvedBy?: string | null
41
+ approvedAt?: Date | null
42
+ /** supersede 专用:新记忆 id 与 repository/scope 兼容性判定结果 */
43
+ supersededById?: string | null
44
+ supersessionCompatible?: boolean
45
+ }
46
+
47
+ export interface TransitionResult {
48
+ from: MemoryStatus
49
+ to: MemoryStatus
50
+ action: MemoryAction
51
+ }
52
+
53
+ /** 判定迁移是否合法;非法时抛 MemoryError(ERR_ILLEGAL_TRANSITION / ERR_EVIDENCE_REQUIRED / ...) */
54
+ export function assertTransition(
55
+ current: MemoryStatus,
56
+ action: MemoryAction,
57
+ ctx: TransitionContext = {},
58
+ ): TransitionResult {
59
+ const t = TRANSITIONS[action]
60
+ if (action === "propose") {
61
+ // 创建语义:不与现有状态冲突(由调用方决定新建或合并)
62
+ return { from: current, to: t.to, action }
63
+ }
64
+ if (!t.from.includes(current)) {
65
+ throw new MemoryError("ERR_ILLEGAL_TRANSITION", `${current} --${action}--> ${t.to} 不允许(允许起点: ${t.from.join("/")})`)
66
+ }
67
+ if (action === "approve") {
68
+ if (!ctx.evidenceCount || ctx.evidenceCount < 1) {
69
+ throw new MemoryError("ERR_EVIDENCE_REQUIRED")
70
+ }
71
+ if (!ctx.approvedBy) {
72
+ throw new MemoryError("ERR_APPROVAL_REQUIRED", "缺少 approvedBy")
73
+ }
74
+ }
75
+ if (action === "supersede") {
76
+ if (!ctx.supersededById) {
77
+ throw new MemoryError("ERR_SUPERSESSION_INVALID", "缺少 supersededById")
78
+ }
79
+ if (ctx.supersessionCompatible === false) {
80
+ throw new MemoryError("ERR_SUPERSESSION_INVALID", "新旧 Memory 的 repository/scope 不兼容")
81
+ }
82
+ }
83
+ return { from: current, to: t.to, action }
84
+ }
85
+
86
+ /** 默认召回允许的状态集合(Plan §7.1:默认排除 CANDIDATE/PENDING/REJECTED/ARCHIVED/SUPERSEDED/过期项) */
87
+ export const DEFAULT_RECALL_STATUSES: readonly MemoryStatus[] = ["ACTIVE"]
88
+
89
+ /** 显式诊断模式额外放行 STALE(带警告) */
90
+ export const DIAGNOSTIC_RECALL_STATUSES: readonly MemoryStatus[] = ["ACTIVE", "STALE"]
@@ -0,0 +1,117 @@
1
+ /*
2
+ * Embedding 抽象与降级(Plan §8.1/§8.4,Spec §9)
3
+ *
4
+ * 提供者:none(默认,显式不可用)/ local-onnx / openai-compatible(Phase 5 轮 3)。
5
+ * Vector adapter:pgvector / sqlite-vec(retrieval/vector/),能力检测失败即降级。
6
+ * 契约:记忆系统任何环节不得因 embedding 缺失而阻塞 Gate(FTS-only 合法降级)。
7
+ */
8
+ import { MemoryError } from "../domain/errors.js"
9
+ import type { ComponentHealth, RankedId, RecallFilter } from "../retrieval/types.js"
10
+
11
+ export interface EmbeddingProvider {
12
+ readonly id: string // "none" | "local-onnx" | "openai-compatible" | "custom"
13
+ readonly dimension: number
14
+ embed(texts: string[]): Promise<number[][]>
15
+ health(): Promise<ComponentHealth>
16
+ }
17
+
18
+ export interface VectorSearchAdapter {
19
+ search(vector: number[], filter: RecallFilter, limit: number): Promise<RankedId[]>
20
+ upsert(memoryId: string, vector: number[], model: string): Promise<void>
21
+ health(): Promise<ComponentHealth>
22
+ }
23
+
24
+ /** 首版唯一 provider:显式不可用。调用方据此走 FTS-only 并标注 degradedMode */
25
+ export function createNoneEmbeddingProvider(): EmbeddingProvider {
26
+ return {
27
+ id: "none",
28
+ dimension: 0,
29
+ embed: (): Promise<number[][]> => Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED")),
30
+ health: (): Promise<ComponentHealth> =>
31
+ Promise.resolve({ component: "embedding", status: "disabled", detail: "EmbeddingProvider=none(首版定案,FTS-only)" }),
32
+ }
33
+ }
34
+
35
+ /** Vector adapter capability 存根:health 报告 unavailable,search/upsert 拒绝 */
36
+ export function createUnavailableVectorAdapter(): VectorSearchAdapter {
37
+ return {
38
+ search: (): Promise<RankedId[]> =>
39
+ Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED", "VectorSearchAdapter 未配置(Phase 5 接入)")),
40
+ upsert: (): Promise<void> =>
41
+ Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED", "VectorSearchAdapter 未配置(Phase 5 接入)")),
42
+ health: (): Promise<ComponentHealth> =>
43
+ Promise.resolve({ component: "vector-search", status: "unavailable", detail: "向量索引未启用(Phase 5 接入 pgvector/sqlite-vec)" }),
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 维度真源校验(Review R5 回流):provider 上报维度必须与 DB 侧声明维度一致,否则拒写。
49
+ * declaredDim 来自 AddMemory.embeddingDim(写入路径的真源)。
50
+ */
51
+ export function assertEmbeddingDimension(providerDim: number, declaredDim: number): void {
52
+ if (declaredDim <= 0) return // 未声明维度:首写,由 provider 值落库
53
+ if (providerDim !== declaredDim) {
54
+ throw new MemoryError(
55
+ "ERR_DIMENSION_MISMATCH",
56
+ `嵌入维度不一致:provider=${providerDim} vs 声明=${declaredDim}(拒绝写入,避免脏向量)`,
57
+ )
58
+ }
59
+ }
60
+
61
+ export type EmbeddingMode = "none" | "local-onnx" | "openai-compatible"
62
+
63
+ export interface EmbeddingEnvConfig {
64
+ mode?: string
65
+ model?: string
66
+ dimension?: number
67
+ baseUrl?: string
68
+ apiKey?: string
69
+ cacheDir?: string
70
+ remoteHost?: string
71
+ }
72
+
73
+ /** 从环境变量解析提供者配置(纯函数,便于测试与文档化) */
74
+ export function parseEmbeddingEnv(env: NodeJS.ProcessEnv = process.env): EmbeddingEnvConfig {
75
+ const mode = (env.ADD_MEMORY_EMBEDDING ?? "none").toLowerCase()
76
+ const dim = Number(env.ADD_MEMORY_EMBEDDING_DIM ?? "")
77
+ return {
78
+ mode,
79
+ model: env.ADD_MEMORY_EMBEDDING_MODEL,
80
+ dimension: Number.isFinite(dim) && dim > 0 ? Math.floor(dim) : undefined,
81
+ baseUrl: env.ADD_MEMORY_EMBEDDING_BASE_URL,
82
+ apiKey: env.ADD_MEMORY_EMBEDDING_API_KEY,
83
+ cacheDir: env.ADD_MEMORY_EMBEDDING_CACHE_DIR,
84
+ remoteHost: env.ADD_MEMORY_EMBEDDING_REMOTE_HOST,
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 按配置装配提供者(默认 none → 行为与首版一致)。
90
+ * local-onnx / openai-compatible 走动态 import,未配置的路径不加载任何重依赖。
91
+ */
92
+ export async function createEmbeddingProviderFromConfig(
93
+ config: EmbeddingEnvConfig = parseEmbeddingEnv(),
94
+ ): Promise<EmbeddingProvider> {
95
+ const mode = (config.mode ?? "none") as EmbeddingMode
96
+ if (mode === "local-onnx") {
97
+ const { createLocalOnnxEmbeddingProvider } = await import("./local-onnx.js")
98
+ return createLocalOnnxEmbeddingProvider({
99
+ model: config.model,
100
+ cacheDir: config.cacheDir,
101
+ remoteHost: config.remoteHost,
102
+ })
103
+ }
104
+ if (mode === "openai-compatible") {
105
+ if (!config.baseUrl) {
106
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", "openai-compatible 需要 ADD_MEMORY_EMBEDDING_BASE_URL")
107
+ }
108
+ const { createOpenAiCompatibleEmbeddingProvider } = await import("./openai-compatible.js")
109
+ return createOpenAiCompatibleEmbeddingProvider({
110
+ baseUrl: config.baseUrl,
111
+ model: config.model ?? "text-embedding-3-small",
112
+ apiKey: config.apiKey,
113
+ dimension: config.dimension,
114
+ })
115
+ }
116
+ return createNoneEmbeddingProvider()
117
+ }
@@ -0,0 +1,105 @@
1
+ /*
2
+ * 本地 ONNX 嵌入提供者(Plan §3.4 轮 3 Task 3.1 / Spec §6 §VectorCapability)
3
+ *
4
+ * 关键约束:
5
+ * 1. **可选依赖**:@huggingface/transformers 与模型权重都可能在目标机器缺失——加载失败
6
+ * 必须收敛为 ERR_EMBEDDING_DISABLED(调用方据此走 fts-only),绝不抛裸异常阻塞 Gate;
7
+ * 2. **维度以模型元数据为真源**:dimension 由首次推理的实际输出长度决定(不硬编码),
8
+ * 与 DB 侧 AddMemory.embeddingDim 不一致时由调用方拒绝写入(Review R5 回流);
9
+ * 3. 惰性加载:不在模块顶层 import/下载模型,首次 embed 时才初始化。
10
+ */
11
+ import { MemoryError } from "../domain/errors.js"
12
+ import type { ComponentHealth } from "../retrieval/types.js"
13
+ import type { EmbeddingProvider } from "./index.js"
14
+
15
+ /** 默认模型:中文小模型(真源维度 512,与 Plan R5「维度真源」一致) */
16
+ export const DEFAULT_LOCAL_ONNX_MODEL = "Xenova/bge-small-zh-v1.5"
17
+
18
+ export interface LocalOnnxOptions {
19
+ model?: string
20
+ /** 显式缓存目录;缺省复用 HF_HUB_CACHE → HF_HOME/hub → ~/.cache/huggingface/hub */
21
+ cacheDir?: string
22
+ /** 镜像站(内网/受限网络) */
23
+ remoteHost?: string
24
+ /** 测试注入:直接给定 pipeline 工厂 */
25
+ loadPipeline?: (model: string) => Promise<(texts: string[]) => Promise<number[][]>>
26
+ }
27
+
28
+ export interface LocalOnnxProvider extends EmbeddingProvider {
29
+ /** 首次推理后可用;未初始化时为 0(调用方以 assertEmbeddingDimension 校验) */
30
+ readonly resolvedDimension: number
31
+ }
32
+
33
+ export function createLocalOnnxEmbeddingProvider(opts: LocalOnnxOptions = {}): LocalOnnxProvider {
34
+ const model = opts.model ?? DEFAULT_LOCAL_ONNX_MODEL
35
+ let embedFn: ((texts: string[]) => Promise<number[][]>) | null = null
36
+ let dimension = 0
37
+
38
+ async function ensureLoaded(): Promise<(texts: string[]) => Promise<number[][]>> {
39
+ if (embedFn) return embedFn
40
+ try {
41
+ if (opts.loadPipeline) {
42
+ embedFn = await opts.loadPipeline(model)
43
+ } else {
44
+ const { pipeline, env } = await import("@huggingface/transformers")
45
+ if (opts.cacheDir) env.cacheDir = opts.cacheDir
46
+ if (opts.remoteHost) env.remoteHost = opts.remoteHost
47
+ const extractor = await pipeline("feature-extraction", model)
48
+ embedFn = async (texts: string[]) => {
49
+ const result = await extractor(texts, { pooling: "mean", normalize: true })
50
+ const list = result.tolist() as number[][]
51
+ return list.length === texts.length ? list : [list as unknown as number[]]
52
+ }
53
+ }
54
+ return embedFn
55
+ } catch (error) {
56
+ throw new MemoryError(
57
+ "ERR_EMBEDDING_DISABLED",
58
+ `本地 ONNX 模型不可用(model=${model}):${error instanceof Error ? error.message : String(error)}`,
59
+ )
60
+ }
61
+ }
62
+
63
+ return {
64
+ id: "local-onnx",
65
+ get dimension(): number {
66
+ return dimension
67
+ },
68
+ get resolvedDimension(): number {
69
+ return dimension
70
+ },
71
+ async embed(texts: string[]): Promise<number[][]> {
72
+ if (texts.length === 0) return []
73
+ const fn = await ensureLoaded()
74
+ let vectors: number[][]
75
+ try {
76
+ vectors = await fn(texts)
77
+ } catch (error) {
78
+ throw new MemoryError(
79
+ "ERR_EMBEDDING_DISABLED",
80
+ `本地 ONNX 推理失败(model=${model}):${error instanceof Error ? error.message : String(error)}`,
81
+ )
82
+ }
83
+ const dim = vectors[0]?.length ?? 0
84
+ if (dim === 0) {
85
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", `本地 ONNX 返回空向量(model=${model})`)
86
+ }
87
+ // 维度真源:以模型实际输出为准;同一 provider 内必须自洽
88
+ if (dimension !== 0 && dimension !== dim) {
89
+ throw new MemoryError("ERR_DIMENSION_MISMATCH", `provider 维度漂移:${dimension} → ${dim}`)
90
+ }
91
+ dimension = dim
92
+ return vectors
93
+ },
94
+ health(): Promise<ComponentHealth> {
95
+ if (embedFn && dimension > 0) {
96
+ return Promise.resolve({ component: "embedding", status: "ok", detail: `local-onnx model=${model} dim=${dimension}` })
97
+ }
98
+ return Promise.resolve({
99
+ component: "embedding",
100
+ status: "degraded",
101
+ detail: `local-onnx 未初始化(model=${model});首次 embed 时加载,失败则降级 fts-only`,
102
+ })
103
+ },
104
+ }
105
+ }
@@ -0,0 +1,87 @@
1
+ /*
2
+ * OpenAI 兼容嵌入提供者(Plan §3.4 轮 3 Task 3.1)
3
+ *
4
+ * 适用于任意 /v1/embeddings 兼容端点(本地 vLLM / 自建网关 / 官方 API)。
5
+ * 契约同 local-onnx:维度以首次响应为准(配置项 dimension 仅作期望值校验),
6
+ * 任何失败收敛为 ERR_EMBEDDING_DISABLED,不阻塞 Gate。
7
+ */
8
+ import { MemoryError } from "../domain/errors.js"
9
+ import type { ComponentHealth } from "../retrieval/types.js"
10
+ import type { EmbeddingProvider } from "./index.js"
11
+
12
+ export interface OpenAiCompatibleOptions {
13
+ baseUrl: string
14
+ model: string
15
+ apiKey?: string
16
+ /** 期望维度(可选):与首次响应不一致时报 ERR_DIMENSION_MISMATCH */
17
+ dimension?: number
18
+ timeoutMs?: number
19
+ /** 测试注入 */
20
+ fetchImpl?: typeof fetch
21
+ }
22
+
23
+ export function createOpenAiCompatibleEmbeddingProvider(
24
+ opts: OpenAiCompatibleOptions,
25
+ ): EmbeddingProvider {
26
+ const doFetch = opts.fetchImpl ?? fetch
27
+ const endpoint = `${opts.baseUrl.replace(/\/+$/, "")}/embeddings`
28
+ let dimension = opts.dimension ?? 0
29
+ let lastError: string | null = null
30
+
31
+ return {
32
+ id: "openai-compatible",
33
+ get dimension(): number {
34
+ return dimension
35
+ },
36
+ async embed(texts: string[]): Promise<number[][]> {
37
+ if (texts.length === 0) return []
38
+ const controller = new AbortController()
39
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 15_000)
40
+ let payload: { data?: { embedding?: number[] }[] }
41
+ try {
42
+ const res = await doFetch(endpoint, {
43
+ method: "POST",
44
+ headers: {
45
+ "content-type": "application/json",
46
+ ...(opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {}),
47
+ },
48
+ body: JSON.stringify({ model: opts.model, input: texts }),
49
+ signal: controller.signal,
50
+ })
51
+ if (!res.ok) {
52
+ throw new Error(`HTTP ${res.status}`)
53
+ }
54
+ payload = (await res.json()) as { data?: { embedding?: number[] }[] }
55
+ } catch (error) {
56
+ lastError = error instanceof Error ? error.message : String(error)
57
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", `兼容端点调用失败(${endpoint}):${lastError}`)
58
+ } finally {
59
+ clearTimeout(timer)
60
+ }
61
+
62
+ const vectors = (payload.data ?? []).map((d) => d.embedding ?? []).filter((v) => v.length > 0)
63
+ if (vectors.length === 0) {
64
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", `兼容端点未返回向量(model=${opts.model})`)
65
+ }
66
+ const dim = vectors[0].length
67
+ if (dimension !== 0 && dimension !== dim) {
68
+ throw new MemoryError("ERR_DIMENSION_MISMATCH", `期望维度 ${dimension},实收 ${dim}`)
69
+ }
70
+ dimension = dim
71
+ lastError = null
72
+ return vectors
73
+ },
74
+ health(): Promise<ComponentHealth> {
75
+ if (dimension > 0) {
76
+ return Promise.resolve({ component: "embedding", status: "ok", detail: `openai-compatible model=${opts.model} dim=${dimension}` })
77
+ }
78
+ return Promise.resolve({
79
+ component: "embedding",
80
+ status: lastError ? "unavailable" : "degraded",
81
+ detail: lastError
82
+ ? `兼容端点不可用:${lastError}`
83
+ : `兼容端点未调用(model=${opts.model},endpoint=${endpoint})`,
84
+ })
85
+ },
86
+ }
87
+ }
@@ -0,0 +1,226 @@
1
+ /*
2
+ * Consolidation 异步任务(Spec §10,Plan §9.2 Post-Handoff 行)
3
+ *
4
+ * 职责(全部 fail-open 隔离:单步失败不影响他步,错误入报告):
5
+ * 1. drainEvidenceQueue —— 采证队列落库
6
+ * 2. 重复检测 —— 同 repositoryRef+contentHash 多行 → 报告(不自动合并,人审)
7
+ * 3. 冲突队列 —— CANDIDATE/PENDING 与 ACTIVE 高相似 → 报告(不自动激活,Plan §3)
8
+ * 4. 指标候选 —— AddMetricSnapshot 连续异常 streak 命中 → 创建 CANDIDATE(幂等)
9
+ * 5. Handoff Digest —— HANDOFF Evidence → HANDOFF_DIGEST Candidate(幂等,绝不直接 ACTIVE)
10
+ * 6. 刷新 L1 快照 —— 供 session-start Hook 注入
11
+ */
12
+ import type {
13
+ AddMemoryEvidenceRow,
14
+ AddMemoryEvidenceLinkRow,
15
+ AddMemoryRow,
16
+ AddMetricSnapshotRow,
17
+ TableDelegate,
18
+ } from "../../db-types.js"
19
+ import { detectConflicts, type Conflict } from "../domain/conflicts.js"
20
+ import { contentHash } from "../domain/dedup.js"
21
+ import { detectAnomalyStreak, type MetricPoint } from "../domain/metric-candidate.js"
22
+ import { buildHandoffDigest } from "../domain/handoff-digest.js"
23
+ import { drainEvidenceQueue, type DrainResult } from "./evidence-collector.js"
24
+ import { refreshL1Snapshot, type SnapshotDeps, type SnapshotResult } from "./snapshot.js"
25
+
26
+ export interface ConsolidationDeps extends SnapshotDeps {
27
+ memoryDb: TableDelegate<AddMemoryRow>
28
+ evidenceDb: Pick<TableDelegate<AddMemoryEvidenceRow>, "upsert" | "findMany">
29
+ linkDb: Pick<TableDelegate<AddMemoryEvidenceLinkRow>, "findFirst" | "create">
30
+ metricDb: TableDelegate<AddMetricSnapshotRow>
31
+ }
32
+
33
+ export interface ConsolidationReport {
34
+ drain: DrainResult
35
+ duplicates: { contentHash: string; ids: string[] }[]
36
+ conflictQueue: { candidateId: string; topic: string; conflicts: Conflict[] }[]
37
+ metricCandidatesCreated: string[]
38
+ handoffDigestsCreated: string[]
39
+ snapshot: SnapshotResult | null
40
+ errors: string[]
41
+ }
42
+
43
+ export async function runConsolidation(deps: ConsolidationDeps): Promise<ConsolidationReport> {
44
+ const report: ConsolidationReport = {
45
+ drain: { processed: 0, skipped: 0, errors: [], newOffset: 0 },
46
+ duplicates: [],
47
+ conflictQueue: [],
48
+ metricCandidatesCreated: [],
49
+ handoffDigestsCreated: [],
50
+ snapshot: null,
51
+ errors: [],
52
+ }
53
+
54
+ // 1. 采证队列落库
55
+ try {
56
+ report.drain = await drainEvidenceQueue(deps)
57
+ } catch (e) {
58
+ report.errors.push(`drain: ${e instanceof Error ? e.message : String(e)}`)
59
+ }
60
+
61
+ // 2/3. 重复检测 + 冲突队列(共享一次全量读取)
62
+ try {
63
+ const rows = await deps.memoryDb.findMany({
64
+ where: { repositoryRef: deps.repositoryRef, status: { notIn: ["REJECTED", "ARCHIVED"] } },
65
+ take: 500,
66
+ })
67
+ const byHash = new Map<string, string[]>()
68
+ for (const r of rows) {
69
+ const arr = byHash.get(r.contentHash) ?? []
70
+ arr.push(r.id)
71
+ byHash.set(r.contentHash, arr)
72
+ }
73
+ for (const [hash, ids] of byHash) {
74
+ if (ids.length > 1) report.duplicates.push({ contentHash: hash, ids })
75
+ }
76
+ const actives = rows.filter((r) => r.status === "ACTIVE")
77
+ for (const c of rows.filter((r) => r.status === "CANDIDATE" || r.status === "PENDING")) {
78
+ const conflicts = detectConflicts(c, actives)
79
+ if (conflicts.length > 0) report.conflictQueue.push({ candidateId: c.id, topic: c.topic, conflicts })
80
+ }
81
+ } catch (e) {
82
+ report.errors.push(`dedup/conflicts: ${e instanceof Error ? e.message : String(e)}`)
83
+ }
84
+
85
+ // 4. 指标候选(Plan §10:连续异常 streak 才生成,内容必须是可验证结论)
86
+ try {
87
+ const metrics = await deps.metricDb.findMany({
88
+ where: { repositoryRef: deps.repositoryRef },
89
+ orderBy: { measuredAt: "asc" },
90
+ take: 500,
91
+ })
92
+ const byType = new Map<string, MetricPoint[]>()
93
+ for (const m of metrics) {
94
+ const arr = byType.get(m.metricType) ?? []
95
+ arr.push({
96
+ repositoryRef: m.repositoryRef,
97
+ metricType: m.metricType,
98
+ value: m.value,
99
+ baseline: m.baseline,
100
+ planKeyword: m.planKeyword,
101
+ measuredAt: m.measuredAt,
102
+ sourceRef: m.sourceRef,
103
+ })
104
+ byType.set(m.metricType, arr)
105
+ }
106
+ for (const points of byType.values()) {
107
+ const proposal = detectAnomalyStreak(points)
108
+ if (!proposal) continue
109
+ const hash = contentHash(proposal.content)
110
+ const existing = await deps.memoryDb.findFirst({
111
+ where: {
112
+ repositoryRef: deps.repositoryRef,
113
+ contentHash: hash,
114
+ scopeType: "REPOSITORY",
115
+ scopeValue: deps.repositoryRef,
116
+ status: { notIn: ["REJECTED", "ARCHIVED"] },
117
+ },
118
+ })
119
+ if (existing) continue // 幂等:已提过同结论
120
+ const created = await deps.memoryDb.create({
121
+ data: {
122
+ kind: "PATTERN",
123
+ topic: proposal.topic,
124
+ content: proposal.content,
125
+ scopeType: "REPOSITORY",
126
+ scopeValue: deps.repositoryRef,
127
+ repositoryRef: deps.repositoryRef,
128
+ contentHash: hash,
129
+ createdBy: "memory-jobs:consolidation",
130
+ },
131
+ })
132
+ for (const ref of proposal.evidenceSourceRefs) {
133
+ const ev = await deps.evidenceDb.upsert({
134
+ where: {
135
+ repositoryRef_sourceType_sourceRef_contentHash: {
136
+ repositoryRef: deps.repositoryRef, sourceType: "DPS_GATE", sourceRef: ref, contentHash: contentHash(ref),
137
+ },
138
+ },
139
+ create: {
140
+ repositoryRef: deps.repositoryRef, sourceType: "DPS_GATE", sourceRef: ref,
141
+ excerpt: ref.slice(0, 500), contentHash: contentHash(ref),
142
+ },
143
+ update: {},
144
+ })
145
+ const linked = await deps.linkDb.findFirst({ where: { memoryId: created.id, evidenceId: ev.id } })
146
+ if (!linked) await deps.linkDb.create({ data: { memoryId: created.id, evidenceId: ev.id } })
147
+ }
148
+ report.metricCandidatesCreated.push(created.id)
149
+ }
150
+ } catch (e) {
151
+ report.errors.push(`metric-candidates: ${e instanceof Error ? e.message : String(e)}`)
152
+ }
153
+
154
+ // 5. Handoff Digest(Plan §3.1 轮 2):HANDOFF Evidence → HANDOFF_DIGEST Candidate
155
+ // 幂等去重靠 dedupKey 派生的 contentHash;绝不直接置 ACTIVE(只产 CANDIDATE)
156
+ try {
157
+ const handoffEvidence = await deps.evidenceDb.findMany({
158
+ where: { repositoryRef: deps.repositoryRef, sourceType: "HANDOFF" },
159
+ orderBy: { occurredAt: "asc" },
160
+ take: 200,
161
+ })
162
+ for (const ev of handoffEvidence) {
163
+ const proposal = buildHandoffDigest(
164
+ { handoffRef: ev.sourceRef, excerpt: ev.excerpt, planKeyword: ev.planKeyword },
165
+ { repositoryRef: deps.repositoryRef },
166
+ )
167
+ const existing = await deps.memoryDb.findFirst({
168
+ where: {
169
+ repositoryRef: deps.repositoryRef,
170
+ contentHash: proposal.contentHash,
171
+ scopeType: proposal.scopeType,
172
+ scopeValue: proposal.scopeValue,
173
+ status: { notIn: ["REJECTED", "ARCHIVED"] },
174
+ },
175
+ })
176
+ if (existing) {
177
+ // 幂等:同一交接 evidence 的摘要已入队过 → 仅补齐证据关联
178
+ const linkedAlready = await deps.linkDb.findFirst({
179
+ where: { memoryId: existing.id, evidenceId: ev.id },
180
+ })
181
+ if (!linkedAlready) {
182
+ await deps.linkDb.create({
183
+ data: { memoryId: existing.id, evidenceId: ev.id },
184
+ })
185
+ }
186
+ continue
187
+ }
188
+ const created = await deps.memoryDb.create({
189
+ data: {
190
+ kind: proposal.kind,
191
+ status: "CANDIDATE", // 绝不直接 ACTIVE
192
+ topic: proposal.topic,
193
+ content: proposal.content,
194
+ scopeType: proposal.scopeType,
195
+ scopeValue: proposal.scopeValue,
196
+ repositoryRef: deps.repositoryRef,
197
+ contentHash: proposal.contentHash,
198
+ importance: 0.6,
199
+ confidence: 0.5,
200
+ createdBy: "memory-jobs:consolidation",
201
+ metadata: { ...proposal.metadata, dedupKey: proposal.dedupKey, evidenceId: ev.id },
202
+ },
203
+ })
204
+ const linked = await deps.linkDb.findFirst({
205
+ where: { memoryId: created.id, evidenceId: ev.id },
206
+ })
207
+ if (!linked) {
208
+ await deps.linkDb.create({
209
+ data: { memoryId: created.id, evidenceId: ev.id },
210
+ })
211
+ }
212
+ report.handoffDigestsCreated.push(created.id)
213
+ }
214
+ } catch (e) {
215
+ report.errors.push(`handoff-digest: ${e instanceof Error ? e.message : String(e)}`)
216
+ }
217
+
218
+ // 6. 刷新 L1 快照
219
+ try {
220
+ report.snapshot = await refreshL1Snapshot(deps)
221
+ } catch (e) {
222
+ report.errors.push(`snapshot: ${e instanceof Error ? e.message : String(e)}`)
223
+ }
224
+
225
+ return report
226
+ }