import { existsSync } from 'node:fs'; import path from 'node:path'; import type { ChangeDocSyncInput } from '../types.js'; import { detectRepoType, parseGitStatusFiles } from '../core/project.js'; import { tryExecFile } from '../core/exec.js'; import { resolveProject, toPosix, workspaceRoot } from '../core/workspace.js'; interface DocSyncRule { docs: string[]; match: RegExp; reason: string; } const docSyncRules: DocSyncRule[] = [ { docs: ['Node/kt-template-online-api/README.md', 'Node/kt-template-online-api/API.md', 'docs/obsidian/modules/KT 模块 - API Backend.md'], match: /^(src\/|Node\/kt-template-online-api\/src\/).*(controller|service|dto|entity|module|guard|interceptor|filter)|^Node\/kt-template-online-api\/src\//, reason: 'API 接口、DTO/Entity、模块或服务能力变更,需要同步后端 README/API 和模块索引。', }, { docs: ['Node/kt-template-online-api/API.md', 'docs/qqbot-nas-runtime.md', 'docs/obsidian/modules/KT 模块 - QQBot BangDream FFLogs.md'], match: /qqbot|bangDream|fflogs|ff14Market|napcat/i, reason: 'QQBot/BangDream/FFLogs/NapCat 链路变更,需要同步命令、运行态和 smoke 文档。', }, { docs: ['Vue/kt-template-admin/README.md', 'docs/obsidian/modules/KT 模块 - Admin.md'], match: /Vue\/kt-template-admin\/|apps\/web-antdv-next\/src\/views\/|apps\/web-antdv-next\/src\/api\//, reason: 'Admin 页面、API 客户端或业务视图变更,需要同步 Admin README 和模块页。', }, { docs: ['Vue/kt-template-admin/README.md', 'docs/obsidian/modules/KT 模块 - KtTable.md', 'skills/kt-component-specialist/SKILL.md'], match: /ktTable|KtTable|components\/ktTable/i, reason: 'KtTable 组件或迁移规则变更,需要同步专项模块和 skill。', }, { docs: ['Vue/kt-blog-web/README.md', 'docs/obsidian/modules/KT 模块 - Blog Web.md', 'skills/kt-blog-argon-workflow/SKILL.md'], match: /Vue\/kt-blog-web\/|blog|argon/i, reason: 'Blog/Argon 能力变更,需要同步博客 README、模块页和专项 skill。', }, { docs: ['Plugins/fnos-k8s-dashboard-fpk/README.md', 'Plugins/fnos-k8s-dashboard-fpk/AGENTS.md', 'docs/obsidian/modules/KT 模块 - Plugins.md'], match: /fnos-k8s-dashboard|fnosK8s|fpk|Kubernetes/i, reason: 'fnOS K8s 插件变更,需要同步插件 README/AGENTS 和 Obsidian 模块。', }, { docs: ['mcp/ktWorkflow/README.md', 'AGENTS.md', 'TASKS.md', 'docs/kt-testing-workflows.md', 'docs/obsidian/KT 工作流工程化.md'], match: /mcp\/ktWorkflow|src\/tools\/|src\/registerTools|src\/core|workflow|cleanup|review|selfTest/i, reason: 'ktWorkflow 或自动化规则变更,需要同步 README、根规则、测试流程和当前任务记录。', }, { docs: ['AGENTS.md', 'SKILLS.md', 'docs/kt-context-semantics.md', 'docs/obsidian/modules/KT 模块 - Root Governance.md'], match: /AGENTS\.md|SKILLS\.md|skills\/.*\/SKILL\.md|docs\/kt-context-semantics\.md/, reason: '上下文治理或 skill 规则变更,需要同步根治理文档和 Obsidian 模块。', }, { docs: ['docs/obsidian/KT 文档矩阵.md', 'docs/obsidian/KT Obsidian 工程化.md', 'docs/obsidian/KT 关系图谱.canvas'], match: /\.obsidian|docs\/obsidian|\.canvas$|\.base$/, reason: 'Obsidian vault、Canvas、Base 或文档矩阵变更,需要同步图谱和校验。', }, { docs: ['docs/kt-testing-workflows.md', 'mcp/ktWorkflow/README.md'], match: /\.kt-workspace|test-artifacts|test-logs|verify|playwright|smoke|Jenkinsfile|k8s\/|deploy\//i, reason: '测试、验证、发布或运行态产物流程变更,需要同步测试流程和 ktWorkflow 文档。', }, ]; async function inferChangedFiles(projectKey: string): Promise { const project = resolveProject(projectKey); if (detectRepoType(project.path) !== 'git') return []; const status = await tryExecFile('git', ['status', '--short'], project.path); if (!status.ok) return []; return parseGitStatusFiles(status.stdout).map((file) => project.alias && project.alias !== 'root' ? `${project.relativePath}/${file.replaceAll('\\', '/')}` : file.replaceAll('\\', '/'), ); } function normalizeChangedFile(file: string, projectKey?: string): string { const project = resolveProject(projectKey || 'root'); const normalized = file.replaceAll('\\', '/'); if (project.alias && project.alias !== 'root' && !normalized.startsWith(`${project.relativePath}/`)) { return `${project.relativePath}/${normalized}`; } return normalized; } export async function buildChangeDocSync(input: ChangeDocSyncInput = {}): Promise> { const projectKey = input.project || 'root'; const changedFiles = (input.changedFiles?.length ?? 0) > 0 ? input.changedFiles!.map((file) => normalizeChangedFile(file, projectKey)) : await inferChangedFiles(projectKey); const recommendations = docSyncRules .map((rule) => { const matchedFiles = changedFiles.filter((file) => rule.match.test(file)); return matchedFiles.length > 0 ? { docs: rule.docs.map((file) => ({ exists: existsSync(path.resolve(workspaceRoot, file)), path: file, })), matchedFiles, reason: rule.reason, } : null; }) .filter((item): item is NonNullable => Boolean(item)); const requiredDocs = Array.from( new Set(recommendations.flatMap((item) => item.docs.map((doc) => doc.path))), ).map((file) => ({ exists: existsSync(path.resolve(workspaceRoot, file)), path: file, })); return { changedFiles, commands: [ 'pnpm --dir mcp/ktWorkflow run obsidian-context -- --query ', 'pnpm --dir mcp/ktWorkflow run obsidian-validate', 'pnpm --dir mcp/ktWorkflow run global-review', ], requiredDocs, recommendations, summary: { changedFileCount: changedFiles.length, recommendationCount: recommendations.length, requiredDocCount: requiredDocs.length, }, verification: [ '确认 requiredDocs 中需要更新的文档已同步,未更新时在最终说明原因。', '涉及 Obsidian 索引时运行 obsidian-validate。', '涉及 ktWorkflow/规则/测试流程时运行 self-test 和 global-review。', `当前项目:${toPosix(resolveProject(projectKey).relativePath)}`, ], }; }