Implement layered maps and context retrieval
This commit is contained in:
+143
-52
@@ -1,71 +1,162 @@
|
||||
import { dirname, join, basename, relative } from "path";
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { parsePackageMap, renderPackageMap } from "./format.js";
|
||||
import { extractFileLLM } from "./llm/llm-extract.js";
|
||||
import { extractFileAST } from "./ast/ast-extract.js";
|
||||
import { mergeFileData } from "./merge.js";
|
||||
import { generateDirectoryMap } from "./init.js";
|
||||
import { readdirSync, statSync } from "fs";
|
||||
import { basename, dirname, relative, resolve } from "path";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { parsePackageMap } from "./format.js";
|
||||
import {
|
||||
discoverProject,
|
||||
generateDirectoryArtifacts,
|
||||
buildDirectoryContext,
|
||||
} from "./init.js";
|
||||
import type { DirectoryEntry } from "./discover.js";
|
||||
import type { LLMClient } from "./llm/llm-client.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
|
||||
const SMALL_PACKAGE_THRESHOLD = 10;
|
||||
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<void> {
|
||||
const dirPath = dirname(filePath);
|
||||
const mapPath = join(dirPath, ".pi-map.md");
|
||||
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 (!existsSync(mapPath)) {
|
||||
// No map exists yet — would need to generate from scratch
|
||||
console.warn(`No .pi-map.md found in ${dirPath}`);
|
||||
if (!entry) {
|
||||
console.warn(`No discovered directory entry found for ${relDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const allFiles = readdirSync(dirPath).filter(
|
||||
(f: string) => !f.startsWith(".") && !f.endsWith(".md"),
|
||||
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",
|
||||
);
|
||||
const isSmallPackage = allFiles.length < SMALL_PACKAGE_THRESHOLD;
|
||||
|
||||
if (isSmallPackage) {
|
||||
// Full rewrite for small packages
|
||||
const files = allFiles.filter((f) => {
|
||||
const st = statSync(join(dirPath, f));
|
||||
return st.isFile();
|
||||
});
|
||||
const relDir = relative(process.cwd(), dirPath) || ".";
|
||||
await generateDirectoryMap(
|
||||
{
|
||||
dirPath,
|
||||
relativePath: relDir,
|
||||
files,
|
||||
},
|
||||
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(
|
||||
`Full rewrite of ${mapPath} (small package: ${allFiles.length} files)`,
|
||||
);
|
||||
} else {
|
||||
// Section-level patch
|
||||
const existing = parsePackageMap(readFileSync(mapPath, "utf8"));
|
||||
const llmData = await extractFileLLM(filePath, llmClient, cacheDir);
|
||||
const astData = await extractFileAST(filePath);
|
||||
const fileName = basename(filePath);
|
||||
const updatedFile = mergeFileData(fileName, llmData, astData);
|
||||
|
||||
// Replace the matching file entry
|
||||
const idx = existing.files.findIndex((f) => f.name === updatedFile.name);
|
||||
if (idx >= 0) {
|
||||
existing.files[idx] = updatedFile;
|
||||
} else {
|
||||
existing.files.push(updatedFile);
|
||||
}
|
||||
|
||||
existing.dirty = `${new Date().toISOString()}: ${fileName} patched (section-level)`;
|
||||
writeFileSync(mapPath, renderPackageMap(existing));
|
||||
console.log(`Patched ${mapPath}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Patched ${joinArtifactPath(relDir, ".pi-map.md")} (patchMode: ${mode})`,
|
||||
);
|
||||
}
|
||||
|
||||
function determinePatchMode(
|
||||
entry: DirectoryEntry,
|
||||
absFilePath: string,
|
||||
rootPath: string,
|
||||
explicitMode: PatchMode = "auto",
|
||||
): Exclude<PatchMode, "auto"> {
|
||||
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 getAncestorEntries(
|
||||
entries: DirectoryEntry[],
|
||||
ctx: ReturnType<typeof buildDirectoryContext>,
|
||||
entry: DirectoryEntry,
|
||||
): DirectoryEntry[] {
|
||||
const result: DirectoryEntry[] = [];
|
||||
let parent = ctx.parentMap.get(entry.relativePath);
|
||||
while (parent) {
|
||||
const ancestor = entries.find(
|
||||
(candidate) => candidate.relativePath === parent,
|
||||
);
|
||||
if (ancestor) {
|
||||
result.push(ancestor);
|
||||
}
|
||||
parent = ctx.parentMap.get(parent);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user