140 lines
6.3 KiB
TypeScript
140 lines
6.3 KiB
TypeScript
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<string[]> {
|
||
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<Record<string, unknown>> {
|
||
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<typeof item> => 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 <module>',
|
||
'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)}`,
|
||
],
|
||
};
|
||
}
|