import { basename, dirname, relative, resolve } from "path"; import { existsSync, readFileSync } from "fs"; import { parsePackageMap } from "./format.js"; import { discoverProject, generateDirectoryArtifacts, buildDirectoryContext, getAncestorEntries, } from "./init.js"; import type { DirectoryEntry } from "./discover.js"; import type { LLMClient } from "./llm/llm-client.js"; import { loadConfig } from "./config.js"; export type PatchMode = "auto" | "small" | "structural"; export interface PatchOptions { patchMode?: PatchMode; rootPath?: string; } const SMALL_CHANGE_FILE_WINDOW = 3; const STRUCTURAL_FILE_HINT = /(^|\b)(index|config|cli|command|init|format|discover|patch|validate)(\b|\.)/i; export async function patchFile( filePath: string, llmClient?: LLMClient, cacheDir?: string, options: PatchOptions = {}, ): Promise { const rootPath = resolve(options.rootPath ?? cacheDir ?? process.cwd()); const absFilePath = resolve(rootPath, filePath); const dirPath = dirname(absFilePath); const relDir = normalizeRelativePath(relative(rootPath, dirPath)); const entries = discoverProject(rootPath); const entry = entries.find((candidate) => candidate.relativePath === relDir); if (!entry) { console.warn(`No discovered directory entry found for ${relDir}`); return; } const config = loadConfig(rootPath); const routingOpts = { tagCap: config.tagCap, workflowHintCap: config.workflowHintCap, }; const mode = determinePatchMode( entry, absFilePath, rootPath, options.patchMode, ); const changedCtx = buildDirectoryContext(entries, entry); await generateDirectoryArtifacts( entry, changedCtx, llmClient, cacheDir, undefined, routingOpts, "both", ); for (const ancestor of getAncestorEntries(entries, changedCtx, entry)) { const ancestorCtx = buildDirectoryContext(entries, ancestor); await generateDirectoryArtifacts( ancestor, ancestorCtx, llmClient, cacheDir, undefined, routingOpts, mode === "small" ? "index" : "both", ); } console.log( `Patched ${joinArtifactPath(relDir, ".pi-map.md")} (patchMode: ${mode})`, ); } function determinePatchMode( entry: DirectoryEntry, absFilePath: string, rootPath: string, explicitMode: PatchMode = "auto", ): Exclude { if (explicitMode !== "auto") { return explicitMode; } const existingMapPath = resolve(entry.dirPath, ".pi-map.md"); const fileName = basename(absFilePath); const childCount = entry.relativePath === "." ? 0 : countDirectChildren(rootPath, entry.relativePath); if (childCount > 0) { return "structural"; } if (entry.files.length > SMALL_CHANGE_FILE_WINDOW) { return "structural"; } if (STRUCTURAL_FILE_HINT.test(fileName)) { return "structural"; } if (existsSync(existingMapPath)) { const existing = parsePackageMap(readFileSync(existingMapPath, "utf8")); const existingFile = existing.files.find( (candidate) => candidate.name === fileName, ); if (!existingFile) { return "structural"; } } return "small"; } function countDirectChildren(rootPath: string, relDir: string): number { const entries = discoverProject(rootPath); return entries.filter((entry) => { if (entry.relativePath === "." || entry.relativePath === relDir) return false; const parent = normalizeRelativePath( entry.relativePath.split("/").slice(0, -1).join("/"), ); return parent === relDir; }).length; } function normalizeRelativePath(relPath: string): string { if (!relPath || relPath === ".") return "."; return relPath.replace(/\\/g, "/"); } function joinArtifactPath(relDir: string, artifactName: string): string { return relDir === "." ? artifactName : `${relDir}/${artifactName}`; }