import { existsSync } from "node:fs"; import { basename } from "node:path"; import type { ActiveRun } from "./types.ts"; import { writeEvidenceFile } from "./evidence.ts"; import { discoverMetadataSources, type MetadataDiscoveryItem, type MetadataDiscoveryResult } from "./metadata-discovery.ts"; import { dataSourceEvidenceForRun, COSMIC_METADATA_EVIDENCE, DATA_SOURCE_EVIDENCE } from "./data-source-policy.ts"; import { resolveWorkspacePath } from "../platform/path.ts"; import { formatFormMetadataJson, formatFormMetadataMarkdown, parseFormMetadataFile, type FormMetadata, } from "../metadata/form-metadata.ts"; export type MetadataAutocollectStatus = "collected" | "dry-run" | "already-present" | "no-local-source" | "parse-failed"; export interface MetadataAutocollectResult { status: MetadataAutocollectStatus; message: string; sourcePath?: string; evidencePath?: string; auxiliaryEvidencePath?: string; entities: number; fields: number; plugins: number; warnings: string[]; nextAction: string; discovery: MetadataDiscoveryResult; markdown?: string; } export function collectMetadataEvidence( cwd: string, run: ActiveRun, options: { path?: string; dryRun?: boolean } = {}, ): MetadataAutocollectResult { const discovery = discoverMetadataSources(cwd, run); const targetEvidence = dataSourceEvidenceForRun(run) ?? DATA_SOURCE_EVIDENCE; if (!options.path && discovery.items.some((item) => item.kind === "evidence" && item.path === targetEvidence)) { return { status: "already-present", message: `目标元数据证据已存在:${targetEvidence}`, evidencePath: targetEvidence, entities: 0, fields: 0, plugins: 0, warnings: [], nextAction: "读取已有 evidence 并刷新门禁;仍缺字段/实体事实时再查 MCP 或询问用户。", discovery, }; } const candidates = options.path ? [{ kind: "project-file", path: options.path, action: "按用户指定路径采集元数据。" } satisfies MetadataDiscoveryItem] : discovery.items.filter((item) => item.kind === "project-file"); if (candidates.length === 0) { return { status: "no-local-source", message: "未发现可自动采集的本地 FKERNELXML/fdata/MCP 导出文件。", entities: 0, fields: 0, plugins: 0, warnings: [], nextAction: discovery.recommendedAction, discovery, }; } const failures: string[] = []; for (const candidate of candidates) { const absolutePath = resolveWorkspacePath(cwd, candidate.path); if (!existsSync(absolutePath)) { failures.push(`${candidate.path}: 文件不存在`); continue; } try { const metadata = parseFormMetadataFile(absolutePath); if (!metadataLooksUsable(metadata)) { failures.push(`${candidate.path}: 未解析到可用实体、字段或插件元数据`); continue; } const markdown = withAutocollectHeader(formatFormMetadataMarkdown(metadata), candidate.path); if (options.dryRun) { return { status: "dry-run", message: `已解析本地元数据文件但未写入 evidence:${candidate.path}`, sourcePath: candidate.path, entities: metadata.entities.length, fields: metadata.fields.length, plugins: metadata.plugins.length, warnings: metadata.warnings, nextAction: "确认解析内容正确后运行 kd_metadata_collect 或 kcode meta 写入 evidence。", discovery, markdown, }; } const auxiliaryEvidencePath = targetEvidence === COSMIC_METADATA_EVIDENCE ? writeEvidenceFile(cwd, run, DATA_SOURCE_EVIDENCE, markdown, evidenceOptions(candidate.path, "form-metadata")) : undefined; const evidencePath = writeEvidenceFile( cwd, run, targetEvidence, targetEvidence === COSMIC_METADATA_EVIDENCE ? formatFormMetadataJson(metadata) : markdown, evidenceOptions(candidate.path, targetEvidence === COSMIC_METADATA_EVIDENCE ? "cosmic-metadata" : "form-metadata"), ); return { status: "collected", message: `已自动采集本地元数据证据:${candidate.path}`, sourcePath: candidate.path, evidencePath, auxiliaryEvidencePath, entities: metadata.entities.length, fields: metadata.fields.length, plugins: metadata.plugins.length, warnings: metadata.warnings, nextAction: "刷新门禁;若仍阻塞,按门禁原因补 SDK 签名、TDD 或业务验收证据。", discovery, markdown, }; } catch (error) { failures.push(`${candidate.path}: ${error instanceof Error ? error.message : String(error)}`); } } return { status: "parse-failed", message: `本地元数据文件存在,但未能解析为可用 evidence:${failures.join(";")}`, entities: 0, fields: 0, plugins: 0, warnings: failures, nextAction: "改用已配置只读 MCP 查询 FKERNELXML/fdata 并导出 JSON/XML,或让用户提供目标 FormId、字段/实体和真实数据来源。", discovery, }; } export function formatMetadataAutocollectResult(result: MetadataAutocollectResult): string { return [ `状态:${result.status}`, `消息:${result.message}`, result.sourcePath ? `来源文件:${result.sourcePath}` : undefined, result.evidencePath ? `主证据:${result.evidencePath}` : undefined, result.auxiliaryEvidencePath ? `辅助证据:${result.auxiliaryEvidencePath}` : undefined, `实体:${result.entities}`, `字段:${result.fields}`, `插件引用:${result.plugins}`, result.warnings.length > 0 ? `警告:${result.warnings.join(";")}` : undefined, `下一步:${result.nextAction}`, result.markdown ? ["", result.markdown.trimEnd()].join("\n") : undefined, ].filter(Boolean).join("\n"); } function evidenceOptions(sourcePath: string, kind: "form-metadata" | "cosmic-metadata") { return { kind, source: "project-metadata" as const, command: `kd_metadata_collect path=${sourcePath}`, exitCode: 0, }; } function metadataLooksUsable(metadata: FormMetadata): boolean { return metadata.entities.length > 0 || metadata.fields.length > 0 || metadata.plugins.length > 0; } function withAutocollectHeader(markdown: string, sourcePath: string): string { return [ "", ``, ``, "", markdown.trimEnd(), "", ].join("\n"); }