add-coder 0.3.26 → 0.3.27

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 (132) hide show
  1. package/README.md +36 -10
  2. package/dist/index.js +481 -209
  3. package/package.json +14 -4
  4. package/templates/.add-coder-src-hash.json +129 -106
  5. package/templates/adapters/claude/hooks/doc-format-guard.sh +112 -31
  6. package/templates/adapters/claude/hooks/lib/common.sh +46 -87
  7. package/templates/adapters/claude/hooks/lib/context-inject.sh +2 -2
  8. package/templates/adapters/claude/hooks/lib/notify.sh +6 -4
  9. package/templates/adapters/claude/hooks/lib/preload-templates.sh +27 -2
  10. package/templates/adapters/claude/hooks/lib/state-detect.sh +8 -112
  11. package/templates/adapters/claude/hooks/lib/vocabulary.sh +13 -7
  12. package/templates/adapters/claude/hooks/prompt-submit.sh +1 -1
  13. package/templates/adapters/claude/hooks/stop-check.sh +9 -0
  14. package/templates/adapters/codex/config.toml.example +12 -0
  15. package/templates/adapters/codex/hooks/doc-format-guard.sh +121 -39
  16. package/templates/adapters/codex/hooks/lib/common.sh +47 -99
  17. package/templates/adapters/codex/hooks/lib/context-inject.sh +4 -4
  18. package/templates/adapters/codex/hooks/lib/notify.sh +6 -4
  19. package/templates/adapters/codex/hooks/lib/preload-templates.sh +27 -6
  20. package/templates/adapters/codex/hooks/lib/state-detect.sh +5 -114
  21. package/templates/adapters/codex/hooks/lib/vocabulary.sh +3 -8
  22. package/templates/adapters/codex/hooks/notification.sh +3 -18
  23. package/templates/adapters/codex/hooks/post-tool-use.sh +24 -119
  24. package/templates/adapters/codex/hooks/pre-tool-use.sh +97 -184
  25. package/templates/adapters/codex/hooks/prompt-submit.sh +8 -3
  26. package/templates/adapters/codex/hooks/session-start.sh +19 -9
  27. package/templates/adapters/codex/hooks/stop-check.sh +44 -18
  28. package/templates/adapters/codex/hooks.json +67 -1
  29. package/templates/adapters/qoder/hooks/doc-format-guard.sh +119 -38
  30. package/templates/adapters/qoder/hooks/lib/common.sh +49 -87
  31. package/templates/adapters/qoder/hooks/lib/context-inject.sh +2 -2
  32. package/templates/adapters/qoder/hooks/lib/notify.sh +6 -4
  33. package/templates/adapters/qoder/hooks/lib/preload-templates.sh +27 -2
  34. package/templates/adapters/qoder/hooks/lib/state-detect.sh +8 -112
  35. package/templates/adapters/qoder/hooks/lib/vocabulary.sh +13 -7
  36. package/templates/adapters/qoder/hooks/prompt-submit.sh +1 -1
  37. package/templates/adapters/qoder/hooks/session-start.sh +1 -1
  38. package/templates/adapters/qoder/hooks/stop-check.sh +11 -0
  39. package/templates/adapters/trae/hooks/doc-format-guard.sh +120 -38
  40. package/templates/adapters/trae/hooks/lib/common.sh +46 -87
  41. package/templates/adapters/trae/hooks/lib/context-inject.sh +2 -2
  42. package/templates/adapters/trae/hooks/lib/notify.sh +5 -3
  43. package/templates/adapters/trae/hooks/lib/preload-templates.sh +27 -2
  44. package/templates/adapters/trae/hooks/lib/state-detect.sh +8 -115
  45. package/templates/adapters/trae/hooks/lib/vocabulary.sh +13 -7
  46. package/templates/adapters/trae/hooks/pre-tool-use.sh +14 -8
  47. package/templates/adapters/trae/hooks/stop-check.sh +9 -0
  48. package/templates/adapters/vscode/hooks/doc-format-guard.sh +112 -31
  49. package/templates/adapters/vscode/hooks/lib/common.sh +46 -87
  50. package/templates/adapters/vscode/hooks/lib/context-inject.sh +2 -2
  51. package/templates/adapters/vscode/hooks/lib/notify.sh +6 -4
  52. package/templates/adapters/vscode/hooks/lib/preload-templates.sh +27 -2
  53. package/templates/adapters/vscode/hooks/lib/state-detect.sh +8 -112
  54. package/templates/adapters/vscode/hooks/lib/vocabulary.sh +13 -7
  55. package/templates/adapters/vscode/hooks/notification.sh +3 -5
  56. package/templates/adapters/vscode/hooks/prompt-submit.sh +1 -1
  57. package/templates/adapters/vscode/hooks/stop-check.sh +9 -0
  58. package/templates/core/docs/ADD-governance-claude-code.md +1 -1
  59. package/templates/core/docs/ADD-governance-codex.md +1 -1
  60. package/templates/core/docs/ADD-governance-qoder-cn.md +1 -1
  61. package/templates/core/docs/ADD-governance-trae.md +1 -1
  62. package/templates/core/docs/ADD-governance-vscode-copilot.md +1 -1
  63. package/templates/core/hooks/doc-format-guard.sh +120 -38
  64. package/templates/core/hooks/lib/common.sh +46 -87
  65. package/templates/core/hooks/lib/context-inject.sh +2 -2
  66. package/templates/core/hooks/lib/notify.sh +5 -3
  67. package/templates/core/hooks/lib/preload-templates.sh +27 -2
  68. package/templates/core/hooks/lib/state-detect.sh +8 -115
  69. package/templates/core/hooks/lib/vocabulary.sh +13 -7
  70. package/templates/core/hooks/pre-tool-use.sh +14 -8
  71. package/templates/core/hooks/stop-check.sh +9 -0
  72. package/templates/core/prisma/add.prisma +140 -82
  73. package/templates/core/rules/project_rules.md +27 -24
  74. package/templates/core/scripts/db-ensure.sh +8 -0
  75. package/templates/core/scripts/gen-plan-index.sh +4 -15
  76. package/templates/core/scripts/mcp-server/notifications/hitl.ts +9 -1
  77. package/templates/core/scripts/mcp-server/notifications/hook.ts +25 -9
  78. package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +34 -0
  79. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +14 -2
  80. package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
  81. package/templates/core/scripts/mcp-server/sampling/review.ts +25 -10
  82. package/templates/core/scripts/mcp-server/shared/db-types.ts +33 -8
  83. package/templates/core/scripts/mcp-server/shared/dps-scoring.strategy.ts +1 -1
  84. package/templates/core/scripts/mcp-server/shared/env.ts +28 -8
  85. package/templates/core/scripts/mcp-server/shared/fs.ts +3 -0
  86. package/templates/core/scripts/mcp-server/shared/hitl-interaction.strategy.ts +2 -3
  87. package/templates/core/scripts/mcp-server/shared/hitl-lifecycle-mutation.ts +129 -0
  88. package/templates/core/scripts/mcp-server/shared/hitl-proposal-mutation.ts +98 -0
  89. package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +3 -0
  90. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-events.ts +62 -0
  91. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-mutation.ts +78 -0
  92. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-subscriber.ts +145 -0
  93. package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +153 -0
  94. package/templates/core/scripts/mcp-server/shared/plan-round-events.ts +56 -0
  95. package/templates/core/scripts/mcp-server/shared/plan-round-mutation.ts +143 -0
  96. package/templates/core/scripts/mcp-server/shared/plan-round-store.ts +47 -0
  97. package/templates/core/scripts/mcp-server/shared/plan-round-subscriber.ts +99 -0
  98. package/templates/core/scripts/mcp-server/shared/plan-status-store.ts +37 -0
  99. package/templates/core/scripts/mcp-server/shared/plan-tracking-mutation.ts +71 -0
  100. package/templates/core/scripts/mcp-server/shared/prisma.ts +12 -6
  101. package/templates/core/scripts/mcp-server/shared/project-root-strategy.ts +1 -2
  102. package/templates/core/scripts/mcp-server/shared/quant-recheck.strategy.ts +16 -0
  103. package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
  104. package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
  105. package/templates/core/scripts/mcp-server/shared/runtime-context.ts +77 -0
  106. package/templates/core/scripts/mcp-server/shared/schema-topology.ts +159 -0
  107. package/templates/core/scripts/mcp-server/tools/audit.ts +49 -7
  108. package/templates/core/scripts/mcp-server/tools/context.ts +117 -121
  109. package/templates/core/scripts/mcp-server/tools/contract.ts +34 -9
  110. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +50 -3
  111. package/templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts +218 -0
  112. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +22 -7
  113. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +75 -4
  114. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +19 -6
  115. package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -0
  116. package/templates/core/scripts/mcp-server/tools/hitl.ts +160 -75
  117. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +9 -2
  118. package/templates/core/scripts/mcp-server/tools/index.ts +69 -3
  119. package/templates/core/scripts/mcp-server/tools/plan.ts +140 -15
  120. package/templates/core/scripts/mcp-server/tools/review.ts +44 -15
  121. package/templates/core/scripts/mcp-server.ts +60 -1
  122. package/templates/core/scripts/plan-status-bridge.ts +26 -0
  123. package/templates/core/skills/add-paradigm/SKILL.md +2 -2
  124. package/templates/core/templates/add-route-template.schema.json +24 -56
  125. package/templates/core/templates/collab-contract-template.md +1 -1
  126. package/templates/core/templates/handoff-multi-round-template.schema.json +48 -0
  127. package/templates/core/templates/handoff-single-round-template.schema.json +92 -0
  128. package/templates/core/templates/handoff-single-round.schema.json +1 -1
  129. package/templates/core/templates/hitl-approval-widget.html +222 -37
  130. package/templates/core/templates/spec-template.schema.json +9 -46
  131. package/templates/core/templates/standard-plan-template.schema.json +1 -1
  132. package/templates/core/templates/tasks-template.schema.json +13 -54
