cb581f44b9
- project_map_reinit now regenerates only the target subtree + ancestors by default, falling back to full reinit when subtree file count exceeds reinitFullThresholdPercent (default 10%). - Add reinitFullThresholdPercent config option. - Expose fix=true on project_map_validate Pi tool for localized repair. - Update docs and runtime guidance to prefer patch / validate --fix before full reinit. - Add integration tests for smart reinit and update typebox mock.
145 lines
3.6 KiB
TypeScript
145 lines
3.6 KiB
TypeScript
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<void> {
|
|
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<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 normalizeRelativePath(relPath: string): string {
|
|
if (!relPath || relPath === ".") return ".";
|
|
return relPath.replace(/\\/g, "/");
|
|
}
|
|
|
|
function joinArtifactPath(relDir: string, artifactName: string): string {
|
|
return relDir === "." ? artifactName : `${relDir}/${artifactName}`;
|
|
}
|