import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { basename, dirname, extname, join, relative } from "node:path"; import { PROJECT_PERSISTENT_RULES, formatPromptLines } from "../harness/prompt-policy.js"; const IGNORED_DIRS = new Set([ ".git", ".idea", ".pi", ".tmp", ".vscode", "bin", "build", "dist", "node_modules", "out", "target", ]); const SOURCE_EXTENSIONS = new Set([".java", ".kt", ".kts", ".cs", ".py", ".xml", ".properties", ".yml", ".yaml", ".sql", ".ksql"]); const BUILD_FILE_NAMES = new Set(["pom.xml", "build.gradle", "build.gradle.kts", "gradlew", "gradlew.bat", "mvnw", "mvnw.cmd", "package.json"]); const MAX_DEPTH = 5; const MAX_ENTRIES = 3000; const MAX_SOURCE_SAMPLES = 60; const MAX_MODULES = 80; export interface ProjectContextResult { path: string; content: string; } export function projectContextPath(cwd: string): string { return join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md"); } export function deepProjectContextPath(cwd: string): string { return join(cwd, ".pi", "kd", "DEEP_CONTEXT.md"); } export function readProjectContext(cwd: string): string | undefined { const path = projectContextPath(cwd); return existsSync(path) ? readFileSync(path, "utf8") : undefined; } export function readDeepProjectContext(cwd: string): string | undefined { const path = deepProjectContextPath(cwd); return existsSync(path) ? readFileSync(path, "utf8") : undefined; } export function ensureProjectContext(cwd: string): ProjectContextResult { const path = projectContextPath(cwd); if (existsSync(path)) return { path, content: readFileSync(path, "utf8") }; return writeProjectContext(cwd); } export function writeProjectContext(cwd: string): ProjectContextResult { const path = projectContextPath(cwd); const content = generateProjectContext(cwd); mkdirSync(join(cwd, ".pi", "kd"), { recursive: true }); writeFileSync(path, content, "utf8"); return { path, content }; } export function writeDeepProjectContext(cwd: string): ProjectContextResult { const path = deepProjectContextPath(cwd); const content = generateDeepProjectContext(cwd); mkdirSync(join(cwd, ".pi", "kd"), { recursive: true }); writeFileSync(path, content, "utf8"); return { path, content }; } export function generateProjectContext(cwd: string): string { const scan = scanProject(cwd); const codeDir = scan.directories.includes("code"); const likelyRoots = detectLikelySourceRoots(scan); const modules = detectModules(scan); const buildFiles = scan.files.filter((file) => BUILD_FILE_NAMES.has(basename(file).toLowerCase()) || isSolutionOrProject(file)); const sourceSamples = scan.files.filter((file) => SOURCE_EXTENSIONS.has(extname(file).toLowerCase())).slice(0, MAX_SOURCE_SAMPLES); return [ "# KCode 项目上下文", "", `- 项目根目录:${cwd}`, `- 项目名称:${basename(cwd)}`, `- 生成时间:${new Date().toISOString()}`, "", "## 持久规则", "", ...formatPromptLines(PROJECT_PERSISTENT_RULES), "- 禁止假设模块结构。必须基于下方真实路径,并在编辑前确认目标文件。", "- 调用文件工具时默认使用项目相对路径。在 Windows 中禁止把路径改写为 /mnt//... 或 //...;绝对路径只允许使用 Windows 路径。", "- 只有 Harness 进入 `execute` 且 PLAN.md 写明真实目标路径后,才能写产品代码。", "", "## 项目结构摘要", "", `- 是否存在 code 目录:${codeDir ? "是" : "否"}`, `- 可能的源码根:${formatList(likelyRoots)}`, `- 构建文件:${formatList(buildFiles)}`, `- 识别到的模块:${formatList(modules)}`, "", "## 源码样例", "", formatBlockList(sourceSamples), "", "## 顶层目录", "", formatBlockList(scan.directories.filter((dir) => !dir.includes("/") && !dir.includes("\\")).sort()), "", ].join("\n"); } export function generateDeepProjectContext(cwd: string): string { const scan = scanProject(cwd); const roots = [...new Set([".", ...detectModules(scan), ...detectLikelySourceRoots(scan)])].slice(0, 120); return [ "# KCode Deep Context Index", "", `- 项目根目录:${cwd}`, `- 生成时间:${new Date().toISOString()}`, "- 用途:按当前 Working Set 最近文件自动注入相邻目录约定;不替代 run.facts、PLAN.md 或 evidence。", "- 规则:业务事实仍必须来自用户确认、项目元数据或 evidence;本文件只描述代码结构和局部约定。", "", ...roots.map((root) => formatDeepContextSection(root, scan)), ].join("\n"); } export function relevantDeepProjectContext(cwd: string, paths: string[] | undefined, maxChars = 1800): string { const content = readDeepProjectContext(cwd); if (!content) return "未生成。执行 `kcode init-deep` 或 `kcode ctx --deep`。"; const sections = parseDeepContextSections(content); const wanted = new Set(); for (const path of paths ?? []) { const normalized = normalize(path); for (const sectionPath of sections.keys()) { if (sectionPath === "." || normalized === sectionPath || normalized.startsWith(`${sectionPath}/`)) wanted.add(sectionPath); } } const selected = [...wanted] .sort((a, b) => b.length - a.length) .slice(0, 6) .map((path) => sections.get(path)) .filter((section): section is string => Boolean(section)); const output = selected.length > 0 ? selected.join("\n\n") : sections.get(".") ?? "无匹配目录上下文。"; return output.length <= maxChars ? output : `${output.slice(0, maxChars)}\n\n[...Deep Context 已截断;完整内容读取 .pi/kd/DEEP_CONTEXT.md...]`; } interface ProjectScan { files: string[]; directories: string[]; } function scanProject(cwd: string): ProjectScan { const files: string[] = []; const directories: string[] = []; const queue: Array<{ path: string; depth: number }> = [{ path: cwd, depth: 0 }]; let entries = 0; while (queue.length > 0 && entries < MAX_ENTRIES) { const current = queue.shift(); if (!current) break; let children: string[]; try { children = readdirSync(current.path); } catch { continue; } for (const child of children) { if (entries >= MAX_ENTRIES) break; const fullPath = join(current.path, child); let stat; try { stat = statSync(fullPath); } catch { continue; } const rel = normalize(relative(cwd, fullPath)); entries++; if (stat.isDirectory()) { if (IGNORED_DIRS.has(child)) continue; directories.push(rel); if (current.depth < MAX_DEPTH) queue.push({ path: fullPath, depth: current.depth + 1 }); continue; } if (stat.isFile()) files.push(rel); } } return { files: files.sort(), directories: directories.sort() }; } function detectLikelySourceRoots(scan: ProjectScan): string[] { const roots = new Set(); const knownRoots = [ "code/src/main/java", "code/src/main/resources", "src/main/java", "src/main/resources", "src", "script", "scripts", "plugins", ]; for (const root of knownRoots) { if (scan.directories.includes(root) || scan.files.some((file) => file.startsWith(`${root}/`))) roots.add(root); } for (const file of scan.files) { if (!SOURCE_EXTENSIONS.has(extname(file).toLowerCase())) continue; const marker = sourceRootFromFile(file); if (marker) roots.add(marker); } return [...roots].slice(0, MAX_MODULES); } function detectModules(scan: ProjectScan): string[] { const modules = new Set(); for (const file of scan.files) { const name = basename(file).toLowerCase(); if (!BUILD_FILE_NAMES.has(name) && !isSolutionOrProject(file)) continue; const dir = normalize(file.slice(0, -basename(file).length).replace(/[\\/]$/, "")); modules.add(dir || "."); } for (const sourceRoot of detectLikelySourceRoots(scan)) { const parts = sourceRoot.split("/"); if (parts.length > 3) modules.add(parts.slice(0, -3).join("/") || "."); } return [...modules].filter(Boolean).sort().slice(0, MAX_MODULES); } function formatDeepContextSection(root: string, scan: ProjectScan): string { const prefix = root === "." ? "" : `${root}/`; const directDirs = scan.directories .filter((dir) => (root === "." ? !dir.includes("/") : dir.startsWith(prefix) && dirname(dir) === root)) .slice(0, 12); const files = scan.files .filter((file) => (root === "." ? !file.includes("/") : file.startsWith(prefix))) .filter((file) => SOURCE_EXTENSIONS.has(extname(file).toLowerCase()) || BUILD_FILE_NAMES.has(basename(file).toLowerCase()) || isSolutionOrProject(file)) .slice(0, 20); const conventions = inferDirectoryConventions(root, files); return [ `## ${root}`, "", `- 子目录:${formatList(directDirs)}`, `- 关键文件:${formatList(files)}`, "- 局部约定:", ...conventions.map((item) => ` - ${item}`), ].join("\n"); } function inferDirectoryConventions(root: string, files: string[]): string[] { const rules: string[] = []; if (files.some((file) => extname(file).toLowerCase() === ".cs")) rules.push("检测到 C# 文件;企业版实现需确认 BOS 插件类型、FormId、字段/实体标识和 dotnet 验证方式。"); if (files.some((file) => extname(file).toLowerCase() === ".java")) rules.push("检测到 Java 文件;Cosmic 实现需确认插件事件、元数据证据和 SDK 签名。"); if (files.some((file) => [".sql", ".ksql"].includes(extname(file).toLowerCase()))) rules.push("检测到 SQL/KSQL;必须确认表名、数据库字段名、幂等/回滚和 lint 证据。"); if (files.some((file) => basename(file).toLowerCase() === "package.json")) rules.push("检测到 Node 包;使用 package.json scripts 中真实命令验证。"); if (root === ".") rules.push("根上下文只描述结构;编辑前必须读取更近目录上下文和目标文件。"); return rules.length > 0 ? rules : ["未识别专用约定;编辑前读取目标文件和同目录相邻实现。"]; } function parseDeepContextSections(content: string): Map { const sections = new Map(); const matches = [...content.matchAll(/^## (.+)$/gm)]; for (let i = 0; i < matches.length; i++) { const match = matches[i]; const next = matches[i + 1]; const start = match.index ?? 0; const end = next?.index ?? content.length; sections.set(match[1].trim(), content.slice(start, end).trim()); } return sections; } function sourceRootFromFile(file: string): string | undefined { const normalized = normalize(file); const javaIndex = normalized.indexOf("/src/main/java/"); if (javaIndex >= 0) return normalized.slice(0, javaIndex + "/src/main/java".length); const resourcesIndex = normalized.indexOf("/src/main/resources/"); if (resourcesIndex >= 0) return normalized.slice(0, resourcesIndex + "/src/main/resources".length); const srcIndex = normalized.indexOf("/src/"); if (srcIndex >= 0) return normalized.slice(0, srcIndex + "/src".length); const first = normalized.split("/")[0]; return first && SOURCE_EXTENSIONS.has(extname(normalized).toLowerCase()) ? first : undefined; } function isSolutionOrProject(file: string): boolean { return [".sln", ".csproj"].includes(extname(file).toLowerCase()); } function formatList(values: string[]): string { return values.length > 0 ? values.join(", ") : "未识别"; } function formatBlockList(values: string[]): string { if (values.length === 0) return "- 未识别"; return values.map((value) => `- ${value}`).join("\n"); } function normalize(path: string): string { return path.replace(/\\/g, "/"); }