@@ -0,0 +1,71 @@
1
+ import type { RuntimeContextKey } from "./runtime-context.js"
2
+ import {
3
+ createPlanLifecycleChangedEnvelope,
4
+ publishPlanLifecycleChanged,
5
+ type PrismaNotifyTransaction,
6
+ } from "./plan-lifecycle-events.js"
7
+ import type { PlanLifecycleStatus } from "./plan-lifecycle.js"
8
+
9
+ export interface TrackedPlanRow {
10
+ id: string
11
+ projectKey: string
12
+ adapterKey: string
13
+ planName: string
14
+ lifecycle: PlanLifecycleStatus
15
+ revision: number
16
+ }
17
+
18
+ export interface PlanTrackingTransaction extends PrismaNotifyTransaction {
19
+ planRecord: {
20
+ findFirst(args: { where: Record<string, unknown> }): Promise<TrackedPlanRow | null>
21
+ create(args: { data: Record<string, unknown> }): Promise<TrackedPlanRow>
22
+ update(args: { where: Record<string, unknown>; data: Record<string, unknown> }): Promise<TrackedPlanRow>
23
+ }
24
+ }
25
+
26
+ export interface PlanTrackingDatabase {
27
+ $transaction<T>(work: (tx: PlanTrackingTransaction) => Promise<T>): Promise<T>
28
+ }
29
+
30
+ export async function trackPlanAndPublish(
31
+ database: PlanTrackingDatabase,
32
+ input: {
33
+ context: RuntimeContextKey
34
+ planName: string
35
+ planPath: string
36
+ projection: Record<string, unknown>
37
+ },
38
+ ): Promise<{ created: boolean; plan: TrackedPlanRow }> {
39
+ return database.$transaction(async (tx) => {
40
+ const scope = {
41
+ projectKey: input.context.projectKey,
42
+ adapterKey: input.context.adapterKey,
43
+ planName: input.planName,
44
+ }
45
+ const existing = await tx.planRecord.findFirst({ where: scope })
46
+ const plan = existing
47
+ ? await tx.planRecord.update({
48
+ where: { id: existing.id, revision: existing.revision },
49
+ data: { ...input.projection, planPath: input.planPath, revision: { increment: 1 } },
50
+ })
51
+ : await tx.planRecord.create({
52
+ data: {
53
+ ...scope,
54
+ ...input.projection,
55
+ planPath: input.planPath,
56
+ lifecycle: "DRAFT",
57
+ revision: 0,
58
+ },
59
+ })
60
+ const expectedRevision = existing ? existing.revision + 1 : 0
61
+ if (plan.revision !== expectedRevision) {
62
+ throw new Error(`Plan track revision 未按预期变化: expected=${expectedRevision}, actual=${plan.revision}`)
63
+ }
64
+ await publishPlanLifecycleChanged(tx, createPlanLifecycleChangedEnvelope({
65
+ context: input.context,
66
+ planId: plan.id,
67
+ revision: plan.revision,
68
+ }))
69
+ return { created: existing === null, plan }
70
+ })
71
+ }
@@ -1,8 +1,8 @@
1
- import { DATABASE_URL, PROJECT_ROOT } from "./env.js"
2
- import { join, dirname, resolve as pathResolve } from "path"
1
+ import { DATABASE_URL, PROJECT_ROOT, MAGIC_DIR } from "./env.js"
2
+ import { join, dirname } from "path"
3
3
  import { existsSync } from "fs"
