Files
pi-map/src/patch.ts
T
Developer cb581f44b9 feat: smart subtree-aware reinit and validate --fix
- 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.
2026-06-16 11:46:48 +00:00

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}`;
}