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.
This commit is contained in:
+186
-1
@@ -8,7 +8,7 @@ import {
|
||||
} from "fs";
|
||||
import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
import { initProject } from "../src/init.js";
|
||||
import { initProject, reinitPath } from "../src/init.js";
|
||||
import { patchFile } from "../src/patch.js";
|
||||
import { validateMaps } from "../src/validate.js";
|
||||
import { createMockFileClient } from "./mock-llm.js";
|
||||
@@ -356,4 +356,189 @@ describe("integration", () => {
|
||||
readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"),
|
||||
).toContain("# src (index)");
|
||||
});
|
||||
|
||||
it("reinit on root regenerates all artifacts", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.md"),
|
||||
readFileSync(join(dir, ".pi-map.md"), "utf8") + "\nCORRUPTED",
|
||||
);
|
||||
await reinitPath(dir, { llmClient: client, verbose: false });
|
||||
expect(readFileSync(join(dir, ".pi-map.md"), "utf8")).not.toContain(
|
||||
"CORRUPTED",
|
||||
);
|
||||
});
|
||||
|
||||
it("reinit on a small subtree regenerates subtree and ancestors but not siblings", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
mkdirSync(join(dir, "lib"));
|
||||
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
writeFileSync(
|
||||
join(dir, "lib", `b${i}.ts`),
|
||||
`export const b${i} = ${i};\n`,
|
||||
);
|
||||
}
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
||||
);
|
||||
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
writeFileSync(
|
||||
join(dir, "src", ".pi-map.md"),
|
||||
readFileSync(join(dir, "src", ".pi-map.md"), "utf8") + "\nSUBTREE_MARKER",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, "lib", ".pi-map.md"),
|
||||
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
||||
);
|
||||
|
||||
await reinitPath(join(dir, "src"), {
|
||||
llmClient: client,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
expect(readFileSync(join(dir, "src", ".pi-map.md"), "utf8")).not.toContain(
|
||||
"SUBTREE_MARKER",
|
||||
);
|
||||
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).toContain(
|
||||
"SIBLING_MARKER",
|
||||
);
|
||||
});
|
||||
|
||||
it("reinit falls back to full regeneration when subtree exceeds threshold", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
mkdirSync(join(dir, "lib"));
|
||||
for (let i = 0; i < 11; i++) {
|
||||
writeFileSync(
|
||||
join(dir, "src", `a${i}.ts`),
|
||||
`export const a${i} = ${i};\n`,
|
||||
);
|
||||
}
|
||||
writeFileSync(join(dir, "lib", "b.ts"), `export const b = 1;\n`);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
||||
);
|
||||
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
writeFileSync(
|
||||
join(dir, "lib", ".pi-map.md"),
|
||||
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
||||
);
|
||||
|
||||
await reinitPath(join(dir, "src"), {
|
||||
llmClient: client,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).not.toContain(
|
||||
"SIBLING_MARKER",
|
||||
);
|
||||
});
|
||||
|
||||
it("reinit respects a custom reinitFullThresholdPercent", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
mkdirSync(join(dir, "lib"));
|
||||
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
||||
writeFileSync(join(dir, "lib", "b.ts"), `export const b = 1;\n`);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ reinitFullThresholdPercent: 50 }),
|
||||
);
|
||||
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
writeFileSync(
|
||||
join(dir, "lib", ".pi-map.md"),
|
||||
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
||||
);
|
||||
|
||||
await reinitPath(join(dir, "src"), {
|
||||
llmClient: client,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).toContain(
|
||||
"SIBLING_MARKER",
|
||||
);
|
||||
});
|
||||
|
||||
it("reinit on a small subtree regenerates ancestors up to the root", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
mkdirSync(join(dir, "lib"));
|
||||
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
writeFileSync(
|
||||
join(dir, "lib", `b${i}.ts`),
|
||||
`export const b${i} = ${i};\n`,
|
||||
);
|
||||
}
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
||||
);
|
||||
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.md"),
|
||||
readFileSync(join(dir, ".pi-map.md"), "utf8") + "\nROOT_MARKER",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, "lib", ".pi-map.md"),
|
||||
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
||||
);
|
||||
|
||||
await reinitPath(join(dir, "src"), {
|
||||
llmClient: client,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
expect(readFileSync(join(dir, ".pi-map.md"), "utf8")).not.toContain(
|
||||
"ROOT_MARKER",
|
||||
);
|
||||
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).toContain(
|
||||
"SIBLING_MARKER",
|
||||
);
|
||||
});
|
||||
|
||||
it("reinit on a non-existent path falls back to full regeneration", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
mkdirSync(join(dir, "lib"));
|
||||
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
||||
writeFileSync(join(dir, "lib", "b.ts"), `export const b = 1;\n`);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
||||
);
|
||||
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
writeFileSync(
|
||||
join(dir, "lib", ".pi-map.md"),
|
||||
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
||||
);
|
||||
|
||||
await reinitPath(join(dir, "does-not-exist"), {
|
||||
llmClient: client,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).not.toContain(
|
||||
"SIBLING_MARKER",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user