4
- import { fileURLToPath } from "url"
5
4
  import { createRequire } from "module"
5
+ import { findUpSync } from "find-up"
6
6
 
7
7
  const require = createRequire(import.meta.url)
8
8
 
@@ -11,14 +11,20 @@ const require = createRequire(import.meta.url)
11
11
  // 双库分离项目(add.prisma=postgres + 业务 schema=mysql)需显式设置环境变量
12
12
  // PRISMA_CLIENT_DIR=add-prisma,否则会加载业务库 client 导致 provider mismatch
13
13
  const clientDir = process.env.PRISMA_CLIENT_DIR || "prisma"
14
+ // 锚点查找兜底(find-up 包):magicDir 向上查找项目根,替代手算层级(轮次 3)
15
+ const anchorRoot = (() => {
16
+ const hit = findUpSync(MAGIC_DIR, { cwd: import.meta.dirname, type: "directory" });
17
+ return hit ? dirname(hit) : null;
18
+ })();
14
19
  const candidates = [
15
20
  join(PROJECT_ROOT, `src/generated/${clientDir}/client.ts`),
16
21
  join(PROJECT_ROOT, `src/generated/${clientDir}/client.js`),
17
22
  join(process.cwd(), `src/generated/${clientDir}/client.ts`),
18
23
  join(process.cwd(), `src/generated/${clientDir}/client.js`),
19
- // 从当前文件位置反推:shared/prisma.ts → 上 4 层到项目根
20
- (() => { const d = dirname(fileURLToPath(import.meta.url)); const root = pathResolve(d, "..", "..", "..", ".."); return join(root, `src/generated/${clientDir}/client.ts`) })(),
21
- (() => { const d = dirname(fileURLToPath(import.meta.url)); const root = pathResolve(d, "..", "..", "..", ".."); return join(root, `src/generated/${clientDir}/client.js`) })(),
24
+ ...(anchorRoot ? [
25
+ join(anchorRoot, `src/generated/${clientDir}/client.ts`),
26
+ join(anchorRoot, `src/generated/${clientDir}/client.js`),
27
+ ] : []),
22
28
  ]
