add-coder 0.3.34 → 0.3.37

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 (98) hide show
  1. package/README.en.md +101 -42
  2. package/README.md +63 -18
  3. package/dist/index.js +24 -6
  4. package/package.json +2 -2
  5. package/templates/.add-coder-src-hash.json +94 -39
  6. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
  7. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
  8. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
  9. package/templates/adapters/claude/hooks/session-start.mjs +65 -1
  10. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
  11. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
  12. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
  13. package/templates/adapters/codex/hooks/session-start.mjs +65 -1
  14. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
  15. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
  16. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
  17. package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
  18. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
  19. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
  20. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
  21. package/templates/adapters/trae/hooks/session-start.mjs +65 -1
  22. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
  23. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
  24. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
  25. package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
  26. package/templates/core/governance/doc-format-guard.ts +29 -112
  27. package/templates/core/governance/post-tool-router.ts +33 -1
  28. package/templates/core/governance/prompt-router.ts +47 -0
  29. package/templates/core/governance/session-start-guard.ts +48 -1
  30. package/templates/core/prisma/add.prisma +203 -0
  31. package/templates/core/scripts/db-ensure.sh +92 -2
  32. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  33. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  34. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  35. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  36. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  37. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  38. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  39. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  40. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  41. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  42. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  43. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  44. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  45. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  46. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  47. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  48. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  49. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  50. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  51. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  52. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  53. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  54. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  55. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  56. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  57. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  58. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  59. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  60. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  61. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  62. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  63. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  64. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  65. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  66. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  67. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  68. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  69. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  70. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  71. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +37 -0
  72. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  73. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  74. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  75. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  76. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  77. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  78. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  79. package/templates/core/scripts/mcp-server.ts +36 -0
  80. package/templates/core/templates/checklist-template.md +13 -0
  81. package/templates/core/templates/review-implementation-template.md +24 -0
  82. package/templates/core/templates/review-template.md +16 -0
  83. package/templates/core/validation/index.ts +136 -0
  84. package/templates/core/validation/policy.ts +91 -0
  85. package/templates/core/validation/registry.ts +61 -0
  86. package/templates/core/validation/schema-validator.ts +277 -0
  87. package/templates/core/validation/validators/add-route.ts +32 -0
  88. package/templates/core/validation/validators/checklist.ts +48 -0
  89. package/templates/core/validation/validators/handoff.ts +46 -0
  90. package/templates/core/validation/validators/hitl.ts +22 -0
  91. package/templates/core/validation/validators/index.ts +52 -0
  92. package/templates/core/validation/validators/plan.ts +20 -0
  93. package/templates/core/validation/validators/report.ts +16 -0
  94. package/templates/core/validation/validators/review.ts +30 -0
  95. package/templates/core/validation/validators/spec.ts +25 -0
  96. package/templates/core/validation/validators/tasks.ts +40 -0
  97. package/templates/core/validation/validators/types.ts +32 -0
  98. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Memory 子系统稳定错误码(Plan §12.3:MCP 错误采用稳定错误码,不能只返回自由文本)
