@code2knowledge/skill 0.0.0-stage → 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +377 -2
- package/SKILL.md +46 -0
- package/THIRD_PARTY_NOTICES.md +23 -0
- package/agents/openai.yaml +4 -0
- package/assets/notification-config.example.yml +6 -0
- package/assets/project-context/ontology/concept-types.yaml +52 -0
- package/assets/project-context/ontology/node-types.yaml +82 -0
- package/assets/project-context/ontology/relation-types.yaml +67 -0
- package/assets/project-context/ontology/schema.md +37 -0
- package/assets/project-context/rules/sync-rules.md +15 -0
- package/assets/project-context/rules/validation-rules.yaml +92 -0
- package/package.json +50 -4
- package/references/analysis.md +33 -0
- package/references/downstream-contract.md +23 -0
- package/references/execution.md +154 -0
- package/references/knowledge-model.md +108 -0
- package/references/notifications.md +46 -0
- package/references/wizard.md +110 -0
- package/scripts/knowledge.mjs +54 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/contract.mjs +347 -0
- package/scripts/lib/knowledge.mjs +1054 -0
- package/scripts/lib/notify.mjs +90 -0
- package/scripts/lib/preflight.mjs +63 -0
- package/scripts/lib/wizard.mjs +205 -0
- package/scripts/postinstall.mjs +8 -0
- package/scripts/register.mjs +107 -0
- package/scripts/setup.mjs +9 -0
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
import { requireDocumentChoice, syncMaster } from "./preflight.mjs";
|
|
2
|
+
import { notify } from "./notify.mjs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import fsSync from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { schemaVersion, fullCommit, sha256, makeContext, existingIdentities, writeBusinessTemplates, validateContract, assetFiles } from "./contract.mjs";
|
|
11
|
+
|
|
12
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
13
|
+
function canonicalPath(value) {
|
|
14
|
+
let current = path.resolve(value);
|
|
15
|
+
const missing = [];
|
|
16
|
+
for (;;) {
|
|
17
|
+
try { return path.join(fsSync.realpathSync(current), ...missing.reverse()); }
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code !== "ENOENT") throw error;
|
|
20
|
+
const parent = path.dirname(current);
|
|
21
|
+
if (parent === current) throw error;
|
|
22
|
+
missing.push(path.basename(current)); current = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const workspaceDir = canonicalPath(process.env.C2K_WORKSPACE || path.join(process.cwd(), ".code2knowledge"));
|
|
27
|
+
const projectContextTemplateDir = path.join(rootDir, "assets", "project-context");
|
|
28
|
+
const runRootDir = path.join(workspaceDir, "runs");
|
|
29
|
+
const materialRootDir = path.join(workspaceDir, "materials");
|
|
30
|
+
const knowledgeRootDir = process.cwd();
|
|
31
|
+
const documentExtensions = [".md", ".txt", ".csv", ".yaml", ".yml", ".json", ".docx", ".doc", ".pdf", ".xlsx", ".xls", ".pptx", ".ppt", ".html", ".htm"];
|
|
32
|
+
function emit(event, payload) {
|
|
33
|
+
if (process.env.C2K_VERBOSE && (event === "log" || event === "task")) {
|
|
34
|
+
console.error(payload.message || payload.step || payload.status);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function slugify(value) {
|
|
39
|
+
return String(value || "domain")
|
|
40
|
+
.trim()
|
|
41
|
+
.replace(/[^\p{L}\p{N}._-]+/gu, "-")
|
|
42
|
+
.replace(/^-+|-+$/g, "")
|
|
43
|
+
.toLowerCase().replace(/^\.+$/, "") || "domain";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Business labels may be Chinese; directory identifiers are stable ASCII kebab-case.
|
|
47
|
+
function inferBusinessSlug(name) {
|
|
48
|
+
if (typeof name !== "string" || /[^\x00-\x7f]/.test(name)) return "";
|
|
49
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertBusinessSlug(value, field) {
|
|
53
|
+
if (typeof value !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
54
|
+
throw new Error(`${field} 必须为小写英文、数字及单连字符组成的目录标识(例如 order-center、refund);中文仅用于展示名称。旧中文路径任务请保留原数据,以英文标识新建任务。`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveBusinessSlug(value, name, field) {
|
|
60
|
+
return assertBusinessSlug(value ?? inferBusinessSlug(name), field);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Access is governed by the host Agent; accept paths on Windows, Linux and macOS.
|
|
64
|
+
function assertInsideUserSpace(targetPath) {
|
|
65
|
+
if (typeof targetPath !== "string" || !targetPath.trim()) throw new Error("路径不能为空");
|
|
66
|
+
return canonicalPath(targetPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function pathInfo(inputPath) {
|
|
70
|
+
if (!inputPath) return { exists: false };
|
|
71
|
+
const resolved = assertInsideUserSpace(inputPath);
|
|
72
|
+
try {
|
|
73
|
+
const stat = await fs.stat(resolved);
|
|
74
|
+
return { path: resolved, exists: true, directory: stat.isDirectory(), file: stat.isFile() };
|
|
75
|
+
} catch {
|
|
76
|
+
return { path: resolved, exists: false };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runCommand(command, args, options = {}) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
emit("log", { level: "info", message: `$ ${command} ${args.join(" ")}` });
|
|
83
|
+
const child = spawn(command, args, {
|
|
84
|
+
cwd: options.cwd || rootDir,
|
|
85
|
+
env: { ...process.env, ...options.env },
|
|
86
|
+
shell: false,
|
|
87
|
+
});
|
|
88
|
+
let output = "";
|
|
89
|
+
child.stdout.on("data", (chunk) => {
|
|
90
|
+
const text = chunk.toString();
|
|
91
|
+
output += text;
|
|
92
|
+
emit("log", { level: "stdout", message: text });
|
|
93
|
+
});
|
|
94
|
+
child.stderr.on("data", (chunk) => {
|
|
95
|
+
const text = chunk.toString();
|
|
96
|
+
emit("log", { level: "stderr", message: text });
|
|
97
|
+
});
|
|
98
|
+
child.on("error", reject);
|
|
99
|
+
child.on("close", (code) => {
|
|
100
|
+
if (code === 0) resolve(output);
|
|
101
|
+
else reject(new Error(`${command} 退出码 ${code}`));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runQuietCommand(command, args, options = {}) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const child = spawn(command, args, {
|
|
109
|
+
cwd: options.cwd || rootDir,
|
|
110
|
+
env: { ...process.env, ...options.env },
|
|
111
|
+
shell: false,
|
|
112
|
+
});
|
|
113
|
+
let output = "";
|
|
114
|
+
let errorOutput = "";
|
|
115
|
+
child.stdout.on("data", (chunk) => {
|
|
116
|
+
output += chunk.toString();
|
|
117
|
+
});
|
|
118
|
+
child.stderr.on("data", (chunk) => {
|
|
119
|
+
errorOutput += chunk.toString();
|
|
120
|
+
});
|
|
121
|
+
child.on("error", reject);
|
|
122
|
+
child.on("close", (code) => {
|
|
123
|
+
if (code === 0) resolve(options.raw ? output : output.trim());
|
|
124
|
+
else reject(new Error(errorOutput.trim() || `${command} 退出码 ${code}`));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function commandExists(command, args = ["--version"]) {
|
|
130
|
+
try {
|
|
131
|
+
await runCommand(command, args);
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function collectFiles(dir, extensions) {
|
|
139
|
+
const result = [];
|
|
140
|
+
async function walk(current) {
|
|
141
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (entry.name.startsWith(".")) continue;
|
|
144
|
+
const full = path.join(current, entry.name);
|
|
145
|
+
if (entry.isDirectory()) await walk(full);
|
|
146
|
+
else if (extensions.includes(path.extname(entry.name).toLowerCase())) result.push(full);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await walk(dir);
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeRepoPaths(payload) {
|
|
154
|
+
const rawPaths = Array.isArray(payload.repoDirs)
|
|
155
|
+
? [...payload.repoDirs]
|
|
156
|
+
: String(payload.repoDirs || "").split(/\r?\n/);
|
|
157
|
+
if (payload.repoDir) rawPaths.push(payload.repoDir);
|
|
158
|
+
return [...new Set(rawPaths.map((value) => String(value || "").trim()).filter(Boolean))];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function inspectRepository(repoDir) {
|
|
162
|
+
const resolved = assertInsideUserSpace(repoDir);
|
|
163
|
+
const info = await pathInfo(resolved);
|
|
164
|
+
if (!info.exists || !info.directory) throw new Error(`代码仓库目录不存在:${resolved}`);
|
|
165
|
+
let commit = "UNVERIFIED";
|
|
166
|
+
try {
|
|
167
|
+
commit = await runQuietCommand("git", ["rev-parse", "HEAD"], { cwd: resolved });
|
|
168
|
+
} catch {
|
|
169
|
+
emit("log", { level: "info", message: `未读取到 Git commit,按 UNVERIFIED 记录:${resolved}` });
|
|
170
|
+
}
|
|
171
|
+
let worktreeStatus = "unknown";
|
|
172
|
+
try {
|
|
173
|
+
const rawStatus = await runQuietCommand("git", ["-c", "core.quotepath=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: resolved, raw: true });
|
|
174
|
+
const entries = rawStatus.split("\0").filter(Boolean);
|
|
175
|
+
// Only omit our untracked output files. Tracked modifications always count.
|
|
176
|
+
const generatedRoots = [workspaceDir, path.join(process.cwd(), "code-knowledge")];
|
|
177
|
+
const relevant = entries.filter((entry) => {
|
|
178
|
+
if (!entry.startsWith("?? ")) return true;
|
|
179
|
+
const file = path.resolve(resolved, entry.slice(3));
|
|
180
|
+
return !generatedRoots.some((dir) => file === dir || file.startsWith(dir + path.sep));
|
|
181
|
+
});
|
|
182
|
+
worktreeStatus = relevant.length ? "dirty" : "clean";
|
|
183
|
+
const top = await runQuietCommand("git", ["rev-parse", "--show-toplevel"], { cwd: resolved });
|
|
184
|
+
if (await fs.realpath(top) !== await fs.realpath(resolved)) throw new Error("not repository root");
|
|
185
|
+
} catch { commit = "UNVERIFIED"; worktreeStatus = "unknown"; }
|
|
186
|
+
return { name: path.basename(resolved), path: resolved, commit, worktreeStatus };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildPrompt({ productName, productSlug, contractContext, existingExportDir, domainName, domainScope, convertedDocsDir, repositories, repoManifestPath, draftsDir, domainSlug, analysisPlanPath, evidenceDir }) {
|
|
190
|
+
const scopeText = domainScope || "未填写。请严格围绕本次业务域名称整理,遇到相邻业务域只记录依赖关系,不展开成主知识。";
|
|
191
|
+
const repoLines = repositories.map((repo) => `- ${repo.name}(commit: ${repo.commit}):${repo.contextPath};工作区:${repo.worktreeStatus || "unknown"}`).join("\n") || "- 未提供代码仓库";
|
|
192
|
+
const resolvedAnalysisPlanPath = analysisPlanPath || path.join(draftsDir, "analysis-plan.yaml");
|
|
193
|
+
const resolvedEvidenceDir = evidenceDir || path.join(draftsDir, "evidence");
|
|
194
|
+
return `你负责企业知识链路的知识生产层。请基于授权源码和业务资料,为「${productName}」的业务域「${domainName}」生成可长期维护、供产品/开发/测试共同使用的存量代码知识资产,供产品/开发/测试审核、在 Git 中管理,并由下游检索引用。
|
|
195
|
+
职责链路:源码与业务资料 → Code2Knowledge → Git 知识资产 → 文档站/RAG → MCP → 员工 Agent。
|
|
196
|
+
不承担 RAG、Embedding、MCP Server、员工权限或部署,不自动提交 Git、发布或修改被分析源码。导出不代表业务审核通过或员工可见。
|
|
197
|
+
|
|
198
|
+
材料目录(中心原料优先,源码按候选范围定向核验):
|
|
199
|
+
- 非代码资料转换结果:${convertedDocsDir}
|
|
200
|
+
- 多仓库清单(仅用于服务名、commit 和上下文文件映射):${repoManifestPath}
|
|
201
|
+
${repoLines}
|
|
202
|
+
|
|
203
|
+
原料使用边界(必须遵守):
|
|
204
|
+
- 第一阶段必须先读取上面列出的 repomix/context 文件和多仓库清单,用它们定位当前业务域的入口、事件、Job、服务和候选文件;不能跳过中心原料直接扫描源码。若原料注明 source-index 模式,上下文只包含文件索引,应按索引定向读取当前源码并核对 commit 和工作区状态;索引不等于源码快照。
|
|
205
|
+
- 第二阶段允许读取与候选调用链直接相关的原始源码,用于核实方法实现、调用关系、SQL/事务、配置、异常处理和测试证据;每次扩展都要说明扩展原因。
|
|
206
|
+
- 禁止从仓库根目录全量执行搜索或逐字扫描无关模块;禁止为了补充背景读取与当前业务域无调用关系的源码、codegraph 索引或其他仓库内容。
|
|
207
|
+
- 原始源码只能沿入口到核心服务、跨服务调用、消息、数据读写和相关测试定向扩展,为完成已授权的主流程可沿证据扩展必要依赖,记录扩展原因;范围外依赖最多保留一跳摘要,无法访问的环节标为未验证,不编造完整链路。
|
|
208
|
+
- repomix 与源码结论冲突时,以本次材料记录的 commit 对应源码为准,并在产物中记录冲突;commit 不一致或无法核验时不得标记 VERIFIED。
|
|
209
|
+
- 代码路径只能引用 repomix 或定向源码中已有的仓库内相对路径;不能将本机绝对路径写入知识资产。
|
|
210
|
+
|
|
211
|
+
输出目录:
|
|
212
|
+
- ${draftsDir}
|
|
213
|
+
- 分析计划(先生成并持续更新):${resolvedAnalysisPlanPath}
|
|
214
|
+
- 服务级证据目录(按服务保存结构化中间结果):${resolvedEvidenceDir}
|
|
215
|
+
|
|
216
|
+
执行中间产物(用于控制范围、减少重复读取和支持断点重跑):
|
|
217
|
+
- 先将候选入口、文件、依赖和排除范围写入 analysis-plan.yaml,再开始深读源码。
|
|
218
|
+
- 每个服务将分析结果写入 evidence/<service>.yaml;不要让多个分析任务直接同时修改最终知识文档。
|
|
219
|
+
- 汇总阶段优先读取 analysis-plan.yaml 和 evidence/*.yaml,仅对缺失证据定向补读源码。
|
|
220
|
+
- 已有且 commit 未变化的服务证据可复用;发现源码或范围变化时,只更新受影响的服务证据。
|
|
221
|
+
|
|
222
|
+
分析中间产物:
|
|
223
|
+
- 计划文件:${draftsDir}/analysis-plan.yaml
|
|
224
|
+
- 服务级证据目录:${draftsDir}/evidence/
|
|
225
|
+
|
|
226
|
+
通用导出目录结构:
|
|
227
|
+
- code-knowledge/<product>/<domain>/
|
|
228
|
+
- 本次产品或业务中心:${productName}
|
|
229
|
+
- 本次业务域:${domainName}
|
|
230
|
+
- 本次业务域边界:${scopeText}
|
|
231
|
+
- 已有导出目录(仅只读检查已有稳定 ID,不自动覆盖):${existingExportDir || "未指定"}
|
|
232
|
+
|
|
233
|
+
默认产物采用“本体约束 ontology + 业务语义 domains + 代码事实 graph + 业务代码桥接 mappings”的四层结构:
|
|
234
|
+
|
|
235
|
+
1. domains/overview.md
|
|
236
|
+
- 业务域做什么、解决什么问题、主要角色、模块关系
|
|
237
|
+
|
|
238
|
+
2. domains/flows.md
|
|
239
|
+
- 核心流程,包含入口、前置条件、核心步骤、状态变化、失败/回滚场景
|
|
240
|
+
|
|
241
|
+
3. domains/pitfalls.md
|
|
242
|
+
- 历史问题、已知风险、复杂调用链、SQL/事务/兼容风险
|
|
243
|
+
|
|
244
|
+
4. domains/glossary.yaml
|
|
245
|
+
- 术语表
|
|
246
|
+
|
|
247
|
+
5. domains/concepts.yaml
|
|
248
|
+
- 核心业务概念;type 必须使用 ontology/concept-types.yaml 定义的 11 类之一
|
|
249
|
+
- domain_entity:有业务身份和生命周期的核心对象
|
|
250
|
+
- relation_entity:可独立维护的对象间业务关系
|
|
251
|
+
- state_entity:业务对象当前可判断、可迁移的状态
|
|
252
|
+
- process_entity:由多个步骤组成的业务过程
|
|
253
|
+
- process_state:异步流程、任务或审批的阶段状态
|
|
254
|
+
- value_object:没有独立身份、由属性值定义的概念
|
|
255
|
+
- business_identifier:稳定识别业务对象的编码或组合键
|
|
256
|
+
- business_rule:被多个能力共享的规则性概念
|
|
257
|
+
- async_artifact:消息、事件、任务记录或中转数据
|
|
258
|
+
- config_entity:影响业务行为的开关、阈值、字典或配置
|
|
259
|
+
- external_system:业务语义层依赖或同步的外部平台
|
|
260
|
+
|
|
261
|
+
6. domains/capabilities.yaml
|
|
262
|
+
- 业务能力,每个能力至少关联概念、规则或代码入口
|
|
263
|
+
|
|
264
|
+
7. domains/rules.yaml
|
|
265
|
+
- 业务规则,必须区分 VERIFIED / UNVERIFIED,重要结论标注来源
|
|
266
|
+
|
|
267
|
+
8. graph/curated/${domainSlug}.graph.yaml
|
|
268
|
+
- 代码事实层,节点仅限 Service / Controller / ServiceClass / Repository / Mapper / Table / Entity / Convertor / Enum / FeignClient / MQTopic / Consumer / Job / Util / ExternalSystem / BusinessDomain
|
|
269
|
+
- 边仅限 invokes / calls / feign_calls / reads / writes / references / converts / publishes / consumes / syncs_to / maps / belongs_to / triggers
|
|
270
|
+
|
|
271
|
+
9. mappings/${domainSlug}.mapping.yaml
|
|
272
|
+
- 桥接层,把 capability/rule/flow 映射到代码节点,evidence_refs 引用权威条目的 evidence 数组索引,不重复维护证据
|
|
273
|
+
|
|
274
|
+
10. domains/flows.yaml 和 domains/claims.yaml
|
|
275
|
+
- flows 为权威流程定义,flows.md 为可检索摘要;claims 仅存其他文件无法容纳的风险/独立结论,不复制已有规则和能力
|
|
276
|
+
|
|
277
|
+
知识契约 2.0(与生成骨架及 references/knowledge-model.md 一致):
|
|
278
|
+
- 业务 Markdown 必须有 YAML frontmatter,业务 YAML 用顶层 meta;ontology 标识 type_constraint,维护资料标识 maintenance。
|
|
279
|
+
- 元数据最低含 schema_version/knowledge_id/product/domain/systems/knowledge_type/generated_at/review_status/owner/sources;跨系统 sources 必须逐仓保留完整 commit 和分析时工作区状态;未知 commit 为 null。owner 未知为 null。
|
|
280
|
+
- 本次可用的程序生成元数据上下文(不能编造时间或来源):${JSON.stringify(contractContext || { product: productSlug, domain: domainSlug, sources: repositories.map((r) => ({ repository: r.name, commit: r.commit, worktree_status: r.worktreeStatus || "unknown" })) })}
|
|
281
|
+
- 已有 knowledge_id 和业务 ID 在移动/重新生成时保留;更新前只读检查已有导出资产,复用其稳定 ID,不复制旧审核结论。
|
|
282
|
+
- claim_type 区分 code_fact/business_rule/inference;status 仅为代码证据 VERIFIED/UNVERIFIED;inference 不得 VERIFIED;业务资料不能单独证明代码已实现。
|
|
283
|
+
- review_status 默认 pending,Agent 不得自动 confirmed/rejected、虚构审核人或 owner。只有用户提供真实审核记录时才可记录;审核需 reviewer/reviewed_at/basis/content_sha256,内容或来源变化须重新审核。
|
|
284
|
+
- 文档使用 claim_refs 引用规则/能力/流程/独立结论;逐条表达核验差异,不能用整篇 confidence 掩盖未核实项。每个可检索小节保留业务对象、条件、行为与 ID,不只写“按上述规则”。
|
|
285
|
+
- evidence 必须整理进最终业务条目或图谱,禁止只引用内部 EVIDENCE ID、evidence/ 路径、提示词或临时目录。最终资产不含机器绝对路径、凭证或认证仓库 URL。
|
|
286
|
+
- 每条结论声明 applicability.conditions/versions/limitations;UNVERIFIED 必须说明限制;needs_review/deprecated 不作为当前 VERIFIED。删项先检查引用并记录 Git 差异,不静默删除历史知识。
|
|
287
|
+
- 结构校验只证明程序检查项满足,不能证明调用语义、业务正确性、人工身份或测试真实通过。导出可包含 pending/UNVERIFIED,但必须清楚标记限制。
|
|
288
|
+
|
|
289
|
+
要求:
|
|
290
|
+
- 使用中文。
|
|
291
|
+
- 不要大段复制源码。
|
|
292
|
+
- 不要虚构业务规则,无法确认的标注“需确认”。
|
|
293
|
+
- 区分“代码事实”和“业务推断”。
|
|
294
|
+
- 先识别业务语义,再选择最具体的 concept type;不要用 BusinessDomain、Java 类名或代码节点类型代替业务概念类型。
|
|
295
|
+
- 每个 concept 必须包含 id、name、type、desc、related、status;related 只引用当前域已定义概念,跨域引用必须明确登记来源。
|
|
296
|
+
- 同一业务概念不要仅因分布在多个服务而重复创建;服务归属进入 graph,业务到代码的联系进入 mappings。
|
|
297
|
+
- 严格围绕本次业务域边界整理;相邻业务域只作为依赖、上下游或排除项说明。
|
|
298
|
+
- 采用“范围优先、证据扩展”的分析策略:先从业务域入口(Controller/API/Event/Job)、入口调用到 Service/Domain Service,再追踪 Feign/MQ、Repository/Mapper、Table/Entity 和外部系统;只有当调用链或规则需要时,才补读配置、SQL、转换器、枚举和测试。
|
|
299
|
+
- 分两阶段执行:阶段一只定向定位入口、事件、Job 及其直接调用,先产出候选文件清单;阶段二仅沿候选链路递归读取,按已授权业务边界扩展到必要跨服务依赖、读写表、异常/事务和相关测试。
|
|
300
|
+
- 必须先更新 analysis-plan.yaml,再按服务或证据类型生成 evidence/*.yaml;最终知识文档只能由主任务汇总 evidence 生成。
|
|
301
|
+
- 按服务分析;仅当用户授权且宿主支持时使用子任务。子任务只写各自 evidence,最终产物由主任务汇总。
|
|
302
|
+
- 子任务结果必须包含 service、entry_points、call_chains、data_access、events、rules、tests、unverified 和 evidence 字段;重复概念由主任务统一合并。
|
|
303
|
+
- 不要从头到尾逐字阅读所有仓库原料,也不要为了补充背景扫描与本业务域无调用关系的模块;优先使用多仓库清单和定向搜索定位文件,再读取命中的上下文。
|
|
304
|
+
- 每条主流程至少闭环到“入口→核心服务→数据读写/跨服务调用→状态或结果→异常/回滚”;完成主链路后执行一次遗漏检查,确认未漏掉 Feign、MQ、定时任务、事务和测试证据。
|
|
305
|
+
- 相邻模块只读取用于确认边界或依赖的最小上下文;无法证明与本域相关时,记录为排除项或需确认,不扩展为主知识。
|
|
306
|
+
- 流程需交代触发条件、前置状态、主要步骤、数据变化、异步事件、失败与补偿及适用范围。分别记录测试被发现、被阅读、实际执行及结果;没有测试时声明覆盖限制,不因此否认已有源码可证明的局部事实。无调用证据的模块列入排除项,范围外依赖最多保留 1 跳摘要。
|
|
307
|
+
- 已核验关键结论至少附 kind: code、repository、完整 commit、仓库相对 path、symbol、有效 lines、supports;方法名、注释、索引命中、单个调用点、未证实启用分支、配置默认值和未核实跨服务关系不足以证明业务事实。
|
|
308
|
+
- graph 中带 path 的节点必须是真实文件路径;不确定时不要写入事实层,改写入 UNVERIFIED 或 pitfalls。
|
|
309
|
+
- path 格式统一为 <仓库名>/<仓库内相对路径>,跨服务同名节点以仓库名消歧。
|
|
310
|
+
- meta.sources 逐仓保存真实完整 commit 与工作区状态;兼容字段 meta.verified_commits 只记录来源为 clean 且具有完整 commit 的仓库,未知不能声称已验证。
|
|
311
|
+
- 规则、能力、流程与关键结论采用统一 claim_type/status/review_status/evidence/applicability/lifecycle。规则的详细定义只维护于 domains/rules.yaml,其他文件通过稳定 ID 引用。
|
|
312
|
+
- MQ 的 publishes/consumes 应成对核查;外部系统使用 EXT: 前缀,MQ 使用 MQ: 前缀。
|
|
313
|
+
- mappings 必须让业务问题能一跳定位到代码节点。
|
|
314
|
+
- 每个 VERIFIED capability 至少映射一个代码节点,每条 VERIFIED high 规则必须有 rule_mapping;业务资料规则可以 UNVERIFIED 保留,不能伪造实现映射。
|
|
315
|
+
- 先定边界,再沿入口调用、跨服务 Feign、MQ、数据表、外部系统四条主线抽取,最后检查遗漏闭环。
|
|
316
|
+
- 面向产品写清业务口径,面向开发写清调用与数据事实,面向测试写清规则、状态迁移、异常和回归点。`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const excludedDirs = new Set([".git", "node_modules", "vendor", "dist", "build", "target", ".venv", "venv", ".code2knowledge"]);
|
|
320
|
+
async function writeSourceIndex(repo, outputPath) {
|
|
321
|
+
let files;
|
|
322
|
+
if (repo.commit !== "UNVERIFIED") {
|
|
323
|
+
files = (await runQuietCommand("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: repo.path }))
|
|
324
|
+
.split("\0").filter(Boolean);
|
|
325
|
+
} else {
|
|
326
|
+
files = [];
|
|
327
|
+
async function walk(dir) {
|
|
328
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
329
|
+
if (entry.isSymbolicLink() || excludedDirs.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
330
|
+
const full = path.join(dir, entry.name);
|
|
331
|
+
if (entry.isDirectory()) await walk(full);
|
|
332
|
+
else if (entry.isFile()) files.push(path.relative(repo.path, full).split(path.sep).join("/"));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
await walk(repo.path);
|
|
336
|
+
}
|
|
337
|
+
files = [...new Set(files)].filter((file) => {
|
|
338
|
+
const full = path.resolve(repo.path, file);
|
|
339
|
+
if ([workspaceDir, path.join(process.cwd(), "code-knowledge")].some((dir) => full === dir || full.startsWith(dir + path.sep))) return false;
|
|
340
|
+
return !file.split("/").some((part) => excludedDirs.has(part));
|
|
341
|
+
}).sort();
|
|
342
|
+
await fs.writeFile(outputPath, `# ${repo.name} source-index\n\n模式:source-index(仅文件索引,不是源码快照)\n仓库:${repo.path}\ncommit:${repo.commit}\n工作区:${repo.worktreeStatus}\n\n按业务域从索引定位入口,定向读取源码;dirty/unknown 时不得把工作区当成该 commit 的已验证事实。\n\n## 文件清单\n${files.map((file) => JSON.stringify(file)).join("\n")}\n`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function checkMaterial(materialDir) {
|
|
346
|
+
const material = await readMaterial(materialDir);
|
|
347
|
+
const repositories = [];
|
|
348
|
+
for (const recorded of material.repositories) {
|
|
349
|
+
try {
|
|
350
|
+
const current = await inspectRepository(recorded.path);
|
|
351
|
+
const matches = current.commit !== "UNVERIFIED" && current.commit === recorded.commit &&
|
|
352
|
+
current.worktreeStatus === "clean" && recorded.worktreeStatus === "clean";
|
|
353
|
+
repositories.push({ name: recorded.name, recordedCommit: recorded.commit, currentCommit: current.commit,
|
|
354
|
+
worktreeStatus: current.worktreeStatus, matches });
|
|
355
|
+
} catch (error) { repositories.push({ name: recorded.name, matches: false, error: error.message }); }
|
|
356
|
+
}
|
|
357
|
+
const documents = [];
|
|
358
|
+
for (const doc of material.documents || []) {
|
|
359
|
+
let matches = false;
|
|
360
|
+
try { matches = sha256(await fs.readFile(path.join(material.rawDocsDir, doc.path))) === doc.sha256; } catch { /* Missing source is stale. */ }
|
|
361
|
+
documents.push({ document_id: doc.document_id, path: doc.path, matches });
|
|
362
|
+
}
|
|
363
|
+
if (material.rawDocsDir && Array.isArray(material.documents)) {
|
|
364
|
+
try {
|
|
365
|
+
for (const file of await collectFiles(material.rawDocsDir, documentExtensions)) {
|
|
366
|
+
const relative = path.relative(material.rawDocsDir, file).split(path.sep).join("/");
|
|
367
|
+
if (!material.documents.some((d) => d.path === relative)) documents.push({ path: relative, matches: false, reason: "新增资料" });
|
|
368
|
+
}
|
|
369
|
+
} catch { documents.push({ path: "资料目录", matches: false, reason: "不可读取" }); }
|
|
370
|
+
}
|
|
371
|
+
return { materialDir: material.materialDir, reusable: repositories.every((repo) => repo.matches) && documents.every((doc) => doc.matches), repositories, documents };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function writeDraftTemplates(draftsDir, domainName, domainSlug, verifiedCommits = {}, options = {}) {
|
|
375
|
+
await fs.mkdir(draftsDir, { recursive: true });
|
|
376
|
+
const domainDir = path.join(draftsDir, "domains");
|
|
377
|
+
await fs.cp(path.join(projectContextTemplateDir, "ontology"), path.join(draftsDir, "ontology"), {
|
|
378
|
+
recursive: true,
|
|
379
|
+
force: false,
|
|
380
|
+
errorOnExist: false,
|
|
381
|
+
});
|
|
382
|
+
await fs.cp(path.join(projectContextTemplateDir, "rules"), path.join(draftsDir, "rules"), {
|
|
383
|
+
recursive: true,
|
|
384
|
+
force: false,
|
|
385
|
+
errorOnExist: false,
|
|
386
|
+
});
|
|
387
|
+
await fs.mkdir(path.join(draftsDir, "graph", "curated"), { recursive: true });
|
|
388
|
+
await fs.mkdir(path.join(draftsDir, "mappings"), { recursive: true });
|
|
389
|
+
const evidenceDir = path.join(draftsDir, "evidence");
|
|
390
|
+
await fs.mkdir(evidenceDir, { recursive: true });
|
|
391
|
+
await fs.mkdir(domainDir, { recursive: true });
|
|
392
|
+
|
|
393
|
+
const repositories = Array.isArray(options.repositories) ? options.repositories : [];
|
|
394
|
+
const generatedAt = new Date().toISOString();
|
|
395
|
+
const serviceNames = [...new Set([
|
|
396
|
+
...Object.keys(verifiedCommits || {}),
|
|
397
|
+
...repositories.map((repo) => String(repo.name || "").trim()).filter(Boolean),
|
|
398
|
+
])];
|
|
399
|
+
const analysisPlan = `version: 1\nstatus: pending\ngenerated_at: ${JSON.stringify(generatedAt)}\ndomain: ${JSON.stringify(domainSlug)}\ndomain_name: ${JSON.stringify(domainName)}\nlimits:\n outside_scope_dependency_hops: 1\n authorized_main_flow: evidence_driven\n source_scan: targeted_only\n require_commit_match: true\nphases:\n - id: plan\n status: pending\n output: analysis-plan.yaml\n - id: evidence\n status: pending\n output: evidence/<service>.yaml\n - id: synthesis\n status: pending\n output: domains/ and graph/ and mappings/\ncandidate_services:\n${serviceNames.map((name) => ` - ${JSON.stringify(name)}`).join("\n") || " []"}\ncandidate_entries: []\ncandidate_files: []\ncross_service_dependencies: []\nexcluded_modules: []\nopen_questions: []\nevidence_files:\n${serviceNames.map((name) => ` - ${JSON.stringify(`evidence/${name}.yaml`)}`).join("\n") || " []"}\nnotes: []\n`;
|
|
400
|
+
const analysisPlanPath = path.join(draftsDir, "analysis-plan.yaml");
|
|
401
|
+
if (!fsSync.existsSync(analysisPlanPath)) await fs.writeFile(analysisPlanPath, analysisPlan);
|
|
402
|
+
for (const serviceName of serviceNames) {
|
|
403
|
+
const evidencePath = path.join(evidenceDir, `${serviceName}.yaml`);
|
|
404
|
+
if (fsSync.existsSync(evidencePath)) continue;
|
|
405
|
+
const commit = verifiedCommits?.[serviceName] || repositories.find((repo) => repo.name === serviceName)?.commit || "UNVERIFIED";
|
|
406
|
+
const contextPath = repositories.find((repo) => repo.name === serviceName)?.contextPath || "";
|
|
407
|
+
await fs.writeFile(evidencePath, `version: 1\nstatus: pending\nservice: ${JSON.stringify(serviceName)}\ncommit: ${JSON.stringify(commit)}\ncontext_path: ${JSON.stringify(contextPath)}\nanalysis_plan: ../analysis-plan.yaml\nentry_points: []\ncall_chains: []\ndata_access: []\nevents: []\nrules: []\ntests: []\nunverified: []\nevidence: []\nexcluded: []\n`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const context = options.contractContext || makeContext({ product: options.productSlug || "unassigned", domain: domainSlug,
|
|
411
|
+
repositories: repositories.length ? repositories : Object.entries(verifiedCommits).map(([name, commit]) => ({ name, commit, worktreeStatus: "unknown" })) });
|
|
412
|
+
await writeBusinessTemplates(draftsDir, domainName, context);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function convertDocs(rawDocsDir, convertedDocsDir) {
|
|
416
|
+
await fs.mkdir(convertedDocsDir, { recursive: true });
|
|
417
|
+
const files = await collectFiles(rawDocsDir, [".docx", ".doc", ".pdf", ".xlsx", ".xls", ".pptx", ".ppt", ".html", ".htm"]);
|
|
418
|
+
for (const file of await collectFiles(rawDocsDir, [".md", ".txt", ".csv", ".yaml", ".yml", ".json"])) {
|
|
419
|
+
const out = path.join(convertedDocsDir, path.relative(rawDocsDir, file));
|
|
420
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
421
|
+
await fs.copyFile(file, out);
|
|
422
|
+
}
|
|
423
|
+
if (files.length === 0) {
|
|
424
|
+
emit("log", { level: "info", message: "未发现需要 MarkItDown 转换的非代码资料。" });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const hasMarkitdown = await commandExists("markitdown", ["--version"]);
|
|
428
|
+
if (!hasMarkitdown) throw new Error("未找到 markitdown 命令,请先安装:pip install markitdown");
|
|
429
|
+
for (const file of files) {
|
|
430
|
+
const relative = path.relative(rawDocsDir, file);
|
|
431
|
+
const out = path.join(convertedDocsDir, `${relative}.md`);
|
|
432
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
433
|
+
const markdown = await runCommand("markitdown", [file]);
|
|
434
|
+
await fs.writeFile(out, markdown);
|
|
435
|
+
emit("log", { level: "info", message: `已转换:${file} -> ${out}` });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function runRepomix(repoDir, outputPath) {
|
|
440
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
441
|
+
await runCommand("npx", ["-y", "repomix", repoDir, "--output", outputPath], { cwd: repoDir });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function exportKnowledgeAssets(draftsDir, exportDir) {
|
|
445
|
+
const validation = await validateKnowledgeAssets(draftsDir);
|
|
446
|
+
if (!validation.valid) throw new Error(`知识资产校验失败:${validation.errors.join(";")}`);
|
|
447
|
+
const currentFiles = new Set(await assetFiles(draftsDir));
|
|
448
|
+
for (const file of await assetFiles(exportDir)) {
|
|
449
|
+
if (!currentFiles.has(file)) throw new Error(`导出目标仍有本次草稿未包含的旧资产:${file};请明确保留并标记 deprecated,或授权处理旧文件后重试,工具不会静默删除`);
|
|
450
|
+
}
|
|
451
|
+
await fs.mkdir(exportDir, { recursive: true });
|
|
452
|
+
const assetDirs = ["ontology", "domains", "graph", "mappings", "rules"];
|
|
453
|
+
const copied = [];
|
|
454
|
+
for (const dirName of assetDirs) {
|
|
455
|
+
const source = path.join(draftsDir, dirName);
|
|
456
|
+
if (!fsSync.existsSync(source)) continue;
|
|
457
|
+
const target = path.join(exportDir, dirName);
|
|
458
|
+
await fs.cp(source, target, { recursive: true, force: true });
|
|
459
|
+
copied.push(dirName);
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
copied,
|
|
463
|
+
skipped: ["AI_PROMPT.md", "analysis-plan.yaml", "evidence"],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function assertRunDir(targetPath) {
|
|
468
|
+
const resolved = assertInsideUserSpace(targetPath);
|
|
469
|
+
const root = path.resolve(runRootDir);
|
|
470
|
+
if (resolved === root || !resolved.startsWith(root + path.sep)) {
|
|
471
|
+
throw new Error(`运行目录必须位于工作区 runs 下:${resolved}`);
|
|
472
|
+
}
|
|
473
|
+
return resolved;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function readRun(runDir) {
|
|
477
|
+
const resolved = assertRunDir(runDir);
|
|
478
|
+
const manifestPath = path.join(resolved, "run.json");
|
|
479
|
+
try {
|
|
480
|
+
const run = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
481
|
+
const draftsDir = path.resolve(String(run.draftsDir || ""));
|
|
482
|
+
if (!run.runId || !run.productSlug || !run.domainSlug || !run.draftsDir) throw new Error("字段不完整");
|
|
483
|
+
if ([run.productSlug, run.domainSlug].some((slug) => typeof slug !== "string" || slugify(slug) !== slug)) throw new Error("业务域标识无效");
|
|
484
|
+
if (draftsDir !== path.join(resolved, "drafts")) throw new Error("草稿目录不属于当前任务");
|
|
485
|
+
return { ...run, draftsDir, runDir: resolved, manifestPath };
|
|
486
|
+
} catch {
|
|
487
|
+
throw new Error(`无效的业务域任务,缺少或无法读取 run.json:${resolved}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function updateRun(runDir, changes, eventType) {
|
|
492
|
+
const run = await readRun(runDir);
|
|
493
|
+
const now = new Date().toISOString();
|
|
494
|
+
const updated = {
|
|
495
|
+
...run,
|
|
496
|
+
...changes,
|
|
497
|
+
updatedAt: now,
|
|
498
|
+
lifecycle: [
|
|
499
|
+
...(Array.isArray(run.lifecycle) ? run.lifecycle : []),
|
|
500
|
+
...(eventType ? [{ type: eventType, at: now }] : []),
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
delete updated.runDir;
|
|
504
|
+
delete updated.manifestPath;
|
|
505
|
+
const manifestPath = path.join(runDir, "run.json");
|
|
506
|
+
await fs.writeFile(manifestPath, JSON.stringify(updated, null, 2) + "\n");
|
|
507
|
+
return { ...updated, runDir, manifestPath };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function validateKnowledgeAssets(draftsDir, options = {}) {
|
|
511
|
+
const resolved = assertInsideUserSpace(draftsDir);
|
|
512
|
+
const required = [
|
|
513
|
+
"ontology/node-types.yaml",
|
|
514
|
+
"ontology/relation-types.yaml",
|
|
515
|
+
"ontology/concept-types.yaml",
|
|
516
|
+
"rules/validation-rules.yaml",
|
|
517
|
+
];
|
|
518
|
+
const errors = [];
|
|
519
|
+
const warnings = [];
|
|
520
|
+
const yamlDocuments = new Map();
|
|
521
|
+
let domainSlugs = [];
|
|
522
|
+
try {
|
|
523
|
+
domainSlugs = (await fs.readdir(path.join(resolved, "graph", "curated"), { withFileTypes: true }))
|
|
524
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".graph.yaml"))
|
|
525
|
+
.map((entry) => entry.name.slice(0, -".graph.yaml".length));
|
|
526
|
+
} catch {
|
|
527
|
+
errors.push("缺少 graph/curated 目录");
|
|
528
|
+
}
|
|
529
|
+
if (domainSlugs.length === 0 && errors.length === 0) errors.push("graph/curated 目录中没有业务域图谱");
|
|
530
|
+
for (const domainSlug of domainSlugs) {
|
|
531
|
+
required.push(
|
|
532
|
+
`domains/overview.md`,
|
|
533
|
+
`domains/flows.md`,
|
|
534
|
+
`domains/pitfalls.md`,
|
|
535
|
+
`domains/glossary.yaml`,
|
|
536
|
+
`domains/concepts.yaml`,
|
|
537
|
+
`domains/capabilities.yaml`,
|
|
538
|
+
`domains/rules.yaml`,
|
|
539
|
+
`domains/flows.yaml`,
|
|
540
|
+
`domains/claims.yaml`,
|
|
541
|
+
`graph/curated/${domainSlug}.graph.yaml`,
|
|
542
|
+
`mappings/${domainSlug}.mapping.yaml`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
for (const relativePath of required) {
|
|
546
|
+
try {
|
|
547
|
+
const content = await fs.readFile(path.join(resolved, relativePath), "utf8");
|
|
548
|
+
if (!content.trim()) errors.push(`文件为空:${relativePath}`);
|
|
549
|
+
else {
|
|
550
|
+
if (/需补充|待补充/.test(content)) errors.push(`仍包含待补充内容:${relativePath}`);
|
|
551
|
+
if (/\.ya?ml$/i.test(relativePath)) {
|
|
552
|
+
try {
|
|
553
|
+
yamlDocuments.set(relativePath, parseYaml(content));
|
|
554
|
+
} catch (error) {
|
|
555
|
+
errors.push(`YAML 语法错误:${relativePath}(${error.message.split("\n")[0]})`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
errors.push(`缺少文件:${relativePath}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const arrayKeys = {
|
|
565
|
+
"ontology/node-types.yaml": ["node_types"],
|
|
566
|
+
"ontology/relation-types.yaml": ["relation_types"],
|
|
567
|
+
"ontology/concept-types.yaml": ["concept_types"],
|
|
568
|
+
"rules/validation-rules.yaml": ["validation_rules"],
|
|
569
|
+
"domains/concepts.yaml": ["concepts"],
|
|
570
|
+
"domains/capabilities.yaml": ["capabilities"],
|
|
571
|
+
"domains/rules.yaml": ["rules"],
|
|
572
|
+
"domains/glossary.yaml": ["terms"],
|
|
573
|
+
};
|
|
574
|
+
for (const domain of domainSlugs) {
|
|
575
|
+
arrayKeys[`graph/curated/${domain}.graph.yaml`] = ["nodes", "edges"];
|
|
576
|
+
arrayKeys[`mappings/${domain}.mapping.yaml`] = ["capability_mappings", "rule_mappings", "entry_points"];
|
|
577
|
+
}
|
|
578
|
+
for (const [file, keys] of Object.entries(arrayKeys)) {
|
|
579
|
+
if (!yamlDocuments.has(file)) continue;
|
|
580
|
+
const doc = yamlDocuments.get(file);
|
|
581
|
+
for (const key of keys) {
|
|
582
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc) || !Array.isArray(doc[key]) ||
|
|
583
|
+
doc[key].some((item) => !item || typeof item !== "object" || Array.isArray(item))) {
|
|
584
|
+
errors.push(`YAML 结构错误:${file}.${key} 必须为对象列表`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (errors.some((error) => error.startsWith("YAML 结构错误"))) {
|
|
589
|
+
return { valid: false, checkedAt: new Date().toISOString(), draftsDir: resolved, domainSlugs,
|
|
590
|
+
checkedFiles: required.length, errors, warnings };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const nodeDefinitions = yamlDocuments.get("ontology/node-types.yaml")?.node_types || [];
|
|
594
|
+
const relationDefinitions = yamlDocuments.get("ontology/relation-types.yaml")?.relation_types || [];
|
|
595
|
+
const conceptDefinitions = yamlDocuments.get("ontology/concept-types.yaml")?.concept_types || [];
|
|
596
|
+
const nodeTypes = new Map(nodeDefinitions.map((item) => [item.type, item]));
|
|
597
|
+
const relationTypes = new Map(relationDefinitions.map((item) => [item.rel, item]));
|
|
598
|
+
const conceptTypes = new Set(conceptDefinitions.map((item) => item.type));
|
|
599
|
+
|
|
600
|
+
for (const domainSlug of domainSlugs) {
|
|
601
|
+
const concepts = yamlDocuments.get(`domains/concepts.yaml`)?.concepts || [];
|
|
602
|
+
const capabilities = yamlDocuments.get(`domains/capabilities.yaml`)?.capabilities || [];
|
|
603
|
+
const businessRules = yamlDocuments.get(`domains/rules.yaml`)?.rules || [];
|
|
604
|
+
const graph = yamlDocuments.get(`graph/curated/${domainSlug}.graph.yaml`) || {};
|
|
605
|
+
const mapping = yamlDocuments.get(`mappings/${domainSlug}.mapping.yaml`) || {};
|
|
606
|
+
const conceptIds = new Set(concepts.map((item) => item.id).filter(Boolean));
|
|
607
|
+
const capabilityIds = new Set(capabilities.map((item) => item.id).filter(Boolean));
|
|
608
|
+
const nodeById = new Map();
|
|
609
|
+
const ruleIds = new Set(businessRules.map((item) => item.id));
|
|
610
|
+
for (const [kind, items] of [["concept", concepts], ["capability", capabilities], ["rule", businessRules]]) {
|
|
611
|
+
const ids = new Set();
|
|
612
|
+
for (const item of items) {
|
|
613
|
+
if (!item.id || ids.has(item.id)) errors.push(`${kind} id 缺失或重复:${item.id || domainSlug}`);
|
|
614
|
+
ids.add(item.id);
|
|
615
|
+
if (!["VERIFIED", "UNVERIFIED"].includes(item.status)) errors.push(`${kind} status 必须为 VERIFIED 或 UNVERIFIED:${item.id}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
for (const concept of concepts) {
|
|
620
|
+
if (!concept.id || !concept.name || !concept.type || !concept.desc) errors.push(`业务概念缺少必填字段:${domainSlug}`);
|
|
621
|
+
if (concept.type && !conceptTypes.has(concept.type)) errors.push(`未定义的业务概念类型:${concept.type}`);
|
|
622
|
+
for (const relatedId of Array.isArray(concept.related) ? concept.related : []) {
|
|
623
|
+
if (!conceptIds.has(relatedId)) warnings.push(`跨域概念引用需由新契约检查登记来源:${relatedId}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const capability of capabilities) {
|
|
627
|
+
const linkedConcepts = Array.isArray(capability.concepts) ? capability.concepts : [];
|
|
628
|
+
if (linkedConcepts.length === 0) errors.push(`业务能力未关联概念:${capability.id || domainSlug}`);
|
|
629
|
+
for (const conceptId of linkedConcepts) {
|
|
630
|
+
if (!conceptIds.has(conceptId)) errors.push(`业务能力引用不存在的概念:${capability.id || domainSlug} -> ${conceptId}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
for (const node of Array.isArray(graph.nodes) ? graph.nodes : []) {
|
|
634
|
+
if (!node.id || !node.type) {
|
|
635
|
+
errors.push(`图谱节点缺少 id 或 type:${domainSlug}`);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (nodeById.has(node.id)) errors.push(`图谱节点 id 重复:${node.id}`);
|
|
639
|
+
nodeById.set(node.id, node);
|
|
640
|
+
const definition = nodeTypes.get(node.type);
|
|
641
|
+
if (!definition) errors.push(`未定义的图谱节点类型:${node.type}`);
|
|
642
|
+
for (const property of definition?.required || []) {
|
|
643
|
+
if (node[property] === undefined || node[property] === null || node[property] === "") errors.push(`图谱节点缺少必填属性:${node.id}.${property}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
for (const edge of Array.isArray(graph.edges) ? graph.edges : []) {
|
|
647
|
+
const definition = relationTypes.get(edge.rel);
|
|
648
|
+
const fromNode = nodeById.get(edge.from);
|
|
649
|
+
const toNode = nodeById.get(edge.to);
|
|
650
|
+
if (!definition) errors.push(`未定义的图谱关系:${edge.rel || "<empty>"}`);
|
|
651
|
+
if (!fromNode) errors.push(`图谱关系起点不存在:${edge.from || "<empty>"}`);
|
|
652
|
+
if (!toNode) errors.push(`图谱关系终点不存在:${edge.to || "<empty>"}`);
|
|
653
|
+
if (definition && fromNode && !definition.from?.includes(fromNode.type)) errors.push(`图谱关系起点类型不匹配:${edge.rel} ${fromNode.type}`);
|
|
654
|
+
if (definition && toNode && !definition.to?.includes(toNode.type)) errors.push(`图谱关系终点类型不匹配:${edge.rel} ${toNode.type}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const capabilityMappings = Array.isArray(mapping.capability_mappings) ? mapping.capability_mappings : [];
|
|
658
|
+
const ruleMappings = Array.isArray(mapping.rule_mappings) ? mapping.rule_mappings : [];
|
|
659
|
+
const mappedCapabilities = new Set();
|
|
660
|
+
for (const item of capabilityMappings) {
|
|
661
|
+
const capabilityId = item.capability_id || item.capability || item.id;
|
|
662
|
+
if (capabilityId) mappedCapabilities.add(capabilityId);
|
|
663
|
+
if (capabilityId && !capabilityIds.has(capabilityId)) errors.push(`映射引用不存在的业务能力:${capabilityId}`);
|
|
664
|
+
const graphNodes = item.graph_nodes || item.nodes || item.code_nodes || [];
|
|
665
|
+
if (!Array.isArray(graphNodes) || graphNodes.length === 0) errors.push(`业务能力映射缺少代码节点:${capabilityId || domainSlug}`);
|
|
666
|
+
for (const nodeId of Array.isArray(graphNodes) ? graphNodes : []) {
|
|
667
|
+
if (!nodeById.has(nodeId)) errors.push(`业务能力映射引用不存在的图谱节点:${nodeId}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const capabilityId of capabilityIds) {
|
|
671
|
+
if (capabilities.find((item) => item.id === capabilityId)?.status === "VERIFIED" && !mappedCapabilities.has(capabilityId)) errors.push(`业务能力缺少代码映射:${capabilityId}`);
|
|
672
|
+
}
|
|
673
|
+
const mappedRules = new Set(ruleMappings.map((item) => item.rule_id || item.rule || item.id).filter(Boolean));
|
|
674
|
+
for (const rule of businessRules) {
|
|
675
|
+
if (rule.status === "VERIFIED" && rule.severity === "high" && !mappedRules.has(rule.id)) errors.push(`高优先级规则缺少代码映射:${rule.id}`);
|
|
676
|
+
}
|
|
677
|
+
for (const item of ruleMappings) {
|
|
678
|
+
const id = item.rule_id || item.rule || item.id;
|
|
679
|
+
if (!ruleIds.has(id)) errors.push(`规则映射引用不存在的规则:${id}`);
|
|
680
|
+
const nodes = item.graph_nodes || item.nodes || item.code_nodes || [];
|
|
681
|
+
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
682
|
+
errors.push(`规则映射缺少代码节点:${id}`);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
for (const nodeId of item.graph_nodes || item.nodes || item.code_nodes || []) {
|
|
686
|
+
if (!nodeById.has(nodeId)) errors.push(`规则映射引用不存在的图谱节点:${nodeId}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const contract = await validateContract(resolved, options);
|
|
691
|
+
errors.push(...contract.errors);
|
|
692
|
+
warnings.push(...contract.warnings);
|
|
693
|
+
return {
|
|
694
|
+
valid: errors.length === 0,
|
|
695
|
+
checkedAt: new Date().toISOString(),
|
|
696
|
+
draftsDir: resolved,
|
|
697
|
+
domainSlugs,
|
|
698
|
+
checkedFiles: required.length,
|
|
699
|
+
contractVersion: schemaVersion,
|
|
700
|
+
assets: contract.assets,
|
|
701
|
+
impact: contract.impact,
|
|
702
|
+
errors,
|
|
703
|
+
warnings,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function validateRunSources(run, outputRootDir = run.outputRootDir || knowledgeRootDir) {
|
|
708
|
+
const material = await readMaterial(run.materialDir);
|
|
709
|
+
const freshness = await checkMaterial(material.materialDir);
|
|
710
|
+
const cache = new Map();
|
|
711
|
+
const validation = await validateKnowledgeAssets(run.draftsDir, {
|
|
712
|
+
run, material, currentRepositories: freshness.repositories,
|
|
713
|
+
peerRoot: path.join(outputRootDir, "code-knowledge", run.productSlug),
|
|
714
|
+
readCode: async (repository, commit, file) => {
|
|
715
|
+
const repo = material.repositories.find((r) => r.name === repository);
|
|
716
|
+
if (!repo || !fullCommit.test(commit) || commit !== repo.commit) throw new Error("来源版本不匹配");
|
|
717
|
+
const key = `${repository}:${commit}:${file}`;
|
|
718
|
+
if (!cache.has(key)) {
|
|
719
|
+
const mode = await runQuietCommand("git", ["ls-tree", commit, "--", file], { cwd: repo.path });
|
|
720
|
+
if (!/^100(?:644|755) blob /.test(mode)) throw new Error("不是版本内普通文件");
|
|
721
|
+
cache.set(key, await runQuietCommand("git", ["show", `${commit}:${file}`], { cwd: repo.path, raw: true }));
|
|
722
|
+
}
|
|
723
|
+
return cache.get(key);
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
validation.sourceCheck = freshness;
|
|
727
|
+
if (!run.contractContext || !material.contextMode) validation.warnings.push("历史任务:保留读取和续跑;请依据真实原料补齐 2.0 元数据与证据后重新校验,不自动覆盖旧成果。");
|
|
728
|
+
for (const repo of freshness.repositories.filter((r) => !r.matches)) {
|
|
729
|
+
if (repo.currentCommit && repo.currentCommit !== repo.recordedCommit) validation.errors.push(`仓库 commit 已变化或不可读取,请生成新原料:${repo.name}`);
|
|
730
|
+
else validation.warnings.push(`仓库工作区未核验:${repo.name};引用该仓库的结论不能声明 VERIFIED`);
|
|
731
|
+
}
|
|
732
|
+
for (const doc of freshness.documents.filter((d) => !d.matches)) validation.errors.push(`业务资料已变化或不可读取,请刷新原料:${doc.path}`);
|
|
733
|
+
// Verify graph paths independently of evidence so an otherwise valid claim cannot hide a bad node.
|
|
734
|
+
for (const domain of validation.domainSlugs) {
|
|
735
|
+
let graph;
|
|
736
|
+
try { graph = parseYaml(await fs.readFile(path.join(run.draftsDir, "graph", "curated", `${domain}.graph.yaml`), "utf8")); } catch { continue; }
|
|
737
|
+
for (const node of Array.isArray(graph?.nodes) ? graph.nodes : []) {
|
|
738
|
+
if (!node?.path) continue;
|
|
739
|
+
const parts = String(node.path).split("/");
|
|
740
|
+
const repo = material.repositories.find((r) => r.name === parts[0]);
|
|
741
|
+
if (!repo || parts.length < 2 || parts.includes("..") || String(node.path).includes("\\")) {
|
|
742
|
+
validation.errors.push(`代码节点路径无效:${node.id} -> ${node.path}`); continue;
|
|
743
|
+
}
|
|
744
|
+
if (fullCommit.test(repo.commit || "")) {
|
|
745
|
+
try { await runQuietCommand("git", ["cat-file", "-e", `${repo.commit}:${parts.slice(1).join("/")}`], { cwd: repo.path }); }
|
|
746
|
+
catch { validation.errors.push(`代码节点路径在对应 commit 中不存在:${node.path}`); }
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
validation.valid = validation.errors.length === 0;
|
|
751
|
+
return validation;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function validateRun(runDir) {
|
|
755
|
+
const run = await readRun(runDir);
|
|
756
|
+
const validation = await validateRunSources(run);
|
|
757
|
+
const updated = await updateRun(run.runDir, {
|
|
758
|
+
status: validation.valid ? "validated" : "validation_failed",
|
|
759
|
+
validation,
|
|
760
|
+
}, validation.valid ? "validated" : "validation_failed");
|
|
761
|
+
return { run: updated, validation };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function exportRun(runDir, outputRootDir) {
|
|
765
|
+
const run = await readRun(runDir);
|
|
766
|
+
assertBusinessSlug(run.productSlug, "productSlug");
|
|
767
|
+
assertBusinessSlug(run.domainSlug, "domainSlug");
|
|
768
|
+
const resolvedOutputRoot = assertInsideUserSpace(outputRootDir || run.outputRootDir || knowledgeRootDir);
|
|
769
|
+
const validation = await validateRunSources(run, resolvedOutputRoot);
|
|
770
|
+
if (!validation.valid) throw new Error(`知识资产校验失败:${validation.errors.join(";")}`);
|
|
771
|
+
const exportDir = path.join(resolvedOutputRoot, "code-knowledge", run.productSlug, run.domainSlug);
|
|
772
|
+
const result = await exportKnowledgeAssets(run.draftsDir, exportDir);
|
|
773
|
+
const exportedAt = new Date().toISOString();
|
|
774
|
+
const updated = await updateRun(run.runDir, {
|
|
775
|
+
status: "exported",
|
|
776
|
+
outputRootDir: resolvedOutputRoot,
|
|
777
|
+
exportDir,
|
|
778
|
+
validation,
|
|
779
|
+
lastExport: { exportedAt, exportDir, copied: result.copied },
|
|
780
|
+
}, "exported");
|
|
781
|
+
let notification;
|
|
782
|
+
try {
|
|
783
|
+
const files = await collectFiles(exportDir, [".md", ".yaml", ".yml", ".json"]);
|
|
784
|
+
const fingerprint = sha256((await Promise.all(files.sort().map(async file => `${path.relative(exportDir, file)}:${sha256(await fs.readFile(file))}`))).join("\n"));
|
|
785
|
+
notification = await notify({ workspace: workspaceDir, event: "completed", task: run.runDir,
|
|
786
|
+
summary: `${run.productName} / ${run.domainName}:校验通过并已导出,业务仍待审核。`,
|
|
787
|
+
details: [`本机输出位置(非在线链接):${exportDir}`, `产物版本:${fingerprint}`, `校验提醒:${validation.warnings?.length || 0} 项;代码核验范围和测试限制请查看交付报告。`] });
|
|
788
|
+
} catch { notification = { status: "failed", reason: "通知记录或发送失败;不影响已完成的导出" }; }
|
|
789
|
+
return { run: updated, validation, outputRootDir: resolvedOutputRoot, exportDir, ...result, notification };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function assertMaterialDir(targetPath) {
|
|
793
|
+
const resolved = assertInsideUserSpace(targetPath);
|
|
794
|
+
const materialRoot = path.resolve(materialRootDir);
|
|
795
|
+
if (resolved !== materialRoot && !resolved.startsWith(materialRoot + path.sep)) {
|
|
796
|
+
throw new Error(`原料目录必须位于工作区 materials 下:${resolved}`);
|
|
797
|
+
}
|
|
798
|
+
return resolved;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async function readMaterial(materialDir) {
|
|
802
|
+
const resolved = assertMaterialDir(materialDir);
|
|
803
|
+
const manifestPath = path.join(resolved, "material.json");
|
|
804
|
+
let material;
|
|
805
|
+
try {
|
|
806
|
+
material = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
807
|
+
} catch {
|
|
808
|
+
throw new Error(`无效的中心原料,缺少或无法读取 material.json:${resolved}`);
|
|
809
|
+
}
|
|
810
|
+
if (!material.productName || !material.productSlug || !Array.isArray(material.repositories)) {
|
|
811
|
+
throw new Error(`中心原料清单字段不完整:${manifestPath}`);
|
|
812
|
+
}
|
|
813
|
+
if (typeof material.productSlug !== "string" || slugify(material.productSlug) !== material.productSlug) throw new Error("中心原料标识无效");
|
|
814
|
+
for (const repo of material.repositories) {
|
|
815
|
+
if (!repo || typeof repo.name !== "string" || !repo.name || repo.name.includes("/") || repo.name.includes("\\") ||
|
|
816
|
+
[".", ".."].includes(repo.name) || typeof repo.path !== "string" || !path.isAbsolute(repo.path)) {
|
|
817
|
+
throw new Error("中心原料仓库字段无效");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const materialPaths = [
|
|
821
|
+
material.convertedDocsDir,
|
|
822
|
+
material.repoManifestPath,
|
|
823
|
+
...material.repositories.map((repo) => repo.contextPath),
|
|
824
|
+
];
|
|
825
|
+
if (materialPaths.some((value) => {
|
|
826
|
+
const resolvedPath = path.resolve(String(value || ""));
|
|
827
|
+
return resolvedPath !== resolved && !resolvedPath.startsWith(resolved + path.sep);
|
|
828
|
+
})) {
|
|
829
|
+
throw new Error(`中心原料清单包含原料目录之外的上下文路径:${manifestPath}`);
|
|
830
|
+
}
|
|
831
|
+
return { ...material, materialDir: resolved, manifestPath };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function listMaterials() {
|
|
835
|
+
if (!fsSync.existsSync(materialRootDir)) return [];
|
|
836
|
+
const productDirs = await fs.readdir(materialRootDir, { withFileTypes: true });
|
|
837
|
+
const materials = [];
|
|
838
|
+
for (const productEntry of productDirs) {
|
|
839
|
+
if (!productEntry.isDirectory()) continue;
|
|
840
|
+
const productDir = path.join(materialRootDir, productEntry.name);
|
|
841
|
+
const entries = await fs.readdir(productDir, { withFileTypes: true });
|
|
842
|
+
for (const entry of entries) {
|
|
843
|
+
if (!entry.isDirectory()) continue;
|
|
844
|
+
try {
|
|
845
|
+
materials.push(await readMaterial(path.join(productDir, entry.name)));
|
|
846
|
+
} catch {
|
|
847
|
+
// Ignore incomplete material directories left by interrupted builds.
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return materials.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function createCenterMaterial(payload, taskId = crypto.randomUUID()) {
|
|
855
|
+
emit("task", { taskId, taskType: "material", status: "running", step: "prepare-material" });
|
|
856
|
+
const productName = String(payload.productName || payload.serviceName || "").trim();
|
|
857
|
+
if (!productName) throw new Error("请填写产品或业务中心名称");
|
|
858
|
+
const productSlug = resolveBusinessSlug(payload.productSlug ?? payload.serviceSlug, productName, "productSlug");
|
|
859
|
+
const repoPaths = normalizeRepoPaths(payload);
|
|
860
|
+
const rawDocsDir = payload.rawDocsDir ? assertInsideUserSpace(payload.rawDocsDir) : "";
|
|
861
|
+
if (repoPaths.length === 0 && !rawDocsDir) throw new Error("请至少添加一个代码仓库或补充资料目录");
|
|
862
|
+
|
|
863
|
+
requireDocumentChoice(payload);
|
|
864
|
+
const preflight = await syncMaster(repoPaths, { inspect: inspectRepository, workspace: workspaceDir });
|
|
865
|
+
const repositories = [];
|
|
866
|
+
for (const repoPath of repoPaths) repositories.push(await inspectRepository(repoPath));
|
|
867
|
+
const duplicateNames = repositories.filter((repo, index) => repositories.findIndex((item) => slugify(item.name) === slugify(repo.name)) !== index);
|
|
868
|
+
if (duplicateNames.length) throw new Error(`仓库目录名重复,无法生成稳定服务标识:${[...new Set(duplicateNames.map((repo) => repo.name))].join(", ")}`);
|
|
869
|
+
|
|
870
|
+
const contextMode = payload.contextMode || "repomix";
|
|
871
|
+
if (!["source-index", "repomix"].includes(contextMode)) throw new Error("contextMode 必须为 source-index 或 repomix");
|
|
872
|
+
const createdAt = new Date().toISOString();
|
|
873
|
+
const materialId = `${createdAt.replace(/[:.]/g, "-")}-${productSlug}-${crypto.randomUUID().slice(0, 8)}`;
|
|
874
|
+
const materialDir = path.join(materialRootDir, productSlug, materialId);
|
|
875
|
+
const convertedDocsDir = path.join(materialDir, "converted-docs");
|
|
876
|
+
const repomixDir = path.join(materialDir, "repomix");
|
|
877
|
+
const repoManifestPath = path.join(repomixDir, "README.md");
|
|
878
|
+
await fs.mkdir(materialDir, { recursive: true });
|
|
879
|
+
|
|
880
|
+
if (rawDocsDir) {
|
|
881
|
+
emit("task", { taskId, taskType: "material", status: "running", step: "convert-docs" });
|
|
882
|
+
await convertDocs(rawDocsDir, convertedDocsDir);
|
|
883
|
+
} else {
|
|
884
|
+
await fs.mkdir(convertedDocsDir, { recursive: true });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
await fs.mkdir(repomixDir, { recursive: true });
|
|
888
|
+
for (const repo of repositories) {
|
|
889
|
+
emit("task", { taskId, taskType: "material", status: "running", step: `${contextMode}:${repo.name}` });
|
|
890
|
+
repo.contextPath = path.join(repomixDir, `${slugify(repo.name)}.md`);
|
|
891
|
+
if (contextMode === "repomix") await runRepomix(repo.path, repo.contextPath);
|
|
892
|
+
else await writeSourceIndex(repo, repo.contextPath);
|
|
893
|
+
}
|
|
894
|
+
await fs.writeFile(
|
|
895
|
+
repoManifestPath,
|
|
896
|
+
`# ${productName} 多仓库代码原料\n\n${repositories.map((repo) => `- ${repo.name}\n - 路径:${repo.path}\n - commit:${repo.commit}\n - 上下文:${repo.contextPath}`).join("\n") || "未提供代码仓库。"}\n`,
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
const documents = [];
|
|
900
|
+
if (rawDocsDir) for (const file of await collectFiles(rawDocsDir, documentExtensions)) {
|
|
901
|
+
const relative = path.relative(rawDocsDir, file).split(path.sep).join("/");
|
|
902
|
+
documents.push({ document_id: `DOC:${sha256(relative).slice(0, 24)}`, path: relative, sha256: sha256(await fs.readFile(file)) });
|
|
903
|
+
}
|
|
904
|
+
const material = {
|
|
905
|
+
version: 1,
|
|
906
|
+
preflight,
|
|
907
|
+
documents,
|
|
908
|
+
contextMode,
|
|
909
|
+
materialId,
|
|
910
|
+
productName,
|
|
911
|
+
productSlug,
|
|
912
|
+
createdAt,
|
|
913
|
+
rawDocsDir,
|
|
914
|
+
convertedDocsDir,
|
|
915
|
+
repoManifestPath,
|
|
916
|
+
repositories,
|
|
917
|
+
};
|
|
918
|
+
await fs.writeFile(path.join(materialDir, "material.json"), JSON.stringify(material, null, 2) + "\n");
|
|
919
|
+
return { taskId, ...material, materialDir };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async function createDomainFromMaterial(payload, taskId = crypto.randomUUID()) {
|
|
923
|
+
emit("task", { taskId, taskType: "domain", status: "running", step: "prepare-domain" });
|
|
924
|
+
const material = await readMaterial(payload.materialDir);
|
|
925
|
+
await syncMaster(material.repositories.map(repo => repo.path), { inspect: inspectRepository, workspace: workspaceDir });
|
|
926
|
+
const freshness = await checkMaterial(material.materialDir);
|
|
927
|
+
if (!freshness.reusable) throw new Error("原料已过期,请刷新原料后继续,保留历史证据");
|
|
928
|
+
const productName = material.productName;
|
|
929
|
+
const productSlug = assertBusinessSlug(material.productSlug, "productSlug");
|
|
930
|
+
const domainName = String(payload.domainName || "").trim();
|
|
931
|
+
if (!domainName) throw new Error("请填写业务域名称");
|
|
932
|
+
const domainScope = String(payload.domainScope || "").trim();
|
|
933
|
+
const domainSlug = resolveBusinessSlug(payload.domainSlug, domainName, "domainSlug");
|
|
934
|
+
const configuredOutputRoot = payload.outputRootDir || knowledgeRootDir;
|
|
935
|
+
const outputRootDir = assertInsideUserSpace(configuredOutputRoot);
|
|
936
|
+
const runId = `${new Date().toISOString().replace(/[:.]/g, "-")}-${productSlug}-${domainSlug}-${crypto.randomUUID().slice(0, 8)}`;
|
|
937
|
+
const runDir = path.join(runRootDir, runId);
|
|
938
|
+
const draftsDir = path.join(runDir, "drafts");
|
|
939
|
+
const exportDir = path.join(outputRootDir, "code-knowledge", productSlug, domainSlug);
|
|
940
|
+
await fs.mkdir(runDir, { recursive: true });
|
|
941
|
+
|
|
942
|
+
emit("task", { taskId, taskType: "domain", status: "running", step: "drafts" });
|
|
943
|
+
const verifiedCommits = Object.fromEntries(material.repositories.map((repo) => [repo.name, repo.commit]));
|
|
944
|
+
const identities = await existingIdentities(exportDir);
|
|
945
|
+
const contractContext = makeContext({ product: productSlug, domain: domainSlug, repositories: material.repositories,
|
|
946
|
+
documents: material.documents || [], identities });
|
|
947
|
+
await writeDraftTemplates(draftsDir, domainName, domainSlug, verifiedCommits, { repositories: material.repositories, contractContext });
|
|
948
|
+
const promptPath = path.join(draftsDir, "AI_PROMPT.md");
|
|
949
|
+
const analysisPlanPath = path.join(draftsDir, "analysis-plan.yaml");
|
|
950
|
+
const evidenceDir = path.join(draftsDir, "evidence");
|
|
951
|
+
await fs.writeFile(promptPath, buildPrompt({
|
|
952
|
+
productName,
|
|
953
|
+
productSlug,
|
|
954
|
+
contractContext,
|
|
955
|
+
existingExportDir: exportDir,
|
|
956
|
+
domainName,
|
|
957
|
+
domainScope,
|
|
958
|
+
convertedDocsDir: material.convertedDocsDir,
|
|
959
|
+
repositories: material.repositories,
|
|
960
|
+
repoManifestPath: material.repoManifestPath,
|
|
961
|
+
draftsDir,
|
|
962
|
+
domainSlug,
|
|
963
|
+
analysisPlanPath,
|
|
964
|
+
evidenceDir,
|
|
965
|
+
}));
|
|
966
|
+
await fs.writeFile(
|
|
967
|
+
path.join(runDir, "README.md"),
|
|
968
|
+
`# ${productName} / ${domainName} 知识库构建任务\n\n- 产品或业务中心:${productName}\n- 业务域:${domainName}\n- 业务域边界:${domainScope || "未填写"}\n- 中心原料:${material.materialDir}\n- 原料生成时间:${material.createdAt}\n- 关联服务:${material.repositories.map((repo) => repo.name).join("、") || "无"}\n- 运行目录:${runDir}\n- 转换资料:${material.convertedDocsDir}\n- 多仓库清单:${material.repoManifestPath}\n- 草稿目录:${draftsDir}\n- AI 提示词:${promptPath}\n- 建议导出目录:${exportDir}\n\n下一步:使用 AI_PROMPT.md 补全知识资产,补全资产并通过程序校验后导出到选定本地位置;业务审核保持 pending,导出不等于审核或发布。\n`,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const createdAt = new Date().toISOString();
|
|
972
|
+
const runManifest = {
|
|
973
|
+
version: 1,
|
|
974
|
+
runId,
|
|
975
|
+
taskId,
|
|
976
|
+
contractContext,
|
|
977
|
+
status: "draft_ready",
|
|
978
|
+
createdAt,
|
|
979
|
+
updatedAt: createdAt,
|
|
980
|
+
productName,
|
|
981
|
+
productSlug,
|
|
982
|
+
domainName,
|
|
983
|
+
domainSlug,
|
|
984
|
+
domainScope,
|
|
985
|
+
materialDir: material.materialDir,
|
|
986
|
+
materialId: material.materialId,
|
|
987
|
+
draftsDir,
|
|
988
|
+
promptPath,
|
|
989
|
+
analysisPlanPath,
|
|
990
|
+
evidenceDir,
|
|
991
|
+
outputRootDir,
|
|
992
|
+
exportDir,
|
|
993
|
+
lifecycle: [{ type: "draft_ready", at: createdAt }],
|
|
994
|
+
};
|
|
995
|
+
await fs.writeFile(path.join(runDir, "run.json"), JSON.stringify(runManifest, null, 2) + "\n");
|
|
996
|
+
|
|
997
|
+
return {
|
|
998
|
+
runId,
|
|
999
|
+
taskId,
|
|
1000
|
+
productName,
|
|
1001
|
+
productSlug,
|
|
1002
|
+
domainName,
|
|
1003
|
+
domainSlug,
|
|
1004
|
+
domainScope,
|
|
1005
|
+
contractContext,
|
|
1006
|
+
runDir,
|
|
1007
|
+
materialDir: material.materialDir,
|
|
1008
|
+
materialId: material.materialId,
|
|
1009
|
+
materialCreatedAt: material.createdAt,
|
|
1010
|
+
convertedDocsDir: material.convertedDocsDir,
|
|
1011
|
+
repoManifestPath: material.repoManifestPath,
|
|
1012
|
+
repositories: material.repositories,
|
|
1013
|
+
draftsDir,
|
|
1014
|
+
promptPath,
|
|
1015
|
+
analysisPlanPath,
|
|
1016
|
+
evidenceDir,
|
|
1017
|
+
exportDir,
|
|
1018
|
+
outputRootDir,
|
|
1019
|
+
runManifestPath: path.join(runDir, "run.json"),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function buildKnowledgeBase(payload, taskId = crypto.randomUUID()) {
|
|
1024
|
+
// Reject invalid domain identifiers before creating shared material.
|
|
1025
|
+
resolveBusinessSlug(payload.domainSlug, String(payload.domainName || ""), "domainSlug");
|
|
1026
|
+
const material = payload.materialDir
|
|
1027
|
+
? await readMaterial(payload.materialDir)
|
|
1028
|
+
: await createCenterMaterial(payload, taskId);
|
|
1029
|
+
return createDomainFromMaterial({ ...payload, materialDir: material.materialDir }, taskId);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
export {
|
|
1033
|
+
assertRunDir,
|
|
1034
|
+
workspaceDir,
|
|
1035
|
+
inspectRepository,
|
|
1036
|
+
checkMaterial,
|
|
1037
|
+
buildKnowledgeBase,
|
|
1038
|
+
buildPrompt,
|
|
1039
|
+
createCenterMaterial,
|
|
1040
|
+
createDomainFromMaterial,
|
|
1041
|
+
exportKnowledgeAssets,
|
|
1042
|
+
exportRun,
|
|
1043
|
+
listMaterials,
|
|
1044
|
+
normalizeRepoPaths,
|
|
1045
|
+
readMaterial,
|
|
1046
|
+
readRun,
|
|
1047
|
+
slugify,
|
|
1048
|
+
inferBusinessSlug,
|
|
1049
|
+
assertBusinessSlug,
|
|
1050
|
+
updateRun,
|
|
1051
|
+
validateKnowledgeAssets,
|
|
1052
|
+
validateRun,
|
|
1053
|
+
writeDraftTemplates,
|
|
1054
|
+
};
|