23
29
  // 双库分离项目提示:检测到 add-prisma 客户端但未显式选择时提醒用户自行决策
24
30
  if (clientDir === "prisma" && existsSync(join(PROJECT_ROOT, "src/generated/add-prisma/client.ts"))) {
@@ -4,8 +4,7 @@
4
4
  // >>> CAIJUE GENERATED START >>>
5
5
  export const PROJECT_ROOT_PRIORITY = ["env_var", "dirname_fallback", "cwd_fallback"] as const;
6
6
  export type ProjectRootTier = (typeof PROJECT_ROOT_PRIORITY)[number];
7
- // <<< CAIJUE GENERATED END <<<
8
- // >>> USER CODE >>>
7
+ // <<< CAIJUE GENERATED END <<<// >>> USER CODE >>>
9
8
  import { resolve, dirname } from "path"
10
9
  import { existsSync } from "fs"
11
10
 
@@ -0,0 +1,16 @@
1
+ // ⚠️ 由 caijuehub/transcribe.ts 自动生成,不要手动编辑!
2
+ // 改 *-rules.toml 后重新运行: add-coder generate
3
+
4
+ // >>> CAIJUE GENERATED START >>>
5
+ export const QUANT_RECHECK_CONFIG = {
6
+ ANCHOR_WEIGHT: 3,
7
+ IDF_MIN_DOCS: 5,
8
+ RRF_K: 10,
9
+ MIN_ABS_SIMILARITY: 0.25,
10
+ RRF_MIN_CANDIDATES: 2,
11
+ EWMA_LAMBDA: 0.3,
12
+ EWMA_K: 1.5,
13
+ EWMA_COLD_START: 10,
14
+ EWMA_BASELINE: 0.16,
15
+ } as const;
16
+ // <<< CAIJUE GENERATED END <<<
@@ -0,0 +1,28 @@
1
+ /*
2
+ * @Author : xiaomingming wujixmm@gmail.com
3
+ * @Date : 2026-08-10 18:41:34
4
+ * @LastEditors : xiaomingming wujixmm@gmail.com
5
+ * @LastEditTime : 2026-08-10 18:41:35
6
+ * @FilePath : /farm-agent/home/xmm/ai/add-coder/templates/core/scripts/mcp-server/shared/redact.ts
7
+ * @Description :
8
+ */
9
+ // 敏感信息脱敏(进程层并发契约 v2:日志/错误不泄露 DATABASE_URL/密码)
10
+ // 统一入口:response.ts 的 textResponse/errorResponse 内部调用,一处封装全覆盖所有工具
11
+
12
+ const CONNECTION_URL_RE =
13
+ /(postgres(?:ql)?|mysql|redis|mongodb(?:\+srv)?):\/\/[^\s"'`]+/gi
14
+
15
+ export function redactUrl(url: string): string {
16
+ // postgres://user:pass@host:port/db → postgres://user:****@host:port/db
17
+ return url.replace(/(:\/\/[^:/\s]+:)([^@/\s]+)(@)/, "$1****$3")
18
+ }
19
+
20
+ export function redact(text: string): string {
21
+ return text.replace(CONNECTION_URL_RE, (m) => {
22
+ try {
23
+ return redactUrl(m)
24
+ } catch {
25
+ return m
26
+ }
27
+ })
28
+ }
@@ -1,9 +1,10 @@
1
1
  import type { ToolResponse } from "../types.js"
2
+ import { redact } from "./redact.js"
2
3
 
3
4
  export function textResponse(text: string): { content: ToolResponse } {
4
- return { content: [{ type: "text", text }] }
5
+ return { content: [{ type: "text", text: redact(text) }] }
5
6
  }
6
7
 
7
8
  export function errorResponse(message: string): { content: ToolResponse; isError: boolean } {
8
- return { content: [{ type: "text", text: message }], isError: true }
9
+ return { content: [{ type: "text", text: redact(message) }], isError: true }
9
10
  }
@@ -0,0 +1,77 @@
1
+ import { createHash } from "crypto"
2
+ import { realpathSync } from "fs"
3
+ import { isAbsolute, relative, resolve } from "path"
4
+
5
+ export const ADAPTER_MAGIC_DIR = Object.freeze({
6
+ add: ".add",
7
+ claude: ".claude",
8
+ codex: ".codex",
9
+ qoder: ".qoder",
10
+ trae: ".trae",
11
+ vscode: ".vscode",
12
+ } as const)
13
+
14
+ export type AdapterKey = keyof typeof ADAPTER_MAGIC_DIR
15
+ export type AdapterMagicDir = (typeof ADAPTER_MAGIC_DIR)[AdapterKey]
16
+
17
+ export interface RuntimeContextKey {
18
+ projectKey: string
19
+ adapterKey: AdapterKey
20
+ projectRoot: string
21
+ magicDir: AdapterMagicDir
22
+ contextId: string
23
+ }
24
+
25
+ const MAGIC_DIR_ADAPTER = new Map<AdapterMagicDir, AdapterKey>(
26
+ Object.entries(ADAPTER_MAGIC_DIR).map(([adapterKey, magicDir]) => [magicDir, adapterKey as AdapterKey]),
27
+ )
28
+
29
+ export function adapterKeyFromMagicDir(magicDir: string): AdapterKey {
30
+ const adapterKey = MAGIC_DIR_ADAPTER.get(magicDir as AdapterMagicDir)
31
+ if (!adapterKey) {
32
+ throw new Error(`未知 ADD adapter magicDir: ${magicDir}`)
33
+ }
34
+ return adapterKey
35
+ }
36
+
37
+ export function canonicalProjectRoot(projectRoot: string): string {
38
+ if (!projectRoot.trim()) throw new Error("PROJECT_ROOT 不能为空")
39
+ return realpathSync.native(resolve(projectRoot))
40
+ }
41
+
42
+ export function projectKeyFromRoot(projectRoot: string): string {
43
+ const canonicalRoot = canonicalProjectRoot(projectRoot)
44
+ return createHash("sha256").update(`add-project\0${canonicalRoot}`).digest("hex")
45
+ }
46
+
47
+ export function createRuntimeContext(projectRoot: string, magicDir: string): Readonly<RuntimeContextKey> {
48
+ const adapterKey = adapterKeyFromMagicDir(magicDir)
49
+ const canonicalRoot = canonicalProjectRoot(projectRoot)
50
+ const projectKey = projectKeyFromRoot(canonicalRoot)
51
+ return Object.freeze({
52
+ projectKey,
53
+ adapterKey,
54
+ projectRoot: canonicalRoot,
55
+ magicDir: ADAPTER_MAGIC_DIR[adapterKey],
56
+ contextId: `${projectKey}:${adapterKey}`,
57
+ })
58
+ }
59
+
60
+ export function runtimeScopeRoot(context: RuntimeContextKey): string {
61
+ return resolve(context.projectRoot, context.magicDir)
62
+ }
63
+
64
+ export function isPathInRuntimeScope(context: RuntimeContextKey, candidatePath: string): boolean {
65
+ const scopeRoot = runtimeScopeRoot(context)
66
+ const absoluteCandidate = isAbsolute(candidatePath) ? resolve(candidatePath) : resolve(context.projectRoot, candidatePath)
67
+ const scopedRelative = relative(scopeRoot, absoluteCandidate)
68
+ return scopedRelative === "" || (!scopedRelative.startsWith("..") && !isAbsolute(scopedRelative))
69
+ }
70
+
71
+ export function assertPathInRuntimeScope(context: RuntimeContextKey, candidatePath: string): string {
72
+ const absoluteCandidate = isAbsolute(candidatePath) ? resolve(candidatePath) : resolve(context.projectRoot, candidatePath)
73
+ if (!isPathInRuntimeScope(context, absoluteCandidate)) {
74
+ throw new Error(`路径越出当前 ADD adapter scope: ${absoluteCandidate}(scope=${runtimeScopeRoot(context)})`)
75
+ }
76
+ return absoluteCandidate
77
+ }
@@ -0,0 +1,159 @@
1
+ import { readdir, readFile } from "fs/promises"
2
+ import { basename, join, relative } from "path"
3
+
4
+ export type SchemaView = "business" | "add" | "all"
5
+ export type SchemaTopologyMode = "single" | "split"
6
+ export type SchemaBoundary = "shared" | "business" | "add"
7
+
8
+ export interface PrismaDeclaration {
9
+ kind: "model" | "enum"
10
+ name: string
11
+ sourcePath: string
12
+ sourceFile: string
13
+ boundary: SchemaBoundary
14
+ body: string
15
+ fieldCount: number
16
+ }
17
+
18
+ export interface PrismaSchemaFile {
19
+ path: string
20
+ file: string
21
+ boundary: SchemaBoundary
22
+ declarations: PrismaDeclaration[]
23
+ }
24
+
25
+ export interface PrismaSchemaTopology {
26
+ mode: SchemaTopologyMode
27
+ view: SchemaView
28
+ schemaRoot: string
29
+ files: PrismaSchemaFile[]
30
+ models: PrismaDeclaration[]
31
+ enums: PrismaDeclaration[]
32
+ }
33
+
34
+ function extractBlock(source: string, braceIndex: number): string {
35
+ let depth = 1
36
+ let cursor = braceIndex + 1
37
+ while (depth > 0 && cursor < source.length) {
38
+ if (source[cursor] === "{") depth += 1
39
+ if (source[cursor] === "}") depth -= 1
40
+ cursor += 1
41
+ }
42
+ if (depth !== 0) throw new Error("Prisma declaration 花括号未闭合")
43
+ return source.slice(braceIndex, cursor)
44
+ }
45
+
46
+ export function parsePrismaDeclarations(
47
+ source: string,
48
+ sourcePath: string,
49
+ boundary: SchemaBoundary,
50
+ ): PrismaDeclaration[] {
51
+ const declarations: PrismaDeclaration[] = []
52
+ const declarationRegex = /^\s*(model|enum)\s+(\w+)\s*\{/gm
53
+ let match: RegExpExecArray | null
54
+ while ((match = declarationRegex.exec(source)) !== null) {
55
+ const braceIndex = source.indexOf("{", match.index)
56
+ const body = extractBlock(source, braceIndex)
57
+ const kind = match[1] as "model" | "enum"
58
+ declarations.push({
59
+ kind,
60
+ name: match[2],
61
+ sourcePath,
62
+ sourceFile: basename(sourcePath),
63
+ boundary,
64
+ body,
65
+ fieldCount: kind === "model"
66
+ ? body.split("\n").filter(line => {
67
+ const trimmed = line.trim()
68
+ return trimmed.length > 0 && trimmed !== "{" && trimmed !== "}" && !trimmed.startsWith("//") && !trimmed.startsWith("@@")
69
+ }).length
70
+ : 0,
71
+ })
72
+ }
73
+ return declarations
74
+ }
75
+
76
+ function boundaryFor(file: string, mode: SchemaTopologyMode): SchemaBoundary {
77
+ if (mode === "single") return "shared"
78
+ return file === "add.prisma" ? "add" : "business"
79
+ }
80
+
81
+ function includedInView(file: string, view: SchemaView): boolean {
82
+ if (view === "all") return true
83
+ return view === "add" ? file === "add.prisma" : file !== "add.prisma"
84
+ }
85
+
86
+ export async function loadPrismaSchemaTopology(input: {
87
+ projectRoot: string
88
+ splitDatabase: boolean
89
+ view?: SchemaView
90
+ }): Promise<PrismaSchemaTopology> {
91
+ const schemaRoot = join(input.projectRoot, "prisma")
92
+ const mode: SchemaTopologyMode = input.splitDatabase ? "split" : "single"
93
+ const view = input.view ?? "all"
94
+ let names: string[]
95
+ try {
96
+ names = (await readdir(schemaRoot)).filter(file => file.endsWith(".prisma")).sort()
97
+ } catch {
98
+ throw new Error(`未找到 Prisma schema 目录: ${schemaRoot}`)
99
+ }
100
+ if (names.length === 0) throw new Error(`Prisma schema 目录中没有 *.prisma: ${schemaRoot}`)
101
+
102
+ const selected = names.filter(file => includedInView(file, view))
103
+ if (selected.length === 0) {
104
+ throw new Error(`Schema view=${view} 没有可扫描文件;mode=${mode};全部文件=${names.join(", ")}`)
105
+ }
106
+ const files: PrismaSchemaFile[] = []
107
+ for (const file of selected) {
108
+ const path = join(schemaRoot, file)
109
+ const boundary = boundaryFor(file, mode)
110
+ const source = await readFile(path, "utf8")
111
+ files.push({ path, file, boundary, declarations: parsePrismaDeclarations(source, path, boundary) })
112
+ }
113
+ const declarations = files.flatMap(file => file.declarations)
114
+ const models = declarations.filter(item => item.kind === "model")
115
+ const enums = declarations.filter(item => item.kind === "enum")
116
+ if (models.length === 0) {
117
+ throw new Error(
118
+ `Schema 诊断: 检测到 ${files.length} 个 Prisma 文件但 view=${view} 解析为 0 个模型;` +
119
+ `mode=${mode};扫描=${files.map(file => relative(input.projectRoot, file.path)).join(", ")}`,
120
+ )
121
+ }
122
+
123
+ const byName = new Map<string, PrismaDeclaration[]>()
124
+ for (const model of models) {
125
+ const key = model.name.toLowerCase()
126
+ byName.set(key, [...(byName.get(key) ?? []), model])
127
+ }
128
+ const conflicts = [...byName.entries()].filter(([, entries]) => entries.length > 1)
129
+ if (conflicts.length > 0) {
130
+ const details = conflicts.map(([name, entries]) =>
131
+ `${name}: ${entries.map(entry => `${relative(input.projectRoot, entry.sourcePath)}[${entry.boundary}]`).join(", ")}`,
132
+ )
133
+ throw new Error(`Schema 模型冲突(fail closed): ${details.join("; ")}`)
134
+ }
135
+
136
+ return { mode, view, schemaRoot, files, models, enums }
137
+ }
138
+
139
+ export function formatSchemaTopology(topology: PrismaSchemaTopology, projectRoot: string): string {
140
+ const lines = [
141
+ "=== Prisma Schema 拓扑 ===",
142
+ `mode: ${topology.mode}`,
143
+ `view: ${topology.view}`,
144
+ `databaseBoundaries: ${topology.mode === "single" ? "shared (business + ADD 同库)" : "business, add (分库;all 仅汇总展示,不代表可跨库 join)"}`,
145
+ `scannedFiles: ${topology.files.length}`,
146
+ ]
147
+ for (const file of topology.files) {
148
+ lines.push(` - ${relative(projectRoot, file.path)} [${file.boundary}] models=${file.declarations.filter(item => item.kind === "model").length}`)
149
+ }
150
+ lines.push("", `模型 (${topology.models.length} 个):`)
151
+ for (const model of topology.models) {
152
+ lines.push(` ${model.name} (${model.fieldCount} 字段) — ${relative(projectRoot, model.sourcePath)} [${model.boundary}]`)
153
+ }
154
+ if (topology.enums.length > 0) {
155
+ lines.push("", `枚举 (${topology.enums.length} 个):`)
156
+ for (const item of topology.enums) lines.push(` ${item.name} — ${relative(projectRoot, item.sourcePath)} [${item.boundary}]`)
157
+ }
158
+ return lines.join("\n")
159
+ }
@@ -1,12 +1,14 @@
1
1
  import * as z from "zod/v4"
2
+ import { createHash } from "crypto"
2
3
  import type { ToolRegistrar } from "./registrar.js"
3
4
  import { textResponse, errorResponse } from "../shared/response.js"
4
5
  import { prisma } from "../shared/prisma.js"
5
- import { PROJECT_ID, PROJECT_ROOT } from "../shared/env.js"
6
+ import { PROJECT_ID, PROJECT_ROOT, getRuntimeContext } from "../shared/env.js"
6
7
  import type { AddUserRow, DevOperationRow } from "../shared/db-types.js"
7
8
  import { AddUserRowSchema, DevOperationRowSchema, validatedDelegate } from "../shared/db-types.js"
8
9
 
9
10
  export function registerAuditTools(server: ToolRegistrar) {
11
+ const runtimeContext = getRuntimeContext()
10
12
 
11
13
  // 无类型边界单点(zod 托管):动态 client → 运行期校验的泛型委托
12
14
  const devDb = validatedDelegate<DevOperationRow>(prisma.devOperation, DevOperationRowSchema, "DevOperation")
@@ -30,7 +32,10 @@ export function registerAuditTools(server: ToolRegistrar) {
30
32
  }, async (args: Record<string, string | number | undefined>, _ctx: unknown) => {
31
33
  try {
32
34
  const { targetType, action, targetId, planKeyword, keyword, sinceMinutes, limit = 20 } = args
33
- const where: Record<string, unknown> = {}
35
+ const where: Record<string, unknown> = {
36
+ projectKey: runtimeContext.projectKey,
37
+ producerAdapterKey: runtimeContext.adapterKey,
38
+ }
34
39
  if (sinceMinutes !== undefined) where.createdAt = { gte: new Date(Date.now() - Number(sinceMinutes) * 60 * 1000) }
35
40
  if (targetType) where.targetType = s(targetType); if (action) where.action = s(action); if (targetId) where.targetId = s(targetId); if (planKeyword) where.planKeyword = s(planKeyword)
36
41
  const kw = s(keyword)
@@ -44,6 +49,7 @@ export function registerAuditTools(server: ToolRegistrar) {
44
49
  const lines = [`=== 开发操作审计日志 (条件: ${fl.length > 0 ? fl.join(", ") : "无过滤条件(最近全部)"}) ===`, `服务项目: ${PROJECT_ID} (${PROJECT_ROOT})`, `共 ${logs.length} 条记录`]
45
50
  for (let i = 0; i < logs.length; i++) {
46
51
  const l = logs[i]; lines.push(`[${i+1}] ${l.createdAt.toISOString()}`, ` action: ${l.action} | targetType: ${l.targetType} | targetId: ${l.targetId || "(无)"}`)
52
+ lines.push(` beforeState: ${JSON.stringify(l.beforeState)}`, ` afterState: ${JSON.stringify(l.afterState)}`)
47
53
  if (l.reason) lines.push(` reason: ${l.reason}`); if (l.planKeyword) lines.push(` planKeyword: ${l.planKeyword}`)
48
54
  }
49
55
  if (planKeyword) { lines.push("=== Plan 分组 ===", `planKeyword=${planKeyword} 的操作链:`); for (const l of logs) lines.push(` ${l.createdAt.toISOString().slice(11,19)} ${l.action}`) }
@@ -60,22 +66,58 @@ export function registerAuditTools(server: ToolRegistrar) {
60
66
  targetType: z.string().describe("目标类型: 'API_ROUTE', 'COMPONENT', 'SCHEMA', 'RULE', 'DOC', 'PLAN', 'SPEC', 'SKILL', 'AGENT' 等"),
61
67
  targetId: z.string().optional().describe("目标标识(相对路径)"),
62
68
  planKeyword: z.string().optional().describe("关联 Plan 的关键词"),
63
- beforeState: z.string().describe("操作前的状态(JSON 字符串)。必填,记录变更前的文件内容或配置状态,用于审计回溯。"),
64
- afterState: z.string().describe("操作后的状态(JSON 字符串)。必填,记录变更后的文件内容或配置状态,用于审计回溯。"),
69
+ beforeState: z.string().describe("操作前的状态(非 null JSON 对象或数组字符串)。必填,用于审计回溯。"),
70
+ afterState: z.string().describe("操作后的状态(非 null JSON 对象或数组字符串)。必填,用于审计回溯。"),
65
71
  reason: z.string().optional().describe("操作原因"),
72
+ operationKey: z.string().optional().describe("调用方提供的幂等键;未提供时按本次结构化操作内容计算"),
66
73
  }),
67
74
  }, async (args: Record<string, string | number | undefined>, _ctx: unknown) => {
68
75
  try {
69
- const { action, targetType, targetId, planKeyword, beforeState, afterState, reason } = args
76
+ const { action, targetType, targetId, planKeyword, beforeState, afterState, reason, operationKey } = args
70
77
  const tId = s(targetId)
71
78
  const pathWarnings: string[] = []
72
79
  if (tId && (tId.startsWith("/") || /^[A-Z]:\\/.test(tId))) pathWarnings.push(`⚠️ targetId 使用了绝对路径: "${tId}"。请使用相对路径。`)
80
+ if (!s(beforeState).trim() || !s(afterState).trim()) return errorResponse("beforeState/afterState 必须提供非空 JSON 字符串。")
73
81
  let parsedBefore: unknown, parsedAfter: unknown
74
82
  try { if (beforeState) parsedBefore = JSON.parse(s(beforeState)); if (afterState) parsedAfter = JSON.parse(s(afterState)) } catch { return errorResponse("beforeState/afterState 必须是有效的 JSON 字符串。") }
83
+ const isStructuredState = (value: unknown): value is Record<string, unknown> | unknown[] => typeof value === "object" && value !== null
84
+ if (!isStructuredState(parsedBefore) || !isStructuredState(parsedAfter)) return errorResponse("beforeState/afterState 必须是非 null 的 JSON 对象或数组。")
75
85
  let systemUser = await userDb.findUnique({ where: { username: "ai-assistant" }, select: { id: true } })
76
86
  if (!systemUser) systemUser = await userDb.create({ data: { id: "ai-assistant", username: "ai-assistant", email: "ai-assistant@internal" } })
77
- const log = await devDb.create({ data: { userId: systemUser.id, planKeyword: s(planKeyword) || "unknown", action: s(action), targetType: s(targetType), targetId: tId || "unknown", beforeState: parsedBefore ?? null, afterState: parsedAfter ?? null, reason: s(reason) || null } })
78
- const lines = [`✅ 开发操作已记录`, ` 落库项目: ${PROJECT_ID} (${PROJECT_ROOT})`, ` ID: ${log.id}`, ` action: ${s(action)}`, ` targetType: ${s(targetType)}`, ` targetId: ${tId || "unknown"}`, ` planKeyword: ${s(planKeyword) || "unknown"}`, ` createdAt: ${log.createdAt.toISOString()}`]
87
+ const operationPayload = {
88
+ projectKey: runtimeContext.projectKey,
89
+ producerAdapterKey: runtimeContext.adapterKey,
90
+ action: s(action),
91
+ targetType: s(targetType),
92
+ targetId: tId || "unknown",
93
+ planKeyword: s(planKeyword) || "unknown",
94
+ beforeState: parsedBefore,
95
+ afterState: parsedAfter,
96
+ reason: s(reason) || null,
97
+ }
98
+ const resolvedOperationKey = s(operationKey).trim() || createHash("sha256")
99
+ .update(JSON.stringify(operationPayload))
100
+ .digest("hex")
101
+ const scopedData: Partial<DevOperationRow> = {
102
+ ...operationPayload,
103
+ userId: systemUser.id,
104
+ contextId: runtimeContext.contextId,
105
+ toolName: "record_dev_operation",
106
+ operationKey: resolvedOperationKey,
107
+ }
108
+ const log = await devDb.upsert({
109
+ where: {
110
+ projectKey_producerAdapterKey_toolName_operationKey: {
111
+ projectKey: runtimeContext.projectKey,
112
+ producerAdapterKey: runtimeContext.adapterKey,
113
+ toolName: "record_dev_operation",
114
+ operationKey: resolvedOperationKey,
115
+ },
116
+ },
117
+ create: scopedData,
118
+ update: {},
119
+ })
120
+ const lines = [`✅ 开发操作已记录`, ` 落库项目: ${PROJECT_ID} (${PROJECT_ROOT})`, ` ID: ${log.id}`, ` action: ${s(action)}`, ` targetType: ${s(targetType)}`, ` targetId: ${tId || "unknown"}`, ` planKeyword: ${s(planKeyword) || "unknown"}`, ` beforeState: ${JSON.stringify(log.beforeState)}`, ` afterState: ${JSON.stringify(log.afterState)}`, ` createdAt: ${log.createdAt.toISOString()}`]
79
121
  if (pathWarnings.length > 0) { lines.push(""); lines.push(...pathWarnings) }
80
122
  lines.push("", `📋 落库回查(必须执行):`, tId ? ` query_audit_logs({ targetId: "${tId}" }) — 确认本条记录已写入` : "")
81
123
  return textResponse(lines.join("\n"))