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:
@@ -44,8 +44,10 @@ When project-map artifacts exist in the repo:
|
||||
3. read the local `.pi-map.md` plus relevant source before editing
|
||||
4. run `project_map_patch <file>` (tool) or `project-map patch <file>` (CLI) after each source edit
|
||||
5. run `project_map_validate` (tool) or `project-map validate` (CLI) before freshness-sensitive architectural decisions or final handoff
|
||||
6. for targeted exploration, use `project_map_context <query>` (tool) or `project-map context <query>` (CLI)
|
||||
7. in `strict` mode, only bypass the protocol-path guard with an explicit marker: `[PI_MAP_BYPASS: <brief justification>]`
|
||||
6. if validation shows localized discrepancies, run `project_map_validate` with `fix=true` (tool) or `project-map validate --fix` (CLI)
|
||||
7. only if discrepancies are widespread or structural, run `project_map_reinit` (tool) or `project-map reinit` (CLI)
|
||||
8. for targeted exploration, use `project_map_context <query>` (tool) or `project-map context <query>` (CLI)
|
||||
9. in `strict` mode, only bypass the protocol-path guard with an explicit marker: `[PI_MAP_BYPASS: <brief justification>]`
|
||||
|
||||
## Prompt injection modes
|
||||
|
||||
@@ -102,12 +104,15 @@ Create `.pi-project-map.json` in the project root:
|
||||
"workflowHintCap": 5,
|
||||
"llmProvider": "openai",
|
||||
"llmModel": "gpt-4o-mini",
|
||||
"reinitFullThresholdPercent": 10,
|
||||
"ignorePatterns": ["node_modules", ".git", "dist", "build"]
|
||||
}
|
||||
```
|
||||
|
||||
Providing `ignorePatterns` replaces the built-in default list, so include any defaults you want to keep.
|
||||
|
||||
- `reinitFullThresholdPercent` — when `project_map_reinit` targets a subtree that covers more than this percentage of project files, it falls back to full regeneration
|
||||
|
||||
Knobs that matter in practice:
|
||||
- `promptInjectionMode` — `off` | `advisory` | `strong` | `strict`
|
||||
- `contextBudgetPercent` / `contextBudgetMaxTokens` — caps automatic map/index injection
|
||||
@@ -122,5 +127,5 @@ Knobs that matter in practice:
|
||||
| `project_map_init` | `project-map init [path]` | Generate all paired artifacts. |
|
||||
| `project_map_patch` | `project-map patch <file>` | Regenerate the pair for the changed file's directory and refresh ancestors. |
|
||||
| `project_map_validate` | `project-map validate [--fix]` | Check paired artifacts for staleness and discrepancies; optionally repair. |
|
||||
| `project_map_reinit` | `project-map reinit [path]` | Force full regeneration. |
|
||||
| `project_map_reinit` | `project-map reinit [path]` | Smart regeneration: recomputes the target subtree plus ancestors, and only falls back to full regeneration when the subtree covers more than `reinitFullThresholdPercent` of project files (default 10%). |
|
||||
| `project_map_context` | `project-map context <query>` | Retrieve a ranked context bundle for a natural-language query. |
|
||||
|
||||
+20
-7
@@ -209,11 +209,12 @@ export default function (pi: ExtensionAPI) {
|
||||
name: "project_map_validate",
|
||||
label: "Project Map Validate",
|
||||
description:
|
||||
"Check all .pi-map.md / .pi-map.index.md files for staleness and discrepancies",
|
||||
"Check all .pi-map.md / .pi-map.index.md files for staleness and discrepancies. Optionally repair them.",
|
||||
promptSnippet: "Validate paired project map/index artifacts for accuracy",
|
||||
promptGuidelines: [
|
||||
"Use project_map_validate before making architectural decisions if you suspect stale data",
|
||||
"Use project_map_validate to detect files that were deleted or added outside the agent",
|
||||
"Set fix=true to repair localized discrepancies without running a full project_map_reinit",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
path: Type.Optional(
|
||||
@@ -221,13 +222,23 @@ export default function (pi: ExtensionAPI) {
|
||||
description: "Project root path (default: current directory)",
|
||||
}),
|
||||
),
|
||||
fix: Type.Optional(
|
||||
Type.Boolean({
|
||||
description:
|
||||
"Repair discrepancies automatically (default: false). Requires an LLM client.",
|
||||
default: false,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
try {
|
||||
const targetPath = params.path || ctx.cwd;
|
||||
const client = params.fix ? getPiLLMClient(ctx) : undefined;
|
||||
const result = await validateMaps(targetPath, {
|
||||
fix: false,
|
||||
fix: params.fix ?? false,
|
||||
verbose: false,
|
||||
llmClient: client,
|
||||
cacheDir: ctx.cwd,
|
||||
});
|
||||
const text = result.clean
|
||||
? "All .pi-map.md files are clean."
|
||||
@@ -253,12 +264,14 @@ export default function (pi: ExtensionAPI) {
|
||||
name: "project_map_reinit",
|
||||
label: "Project Map Reinit",
|
||||
description:
|
||||
"Force full regeneration of all .pi-map.md / .pi-map.index.md artifacts",
|
||||
"Regenerate .pi-map.md / .pi-map.index.md artifacts for a subtree plus its ancestors, falling back to full regeneration only when the subtree covers more than the configured percentage of project files (default: 10%)",
|
||||
promptSnippet:
|
||||
"Force full regeneration of paired project map/index artifacts",
|
||||
"Regenerate paired project map/index artifacts for a subtree or the whole project",
|
||||
promptGuidelines: [
|
||||
"Use project_map_reinit when validation shows widespread staleness",
|
||||
"Use project_map_reinit after pulling major changes from version control",
|
||||
"Use project_map_reinit only after project_map_patch and project_map_validate --fix cannot resolve the staleness",
|
||||
"For localized changes, prefer project_map_patch <changed-file> or project_map_validate with fix=true",
|
||||
"Use project_map_reinit for widespread structural damage (e.g. broken links across many directories) or after large merges",
|
||||
"When reinit falls back to full regeneration, it is because the target subtree covers more than the configured reinitFullThresholdPercent of project files",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
path: Type.Optional(
|
||||
@@ -364,7 +377,7 @@ export default function (pi: ExtensionAPI) {
|
||||
|
||||
if (dirtyFiles.length > 0) {
|
||||
ctx.ui.notify(
|
||||
`pi-project-map: ${dirtyFiles.length} dirty packages detected. Run project_map_validate or project_map_reinit.`,
|
||||
`pi-project-map: ${dirtyFiles.length} dirty package(s) detected. Run project_map_validate first; use project_map_reinit only if staleness is widespread.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ function printUsage() {
|
||||
` project-map ${pc.cyan("validate")} [--fix] [path] Check for stale/missing/orphaned entries`,
|
||||
);
|
||||
console.log(
|
||||
` project-map ${pc.cyan("reinit")} [path] Force full regeneration`,
|
||||
` project-map ${pc.cyan("reinit")} [path] Regenerate a subtree or the whole project`,
|
||||
);
|
||||
console.log(
|
||||
` project-map ${pc.cyan("--help")} Show this help message`,
|
||||
@@ -252,7 +252,7 @@ async function main() {
|
||||
await reinitPath(targetPath, {
|
||||
verbose: false,
|
||||
llmClient: client,
|
||||
cacheDir: targetPath,
|
||||
cacheDir: process.cwd(),
|
||||
onProgress: (info) => {
|
||||
const line = renderProgressBar(
|
||||
info.completed,
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface SkillConfig {
|
||||
promptInjectionMode: PromptInjectionMode;
|
||||
contextBudgetPercent: number;
|
||||
contextBudgetMaxTokens: number;
|
||||
/** Percentage of project files a target subtree must cover before reinit falls back to full regeneration (default: 10). */
|
||||
reinitFullThresholdPercent: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: SkillConfig = {
|
||||
@@ -51,6 +53,7 @@ export const DEFAULT_CONFIG: SkillConfig = {
|
||||
promptInjectionMode: "strong",
|
||||
contextBudgetPercent: 15,
|
||||
contextBudgetMaxTokens: 100_000,
|
||||
reinitFullThresholdPercent: 10,
|
||||
};
|
||||
|
||||
export function loadConfig(cwd: string = process.cwd()): SkillConfig {
|
||||
|
||||
+131
-4
@@ -7,8 +7,8 @@ import { extractFileLLM, extractPackageLLM } from "./llm/llm-extract.js";
|
||||
import { extractFileAST } from "./ast/ast-extract.js";
|
||||
import { mergeFileData } from "./merge.js";
|
||||
import { processFiles } from "./llm/llm-batch.js";
|
||||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
import { join, relative, resolve } from "path";
|
||||
import type { LLMClient } from "./llm/llm-client.js";
|
||||
import {
|
||||
createDirectoryModel,
|
||||
@@ -105,6 +105,60 @@ export interface DirectoryContext {
|
||||
isRoot: boolean;
|
||||
}
|
||||
|
||||
export function getAncestorEntries(
|
||||
entries: DirectoryEntry[],
|
||||
ctx: DirectoryContext,
|
||||
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 countFiles(entries: DirectoryEntry[]): number {
|
||||
return entries.reduce((sum, e) => sum + e.files.length, 0);
|
||||
}
|
||||
|
||||
function isSubdirectory(parent: string, child: string): boolean {
|
||||
if (parent === ".") return true;
|
||||
if (child === parent) return true;
|
||||
return child.startsWith(`${parent}/`);
|
||||
}
|
||||
|
||||
function findProjectRoot(targetPath: string): string {
|
||||
let current = resolve(targetPath);
|
||||
while (true) {
|
||||
if (existsSync(join(current, ".pi-project-map.json"))) {
|
||||
return current;
|
||||
}
|
||||
if (existsSync(join(current, ".git"))) {
|
||||
return current;
|
||||
}
|
||||
if (existsSync(join(current, "package.json"))) {
|
||||
return current;
|
||||
}
|
||||
const parent = resolve(current, "..");
|
||||
if (parent === current) {
|
||||
return resolve(targetPath);
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRelativePath(relPath: string): string {
|
||||
if (!relPath || relPath === ".") return ".";
|
||||
return relPath.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
export type ArtifactWriteMode = "both" | "map" | "index";
|
||||
|
||||
export function buildDirectoryContext(
|
||||
@@ -244,8 +298,81 @@ export async function reinitPath(
|
||||
path: string,
|
||||
options: InitOptions = {},
|
||||
): Promise<void> {
|
||||
// Full regeneration clears all dirty markers by overwriting every map/index pair
|
||||
await initProject(path, options);
|
||||
const targetPath = resolve(path);
|
||||
const rootPath = findProjectRoot(targetPath);
|
||||
const targetRelPath =
|
||||
rootPath === targetPath
|
||||
? "."
|
||||
: normalizeRelativePath(relative(rootPath, targetPath));
|
||||
const entries = discoverProject(rootPath);
|
||||
const config = loadConfig(rootPath);
|
||||
const threshold = config.reinitFullThresholdPercent;
|
||||
|
||||
const totalFiles = countFiles(entries);
|
||||
if (totalFiles === 0 || targetRelPath === ".") {
|
||||
await initProject(rootPath, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const subtreeEntries = entries.filter((entry) =>
|
||||
isSubdirectory(targetRelPath, entry.relativePath),
|
||||
);
|
||||
const subtreeFiles = countFiles(subtreeEntries);
|
||||
const percentage = (subtreeFiles / totalFiles) * 100;
|
||||
|
||||
if (percentage > threshold) {
|
||||
await initProject(rootPath, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const routingOpts: RoutingMetadataOptions = {
|
||||
tagCap: options.tagCap ?? config.tagCap,
|
||||
workflowHintCap: options.workflowHintCap ?? config.workflowHintCap,
|
||||
};
|
||||
|
||||
const targetEntry = entries.find((e) => e.relativePath === targetRelPath);
|
||||
if (!targetEntry) {
|
||||
await initProject(rootPath, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const changedCtx = buildDirectoryContext(entries, targetEntry);
|
||||
const ancestors = getAncestorEntries(entries, changedCtx, targetEntry);
|
||||
const dirsToRegenerate = new Set<DirectoryEntry>([
|
||||
...subtreeEntries,
|
||||
...ancestors,
|
||||
]);
|
||||
|
||||
const dirs = Array.from(dirsToRegenerate);
|
||||
const filesToRegenerate = countFiles(dirs);
|
||||
let completedFiles = 0;
|
||||
|
||||
for (const entry of dirs) {
|
||||
const ctx = buildDirectoryContext(entries, entry);
|
||||
await generateDirectoryArtifacts(
|
||||
entry,
|
||||
ctx,
|
||||
options.llmClient,
|
||||
options.cacheDir,
|
||||
(info) => {
|
||||
options.onProgress?.({
|
||||
...info,
|
||||
completed: completedFiles + info.completed,
|
||||
total: filesToRegenerate,
|
||||
dir: entry.relativePath,
|
||||
});
|
||||
},
|
||||
routingOpts,
|
||||
"both",
|
||||
);
|
||||
completedFiles += entry.files.length;
|
||||
}
|
||||
|
||||
if (options.verbose !== false) {
|
||||
console.log(
|
||||
`Smart reinit: regenerated ${dirsToRegenerate.size} directories under ${targetRelPath} (${subtreeFiles}/${totalFiles} files, ${percentage.toFixed(1)}%).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Backward-compatible wrapper for patch/validate compatibility
|
||||
|
||||
+1
-19
@@ -5,6 +5,7 @@ import {
|
||||
discoverProject,
|
||||
generateDirectoryArtifacts,
|
||||
buildDirectoryContext,
|
||||
getAncestorEntries,
|
||||
} from "./init.js";
|
||||
import type { DirectoryEntry } from "./discover.js";
|
||||
import type { LLMClient } from "./llm/llm-client.js";
|
||||
@@ -133,25 +134,6 @@ function countDirectChildren(rootPath: string, relDir: string): number {
|
||||
}).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, "/");
|
||||
|
||||
+1
-1
@@ -291,6 +291,6 @@ function renderNoResultsBundle(query: string): string {
|
||||
"-",
|
||||
"",
|
||||
"## instructions",
|
||||
"No relevant directories found. Try rephrasing the query or run `project_map_reinit` if artifacts are stale.",
|
||||
"No relevant directories found. Try rephrasing the query, or run `project_map_validate` to check whether artifacts are stale.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
+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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ vi.mock("typebox", () => ({
|
||||
Object: (props: unknown) => props,
|
||||
Optional: (prop: unknown) => prop,
|
||||
String: (opts: unknown) => ({ type: "string", ...(opts as object) }),
|
||||
Boolean: (opts: unknown) => ({ type: "boolean", ...(opts as object) }),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -251,7 +252,7 @@ describe("pi-extension", () => {
|
||||
await handler(null, mockCtx);
|
||||
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.stringContaining("1 dirty packages detected"),
|
||||
expect.stringContaining("1 dirty package(s) detected"),
|
||||
"warning",
|
||||
);
|
||||
});
|
||||
|
||||
+5
-4
@@ -10,7 +10,7 @@ A source file was edited, added, or deleted without running `project_map_patch`
|
||||
|
||||
### What to do
|
||||
1. run `project_map_validate` to inspect discrepancies
|
||||
2. if the list is small and localized, run `project_map_patch <changed-file>`
|
||||
2. if the list is small and localized, run `project_map_patch <changed-file>` or `project_map_validate` with `fix=true`
|
||||
3. if the list is large or structural, run `project_map_reinit`
|
||||
4. re-run `project_map_validate` to confirm clean state
|
||||
|
||||
@@ -38,14 +38,14 @@ Many `stale-signature` discrepancies appear.
|
||||
AST exports no longer match listed exports, or generated signatures are stale.
|
||||
|
||||
### Fix
|
||||
- patch the changed file/directory
|
||||
- patch the changed file/directory, or run `project-map validate --fix`
|
||||
- if widespread, reinit and validate again
|
||||
|
||||
### Symptom
|
||||
`broken-link` appears after moving directories.
|
||||
|
||||
### Fix
|
||||
Run `project_map_reinit`.
|
||||
Run `project-map validate --fix` first. If the damage spans many directories, run `project_map_reinit`.
|
||||
|
||||
## Prompt injection mode surprises
|
||||
|
||||
@@ -67,12 +67,13 @@ No project-map context appears in a Pi session.
|
||||
Root pair is injected repeatedly.
|
||||
|
||||
### Likely cause
|
||||
Marker scanning failed or artifact invalidation forced reinjection.
|
||||
Marker scanning failed or artifact invalidation forced reinjection, often because `project_map_reinit` is being used instead of `project_map_patch` or `project_map_validate --fix`.
|
||||
|
||||
### Fix
|
||||
- inspect whether `<!-- PI_MAP_ROOT_PAIR_START -->` is present in outgoing context
|
||||
- check whether root artifacts changed on disk
|
||||
- confirm payload/message serialization still exposes marker text
|
||||
- prefer `project_map_patch <file>` for edits and `project_map_validate` with `fix=true` for localized discrepancies
|
||||
|
||||
### Symptom
|
||||
`advisory` mode shows a reminder but no maps are loaded.
|
||||
|
||||
+25
-12
@@ -9,7 +9,8 @@ For Pi agents and advanced users who want predictable, low-friction navigation a
|
||||
| New project, no `.pi-map.md` files yet, or artifacts are severely outdated | `project_map_init` / `project-map init` |
|
||||
| You just edited one or more source files | `project_map_patch <file>` / `project-map patch <file>` |
|
||||
| You suspect stale data, or you are about to make an architectural decision | `project_map_validate` / `project-map validate` |
|
||||
| Validation shows widespread staleness, or you pulled major changes from version control | `project_map_reinit` / `project-map reinit` |
|
||||
| Localized discrepancies detected by validate | `project_map_validate` with `fix=true` / `project-map validate --fix` |
|
||||
| Widespread structural staleness, or you pulled major changes from version control | `project_map_reinit` / `project-map reinit` |
|
||||
| You have a specific question like "where is auth handled?" | `project_map_context <query>` / `project-map context <query>` |
|
||||
|
||||
## Command behavior
|
||||
@@ -18,11 +19,11 @@ For Pi agents and advanced users who want predictable, low-friction navigation a
|
||||
Run once when you start work on a repo, or after large restructuring. It discovers every non-ignored directory, analyzes files with LLM + AST, and writes both `.pi-map.md` and `.pi-map.index.md` for every directory.
|
||||
|
||||
### patch
|
||||
Run **immediately after editing a source file**. The command regenerates the pair for that file’s directory and refreshes ancestor artifacts.
|
||||
Run **immediately after editing a source file**. The command regenerates the pair for that file's directory and refreshes ancestor artifacts.
|
||||
|
||||
Patch mode is chosen automatically:
|
||||
- **small** — refresh ancestor indexes only
|
||||
- **structural** — refresh ancestor map/index pairs
|
||||
- **small** - refresh ancestor indexes only
|
||||
- **structural** - refresh ancestor map/index pairs
|
||||
|
||||
You can force a mode with `project-map patch <file> --patch-mode=small|structural`.
|
||||
`auto` remains the default.
|
||||
@@ -38,7 +39,9 @@ Run before architectural decisions, broad refactors, or final handoff. It checks
|
||||
Use `project-map validate --fix` to repair affected chains. `--fix` requires an LLM client.
|
||||
|
||||
### reinit
|
||||
Use sparingly. It regenerates every pair from scratch and is the blunt instrument for widespread staleness.
|
||||
Use as a last resort. By default, `project_map_reinit` (or `project-map reinit [path]`) regenerates only the target subtree plus its ancestor directories up to the root. It falls back to full regeneration only when the target subtree covers more than `reinitFullThresholdPercent` of the project's files (default: 10%).
|
||||
|
||||
Reach for reinit when `project_map_patch` and `project_map_validate --fix` cannot repair widespread structural damage (e.g. broken parent/child links across many directories) or after a large merge.
|
||||
|
||||
### context
|
||||
Use when you know what you are looking for:
|
||||
@@ -57,12 +60,14 @@ The returned bundle is deterministic and ranked. Read indexes first, then strong
|
||||
|
||||
1. read the root `.pi-map.index.md` and the `Project Map Protocol`
|
||||
2. use the root index to find the relevant child directory
|
||||
3. read that directory’s `.pi-map.index.md`, then its `.pi-map.md`
|
||||
3. read that directory's `.pi-map.index.md`, then its `.pi-map.md`
|
||||
4. read the relevant source files
|
||||
5. edit source
|
||||
6. run `project_map_patch <changed-file>`
|
||||
7. run tests/build
|
||||
8. run `project_map_validate` before architectural summary or handoff
|
||||
9. if validation reports localized discrepancies, run `project_map_validate` with `fix=true` (or `project-map validate --fix`)
|
||||
10. only if discrepancies are widespread or structural, run `project_map_reinit`
|
||||
|
||||
### Exploring an unfamiliar area
|
||||
|
||||
@@ -76,7 +81,9 @@ Treat the returned bundle as a ranked entry point, not as truth.
|
||||
|
||||
```bash
|
||||
project-map validate
|
||||
# if many discrepancies:
|
||||
# if localized discrepancies:
|
||||
project-map validate --fix
|
||||
# if widespread structural damage:
|
||||
project-map reinit
|
||||
```
|
||||
|
||||
@@ -84,7 +91,9 @@ project-map reinit
|
||||
|
||||
```bash
|
||||
project-map validate
|
||||
# if needed:
|
||||
# if localized discrepancies:
|
||||
project-map validate --fix
|
||||
# if widespread structural damage:
|
||||
project-map reinit
|
||||
```
|
||||
|
||||
@@ -155,9 +164,10 @@ Use bypass markers sparingly.
|
||||
|
||||
```text
|
||||
1. project-map validate
|
||||
2. If clean, read root .pi-map.md and key directory maps
|
||||
3. Cross-check claims against source
|
||||
4. If stale, run project-map reinit first
|
||||
2. If localized discrepancies: project-map validate --fix
|
||||
3. If clean, read root .pi-map.md and key directory maps
|
||||
4. Cross-check claims against source
|
||||
5. Only if widespread: run project-map reinit
|
||||
```
|
||||
|
||||
## Retrieval vs automatic injection
|
||||
@@ -175,7 +185,8 @@ Use both together: injection for baseline orientation, retrieval for focused ent
|
||||
|
||||
- patch after every edit
|
||||
- validate before architectural claims
|
||||
- reinit when many artifacts are stale or after large merges
|
||||
- use `validate --fix` for localized discrepancies
|
||||
- use reinit only for widespread structural staleness or after large merges
|
||||
- watch for the Pi extension warning about dirty packages on session start
|
||||
|
||||
## Configuration quick reference
|
||||
@@ -187,6 +198,7 @@ Use both together: injection for baseline orientation, retrieval for focused ent
|
||||
"contextBudgetMaxTokens": 100000,
|
||||
"tagCap": 8,
|
||||
"workflowHintCap": 5,
|
||||
"reinitFullThresholdPercent": 10,
|
||||
"ignorePatterns": ["node_modules", ".git", "dist", "build"]
|
||||
}
|
||||
```
|
||||
@@ -195,4 +207,5 @@ Providing `ignorePatterns` replaces the built-in default list, so include any de
|
||||
|
||||
- lower `contextBudgetPercent` / `contextBudgetMaxTokens` to reduce token use
|
||||
- raise them if you want deeper auto-loaded context in large projects
|
||||
- `reinitFullThresholdPercent` controls when `project-map reinit [path]` falls back to full regeneration
|
||||
- `strict` is the safest enforcement mode; `strong` is the best default for everyday work
|
||||
|
||||
Reference in New Issue
Block a user