3
+ * 契约:code 字符串冻结,变更需走 Review;message 可携带上下文细节。
4
+ */
5
+ export const MEMORY_ERROR = {
6
+ ERR_REPOSITORY_MISMATCH: "调用方 repositoryRef 与运行时上下文不一致,越权访问被拒绝",
7
+ ERR_ORG_SCOPE_DISABLED: "ORGANIZATION scope 首版禁用(Plan §17-7 定案)",
8
+ ERR_ILLEGAL_TRANSITION: "非法状态迁移",
9
+ ERR_EVIDENCE_REQUIRED: "进入 ACTIVE 必须至少关联一条 Evidence",
10
+ ERR_APPROVAL_REQUIRED: "进入 ACTIVE 必须提供 approvedBy/approvedAt",
11
+ ERR_SUPERSESSION_INVALID: "supersede 需要有效的 supersededById 且 repository/scope 兼容",
12
+ ERR_INVARIANT: "字段不变量违反(如 importance/confidence 越出 [0,1])",
13
+ ERR_FTS_UNAVAILABLE: "FTS 能力不可用且无法降级",
14
+ ERR_SECRET_DETECTED: "内容命中密钥/凭证模式,拒写",
15
+ ERR_NOT_FOUND: "记录不存在",
16
+ ERR_EMBEDDING_DISABLED: "EmbeddingProvider=none,向量能力未启用",
17
+ ERR_DIMENSION_MISMATCH: "嵌入维度与真源不一致(模型元数据 vs 声明维度),拒绝写入脏向量",
18
+ } as const
19
+
20
+ export type MemoryErrorCode = keyof typeof MEMORY_ERROR
21
+
22
+ export class MemoryError extends Error {
23
+ readonly code: MemoryErrorCode
24
+ constructor(code: MemoryErrorCode, detail?: string) {
25
+ super(detail ? `${code}: ${MEMORY_ERROR[code]}(${detail})` : `${code}: ${MEMORY_ERROR[code]}`)
26
+ this.name = "MemoryError"
27
+ this.code = code
28
+ }
29
+ }
30
+
31
+ export function isMemoryError(e: unknown): e is MemoryError {
32
+ return e instanceof MemoryError
33
+ }
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Handoff Digest 候选构造(Plan §3.1 轮 2 / Spec §3 §HandoffDigest)
3
+ *
4
+ * 纯函数:输入 HANDOFF Evidence 行 → 输出可落库的 HANDOFF_DIGEST 候选提案。
5
+ * 三条不变量:
6
+ * 1. **绝不直接 ACTIVE**——提案只带 status=CANDIDATE 语义(由调用方落库时体现);
7
+ * 2. **幂等**——dedupKey 由「来源引用 + 摘要内容」派生,同一 evidence 重放得到同一键;
8
+ * 3. **内容是可验证结论**,不是裸数值或空串:excerpt 为空时显式标注「摘要待人工补充」。
9
+ */
10
+ import { contentHash, normalizeContent } from "./dedup.js"
11
+
12
+ export const HANDOFF_DIGEST_KIND = "HANDOFF_DIGEST" as const
13
+
14
+ /** 摘要正文最大字符数(超出截断并标注) */
15
+ export const HANDOFF_EXCERPT_MAX = 300
16
+
17
+ export interface HandoffDigestInput {
18
+ /** handoff 文档路径或引用(evidence.sourceRef) */
19
+ handoffRef: string
20
+ /** evidence 摘要原文 */
21
+ excerpt: string
22
+ /** 关联 Plan 关键词(evidence.planKeyword,可为空) */
23
+ planKeyword?: string | null
24
+ }
25
+
26
+ export interface HandoffDigestProposal {
27
+ kind: typeof HANDOFF_DIGEST_KIND
28
+ topic: string
29
+ content: string
30
+ scopeType: "PLAN" | "REPOSITORY"
31
+ scopeValue: string
32
+ contentHash: string
33
+ dedupKey: string
34
+ metadata: {
35
+ handoffRef: string
36
+ planKeyword: string | null
37
+ excerptTruncated: boolean
38
+ }
39
+ }
40
+
41
+ /** 从 handoff 引用提取主题名(去目录、去扩展名) */
42
+ export function handoffTopicOf(handoffRef: string): string {
43
+ const base = handoffRef.split(/[\\/]/).pop() ?? handoffRef
44
+ const stem = base.replace(/\.(md|markdown|txt)$/i, "")
45
+ return stem.length > 0 ? stem : handoffRef
46
+ }
47
+
48
+ /**
49
+ * 构造 HANDOFF_DIGEST 候选提案。
50
+ *
51
+ * scope 选择:有 planKeyword → PLAN scope(历史语义,默认召回需显式放行);
52
+ * 无 planKeyword → REPOSITORY scope(可被常规召回命中)。
53
+ */
54
+ export function buildHandoffDigest(
55
+ input: HandoffDigestInput,
56
+ opts: { repositoryRef: string; maxExcerpt?: number },
57
+ ): HandoffDigestProposal {
58
+ const maxExcerpt = opts.maxExcerpt ?? HANDOFF_EXCERPT_MAX
59
+ const topic = handoffTopicOf(input.handoffRef)
60
+ const normalizedExcerpt = normalizeContent(input.excerpt ?? "")
61
+ const planKeyword = input.planKeyword && input.planKeyword.trim().length > 0
62
+ ? input.planKeyword.trim()
63
+ : null
64
+
65
+ const truncated = normalizedExcerpt.length > maxExcerpt
66
+ const body = truncated
67
+ ? `${normalizedExcerpt.slice(0, maxExcerpt)}…(已截断)`
68
+ : normalizedExcerpt
69
+
70
+ const content =
71
+ body.length > 0
72
+ ? `交接摘要 ${topic}:${body}(来源 ${input.handoffRef})`
73
+ : `交接文档 ${input.handoffRef} 已归档,摘要待人工补充(自动摘要为空)。`
74
+
75
+ const scopeType: "PLAN" | "REPOSITORY" = planKeyword ? "PLAN" : "REPOSITORY"
76
+ const scopeValue = planKeyword ?? opts.repositoryRef
77
+
78
+ return {
79
+ kind: HANDOFF_DIGEST_KIND,
80
+ topic,
81
+ content,
82
+ scopeType,
83
+ scopeValue,
84
+ contentHash: contentHash(content),
85
+ dedupKey: contentHash(`${HANDOFF_DIGEST_KIND}|${input.handoffRef}|${normalizedExcerpt}`),
86
+ metadata: {
87
+ handoffRef: input.handoffRef,
88
+ planKeyword,
89
+ excerptTruncated: truncated,
90
+ },
91
+ }
92
+ }
@@ -0,0 +1,79 @@
1
+ /*
2
+ * 指标到知识的转换(Plan §10)
3
+ *
4
+ * entropy/code quality/Gate score 只写 AddMetricSnapshot;单个数值不能自动成为长期文本记忆。
5
+ * 候选生成需满足:同 scope 异常连续达阈值 / 多独立证据指向同根因 / 显著偏离基线且有可解释事件。
6
+ */
7
+
8
+ export interface MetricPoint {
9
+ repositoryRef: string
10
+ metricType: string
11
+ value: number
12
+ baseline?: number | null
13
+ planKeyword?: string | null
14
+ measuredAt: Date
15
+ sourceRef: string
16
+ }
17
+
18
+ export interface MetricCandidateRule {
19
+ /** 同 scope 连续异常阈值(默认 3 次) */
20
+ streakThreshold: number
21
+ /** 显著偏离基线的相对幅度(默认 20%) */
22
+ deviationRatio: number
23
+ /** 判定"异常"的方向:高于基线为坏(higher-worse)或低于基线为坏(lower-worse) */
24
+ direction: "higher-worse" | "lower-worse"
25
+ }
26
+
27
+ export const DEFAULT_METRIC_RULE: MetricCandidateRule = {
28
+ streakThreshold: 3,
29
+ deviationRatio: 0.2,
30
+ direction: "lower-worse",
31
+ }
32
+
33
+ export interface MetricCandidateProposal {
34
+ topic: string
35
+ content: string
36
+ evidenceSourceRefs: string[]
37
+ rule: string
38
+ }
39
+
40
+ function isAnomaly(p: MetricPoint, rule: MetricCandidateRule): boolean {
41
+ if (p.baseline == null) return false
42
+ const dev = Math.abs(p.value - p.baseline) / (Math.abs(p.baseline) || 1)
43
+ if (dev < rule.deviationRatio) return false
44
+ return rule.direction === "lower-worse" ? p.value < p.baseline : p.value > p.baseline
45
+ }
46
+
47
+ /**
48
+ * 检测「同 scope 连续异常」:按时间排序后尾部连续异常数 ≥ streakThreshold 时生成候选建议。
49
+ * 纯函数;是否真正创建 Candidate 由调用方(consolidation job / 人工)决定。
50
+ */
51
+ export function detectAnomalyStreak(
52
+ points: MetricPoint[],
53
+ rule: MetricCandidateRule = DEFAULT_METRIC_RULE,
54
+ ): MetricCandidateProposal | null {
55
+ if (points.length < rule.streakThreshold) return null
56
+ const sorted = [...points].sort((a, b) => a.measuredAt.getTime() - b.measuredAt.getTime())
57
+ let streak = 0
58
+ for (let i = sorted.length - 1; i >= 0; i--) {
59
+ if (isAnomaly(sorted[i], rule)) streak++
60
+ else break
61
+ }
62
+ if (streak < rule.streakThreshold) return null
63
+ const last = sorted[sorted.length - 1]
64
+ const streakPoints = sorted.slice(-streak)
65
+ return {
66
+ topic: `${last.metricType} 连续 ${streak} 次显著偏离基线`,
67
+ content:
68
+ `指标 ${last.metricType} 在最近 ${streak} 次测量中连续偏离基线超过 ${rule.deviationRatio * 100}%` +
69
+ `(baseline=${streakPoints[0].baseline}, 最新值=${last.value})。` +
70
+ `建议人工审核是否形成可验证结论后再转为长期记忆。`,
71
+ evidenceSourceRefs: streakPoints.map((p) => p.sourceRef),
72
+ rule: `anomaly-streak>=${rule.streakThreshold} deviation>=${rule.deviationRatio} ${rule.direction}`,
73
+ }
74
+ }
75
+
76
+ /** Gate 执行后生成 sourceRef(Review P1 #2 回流:{gate}:{planKeyword}:{runId}) */
77
+ export function metricSourceRef(gate: string, planKeyword: string, runId: string): string {
78
+ return `${gate}:${planKeyword}:${runId}`
79
+ }
@@ -0,0 +1,94 @@
1
+ /*
2
+ * Memory Scope 规则(Plan §7.2)
3
+ *
4
+ * 优先级:symbol > path > module > branch > repository > organization
5
+ * ORGANIZATION 首版禁用(写入拒绝,§17-7);PLAN/SPEC 只在对应流程或显式历史查询中参与。
6
+ */
7
+ import { MemoryError } from "./errors.js"
8
+
9
+ export type MemoryScopeType =
10
+ | "ORGANIZATION" | "REPOSITORY" | "BRANCH" | "MODULE"
11
+ | "PATH" | "SYMBOL" | "PLAN" | "SPEC"
12
+
13
+ export interface Scope {
14
+ type: MemoryScopeType
15
+ value: string
16
+ }
17
+
18
+ /** 召回时的上下文坐标(由调用方从运行时环境组装) */
19
+ export interface ScopeContext {
20
+ repository: string
21
+ branch?: string
22
+ module?: string
23
+ paths?: string[]
24
+ symbols?: string[]
25
+ planKeyword?: string
26
+ specRef?: string
27
+ /** 显式历史查询时放行 PLAN/SPEC scope(默认 false) */
28
+ includePlanScope?: boolean
29
+ }
30
+
31
+ const RANK: Record<MemoryScopeType, number> = {
32
+ SYMBOL: 7,
33
+ PATH: 6,
34
+ MODULE: 5,
35
+ BRANCH: 4,
36
+ REPOSITORY: 3,
37
+ PLAN: 2,
38
+ SPEC: 2,
39
+ ORGANIZATION: 1,
40
+ }
41
+
42
+ export function scopeRank(t: MemoryScopeType): number {
43
+ return RANK[t]
44
+ }
45
+
46
+ /** 写入守卫:ORGANIZATION 首版禁用 */
47
+ export function assertScopeWritable(scope: Scope): void {
48
+ if (scope.type === "ORGANIZATION") {
49
+ throw new MemoryError("ERR_ORG_SCOPE_DISABLED")
50
+ }
51
+ }
52
+
53
+ /** 判定一条 Memory 的 scope 在当前上下文是否有效(召回过滤用) */
54
+ export function scopeApplies(memScope: Scope, ctx: ScopeContext): boolean {
55
+ switch (memScope.type) {
56
+ case "ORGANIZATION":
57
+ return false // 首版禁用
58
+ case "REPOSITORY":
59
+ return memScope.value === ctx.repository
60
+ case "BRANCH":
61
+ return !!ctx.branch && ctx.branch === memScope.value
62
+ case "MODULE":
63
+ // module 语义:当前任一路径以该模块目录为前缀
64
+ return !!ctx.module && ctx.module === memScope.value ||
65
+ (ctx.paths ?? []).some((p) => p === memScope.value || p.startsWith(memScope.value + "/"))
66
+ case "PATH":
67
+ return (ctx.paths ?? []).some(
68
+ (p) => p === memScope.value || p.startsWith(memScope.value.replace(/\/?$/, "/")),
69
+ )
70
+ case "SYMBOL":
71
+ return (ctx.symbols ?? []).includes(memScope.value)
72
+ case "PLAN":
73
+ return !!ctx.includePlanScope && !!ctx.planKeyword && ctx.planKeyword === memScope.value
74
+ case "SPEC":
75
+ return !!ctx.includePlanScope && !!ctx.specRef && ctx.specRef === memScope.value
76
+ }
77
+ }
78
+
79
+ /**
80
+ * supersede 兼容性(Plan §4.4):新旧 Memory 的 repository/scope 必须兼容。
81
+ * 兼容 = 同类型同值;或 PATH/MODULE 类型间存在前缀覆盖;或一方为 REPOSITORY(全库覆盖)。
82
+ */
83
+ export function scopesCompatible(a: Scope, b: Scope): boolean {
84
+ if (a.type === b.type && a.value === b.value) return true
85
+ if (a.type === "REPOSITORY" || b.type === "REPOSITORY") {
86
+ return a.type === "REPOSITORY" && b.type === "REPOSITORY" ? a.value === b.value : true
87
+ }
88
+ const pathLike = (t: MemoryScopeType) => t === "PATH" || t === "MODULE"
89
+ if (pathLike(a.type) && pathLike(b.type)) {
90
+ return a.value.startsWith(b.value.replace(/\/?$/, "/")) ||
91
+ b.value.startsWith(a.value.replace(/\/?$/, "/"))
92
+ }
93
+ return false
94
+ }
@@ -0,0 +1,50 @@
1
+ /*
2
+ * 密钥/凭证扫描(Plan §11:Evidence 摘录在写入前执行秘密与敏感信息检测)
3
+ * 命中即拒写(ERR_SECRET_DETECTED)。宁可误报,不可漏报。
4
+ */
5
+ import { MemoryError } from "./errors.js"
6
+
7
+ export interface SecretHit {
8
+ pattern: string
9
+ index: number
10
+ preview: string
11
+ }
12
+
13
+ interface SecretPattern {
14
+ name: string
15
+ re: RegExp
16
+ }
17
+
18
+ const PATTERNS: SecretPattern[] = [
19
+ { name: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
20
+ { name: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/ },
21
+ { name: "github-token", re: /\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/ },
22
+ { name: "openai-style-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
23
+ { name: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
24
+ { name: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/i },
25
+ { name: "conn-string-password", re: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:[^@\s]+@/i },
26
+ { name: "generic-secret-assign", re: /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token)\b\s*[:=]\s*["'][^"'\s]{8,}["']/i },
27
+ { name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
28
+ ]
29
+
30
+ export function scanSecrets(text: string): SecretHit[] {
31
+ const hits: SecretHit[] = []
32
+ for (const p of PATTERNS) {
33
+ const m = p.re.exec(text)
34
+ if (m) {
35
+ hits.push({
36
+ pattern: p.name,
37
+ index: m.index,
38
+ preview: text.slice(Math.max(0, m.index - 10), m.index + 20).replace(/\s+/g, " ").slice(0, 40),
39
+ })
40
+ }
41
+ }
42
+ return hits
43
+ }
44
+
45
+ export function assertNoSecrets(text: string): void {
46
+ const hits = scanSecrets(text)
47
+ if (hits.length > 0) {
48
+ throw new MemoryError("ERR_SECRET_DETECTED", hits.map((h) => h.pattern).join(","))
49
+ }
50
+ }
@@ -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
+ }