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:
Developer
2026-06-16 11:46:48 +00:00
parent 5f1c107667
commit cb581f44b9
11 changed files with 384 additions and 54 deletions
+8 -3
View File
@@ -44,8 +44,10 @@ When project-map artifacts exist in the repo:
3. read the local `.pi-map.md` plus relevant source before editing 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 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 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) 6. if validation shows localized discrepancies, run `project_map_validate` with `fix=true` (tool) or `project-map validate --fix` (CLI)
7. in `strict` mode, only bypass the protocol-path guard with an explicit marker: `[PI_MAP_BYPASS: <brief justification>]` 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 ## Prompt injection modes
@@ -102,12 +104,15 @@ Create `.pi-project-map.json` in the project root:
"workflowHintCap": 5, "workflowHintCap": 5,
"llmProvider": "openai", "llmProvider": "openai",
"llmModel": "gpt-4o-mini", "llmModel": "gpt-4o-mini",
"reinitFullThresholdPercent": 10,
"ignorePatterns": ["node_modules", ".git", "dist", "build"] "ignorePatterns": ["node_modules", ".git", "dist", "build"]
} }
``` ```
Providing `ignorePatterns` replaces the built-in default list, so include any defaults you want to keep. 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: Knobs that matter in practice:
- `promptInjectionMode``off` | `advisory` | `strong` | `strict` - `promptInjectionMode``off` | `advisory` | `strong` | `strict`
- `contextBudgetPercent` / `contextBudgetMaxTokens` — caps automatic map/index injection - `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_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_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_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. | | `project_map_context` | `project-map context <query>` | Retrieve a ranked context bundle for a natural-language query. |
+20 -7
View File
@@ -209,11 +209,12 @@ export default function (pi: ExtensionAPI) {
name: "project_map_validate", name: "project_map_validate",
label: "Project Map Validate", label: "Project Map Validate",
description: 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", promptSnippet: "Validate paired project map/index artifacts for accuracy",
promptGuidelines: [ promptGuidelines: [
"Use project_map_validate before making architectural decisions if you suspect stale data", "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", "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({ parameters: Type.Object({
path: Type.Optional( path: Type.Optional(
@@ -221,13 +222,23 @@ export default function (pi: ExtensionAPI) {
description: "Project root path (default: current directory)", 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) { async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
try { try {
const targetPath = params.path || ctx.cwd; const targetPath = params.path || ctx.cwd;
const client = params.fix ? getPiLLMClient(ctx) : undefined;
const result = await validateMaps(targetPath, { const result = await validateMaps(targetPath, {
fix: false, fix: params.fix ?? false,
verbose: false, verbose: false,
llmClient: client,
cacheDir: ctx.cwd,
}); });
const text = result.clean const text = result.clean
? "All .pi-map.md files are clean." ? "All .pi-map.md files are clean."
@@ -253,12 +264,14 @@ export default function (pi: ExtensionAPI) {
name: "project_map_reinit", name: "project_map_reinit",
label: "Project Map Reinit", label: "Project Map Reinit",
description: 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: promptSnippet:
"Force full regeneration of paired project map/index artifacts", "Regenerate paired project map/index artifacts for a subtree or the whole project",
promptGuidelines: [ promptGuidelines: [
"Use project_map_reinit when validation shows widespread staleness", "Use project_map_reinit only after project_map_patch and project_map_validate --fix cannot resolve the staleness",
"Use project_map_reinit after pulling major changes from version control", "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({ parameters: Type.Object({
path: Type.Optional( path: Type.Optional(
@@ -364,7 +377,7 @@ export default function (pi: ExtensionAPI) {
if (dirtyFiles.length > 0) { if (dirtyFiles.length > 0) {
ctx.ui.notify( 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", "warning",
); );
} }
+2 -2
View File
@@ -28,7 +28,7 @@ function printUsage() {
` project-map ${pc.cyan("validate")} [--fix] [path] Check for stale/missing/orphaned entries`, ` project-map ${pc.cyan("validate")} [--fix] [path] Check for stale/missing/orphaned entries`,
); );
console.log( 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( console.log(
` project-map ${pc.cyan("--help")} Show this help message`, ` project-map ${pc.cyan("--help")} Show this help message`,
@@ -252,7 +252,7 @@ async function main() {
await reinitPath(targetPath, { await reinitPath(targetPath, {
verbose: false, verbose: false,
llmClient: client, llmClient: client,
cacheDir: targetPath, cacheDir: process.cwd(),
onProgress: (info) => { onProgress: (info) => {
const line = renderProgressBar( const line = renderProgressBar(
info.completed, info.completed,
+3
View File
@@ -16,6 +16,8 @@ export interface SkillConfig {
promptInjectionMode: PromptInjectionMode; promptInjectionMode: PromptInjectionMode;
contextBudgetPercent: number; contextBudgetPercent: number;
contextBudgetMaxTokens: 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 = { export const DEFAULT_CONFIG: SkillConfig = {
@@ -51,6 +53,7 @@ export const DEFAULT_CONFIG: SkillConfig = {
promptInjectionMode: "strong", promptInjectionMode: "strong",
contextBudgetPercent: 15, contextBudgetPercent: 15,
contextBudgetMaxTokens: 100_000, contextBudgetMaxTokens: 100_000,
reinitFullThresholdPercent: 10,
}; };
export function loadConfig(cwd: string = process.cwd()): SkillConfig { export function loadConfig(cwd: string = process.cwd()): SkillConfig {
+131 -4
View File
@@ -7,8 +7,8 @@ import { extractFileLLM, extractPackageLLM } from "./llm/llm-extract.js";
import { extractFileAST } from "./ast/ast-extract.js"; import { extractFileAST } from "./ast/ast-extract.js";
import { mergeFileData } from "./merge.js"; import { mergeFileData } from "./merge.js";
import { processFiles } from "./llm/llm-batch.js"; import { processFiles } from "./llm/llm-batch.js";
import { writeFileSync } from "fs"; import { existsSync, writeFileSync } from "fs";
import { join } from "path"; import { join, relative, resolve } from "path";
import type { LLMClient } from "./llm/llm-client.js"; import type { LLMClient } from "./llm/llm-client.js";
import { import {
createDirectoryModel, createDirectoryModel,
@@ -105,6 +105,60 @@ export interface DirectoryContext {
isRoot: boolean; 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 type ArtifactWriteMode = "both" | "map" | "index";
export function buildDirectoryContext( export function buildDirectoryContext(
@@ -244,8 +298,81 @@ export async function reinitPath(
path: string, path: string,
options: InitOptions = {}, options: InitOptions = {},
): Promise<void> { ): Promise<void> {
// Full regeneration clears all dirty markers by overwriting every map/index pair const targetPath = resolve(path);
await initProject(path, options); 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 // Backward-compatible wrapper for patch/validate compatibility
+1 -19
View File
@@ -5,6 +5,7 @@ import {
discoverProject, discoverProject,
generateDirectoryArtifacts, generateDirectoryArtifacts,
buildDirectoryContext, buildDirectoryContext,
getAncestorEntries,
} from "./init.js"; } from "./init.js";
import type { DirectoryEntry } from "./discover.js"; import type { DirectoryEntry } from "./discover.js";
import type { LLMClient } from "./llm/llm-client.js"; import type { LLMClient } from "./llm/llm-client.js";
@@ -133,25 +134,6 @@ function countDirectChildren(rootPath: string, relDir: string): number {
}).length; }).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 { function normalizeRelativePath(relPath: string): string {
if (!relPath || relPath === ".") return "."; if (!relPath || relPath === ".") return ".";
return relPath.replace(/\\/g, "/"); return relPath.replace(/\\/g, "/");
+1 -1
View File
@@ -291,6 +291,6 @@ function renderNoResultsBundle(query: string): string {
"-", "-",
"", "",
"## instructions", "## 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"); ].join("\n");
} }
+186 -1
View File
@@ -8,7 +8,7 @@ import {
} from "fs"; } from "fs";
import { join } from "path"; import { join } from "path";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { initProject } from "../src/init.js"; import { initProject, reinitPath } from "../src/init.js";
import { patchFile } from "../src/patch.js"; import { patchFile } from "../src/patch.js";
import { validateMaps } from "../src/validate.js"; import { validateMaps } from "../src/validate.js";
import { createMockFileClient } from "./mock-llm.js"; import { createMockFileClient } from "./mock-llm.js";
@@ -356,4 +356,189 @@ describe("integration", () => {
readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"), readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"),
).toContain("# src (index)"); ).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",
);
});
}); });
+2 -1
View File
@@ -16,6 +16,7 @@ vi.mock("typebox", () => ({
Object: (props: unknown) => props, Object: (props: unknown) => props,
Optional: (prop: unknown) => prop, Optional: (prop: unknown) => prop,
String: (opts: unknown) => ({ type: "string", ...(opts as object) }), 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); await handler(null, mockCtx);
expect(mockNotify).toHaveBeenCalledWith( expect(mockNotify).toHaveBeenCalledWith(
expect.stringContaining("1 dirty packages detected"), expect.stringContaining("1 dirty package(s) detected"),
"warning", "warning",
); );
}); });
+5 -4
View File
@@ -10,7 +10,7 @@ A source file was edited, added, or deleted without running `project_map_patch`
### What to do ### What to do
1. run `project_map_validate` to inspect discrepancies 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` 3. if the list is large or structural, run `project_map_reinit`
4. re-run `project_map_validate` to confirm clean state 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. AST exports no longer match listed exports, or generated signatures are stale.
### Fix ### Fix
- patch the changed file/directory - patch the changed file/directory, or run `project-map validate --fix`
- if widespread, reinit and validate again - if widespread, reinit and validate again
### Symptom ### Symptom
`broken-link` appears after moving directories. `broken-link` appears after moving directories.
### Fix ### 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 ## Prompt injection mode surprises
@@ -67,12 +67,13 @@ No project-map context appears in a Pi session.
Root pair is injected repeatedly. Root pair is injected repeatedly.
### Likely cause ### 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 ### Fix
- inspect whether `<!-- PI_MAP_ROOT_PAIR_START -->` is present in outgoing context - inspect whether `<!-- PI_MAP_ROOT_PAIR_START -->` is present in outgoing context
- check whether root artifacts changed on disk - check whether root artifacts changed on disk
- confirm payload/message serialization still exposes marker text - 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 ### Symptom
`advisory` mode shows a reminder but no maps are loaded. `advisory` mode shows a reminder but no maps are loaded.
+25 -12
View File
@@ -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` | | 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 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` | | 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>` | | You have a specific question like "where is auth handled?" | `project_map_context <query>` / `project-map context <query>` |
## Command behavior ## 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. 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 ### patch
Run **immediately after editing a source file**. The command regenerates the pair for that files 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: Patch mode is chosen automatically:
- **small** refresh ancestor indexes only - **small** - refresh ancestor indexes only
- **structural** refresh ancestor map/index pairs - **structural** - refresh ancestor map/index pairs
You can force a mode with `project-map patch <file> --patch-mode=small|structural`. You can force a mode with `project-map patch <file> --patch-mode=small|structural`.
`auto` remains the default. `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. Use `project-map validate --fix` to repair affected chains. `--fix` requires an LLM client.
### reinit ### 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 ### context
Use when you know what you are looking for: 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` 1. read the root `.pi-map.index.md` and the `Project Map Protocol`
2. use the root index to find the relevant child directory 2. use the root index to find the relevant child directory
3. read that directorys `.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 4. read the relevant source files
5. edit source 5. edit source
6. run `project_map_patch <changed-file>` 6. run `project_map_patch <changed-file>`
7. run tests/build 7. run tests/build
8. run `project_map_validate` before architectural summary or handoff 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 ### Exploring an unfamiliar area
@@ -76,7 +81,9 @@ Treat the returned bundle as a ranked entry point, not as truth.
```bash ```bash
project-map validate project-map validate
# if many discrepancies: # if localized discrepancies:
project-map validate --fix
# if widespread structural damage:
project-map reinit project-map reinit
``` ```
@@ -84,7 +91,9 @@ project-map reinit
```bash ```bash
project-map validate project-map validate
# if needed: # if localized discrepancies:
project-map validate --fix
# if widespread structural damage:
project-map reinit project-map reinit
``` ```
@@ -155,9 +164,10 @@ Use bypass markers sparingly.
```text ```text
1. project-map validate 1. project-map validate
2. If clean, read root .pi-map.md and key directory maps 2. If localized discrepancies: project-map validate --fix
3. Cross-check claims against source 3. If clean, read root .pi-map.md and key directory maps
4. If stale, run project-map reinit first 4. Cross-check claims against source
5. Only if widespread: run project-map reinit
``` ```
## Retrieval vs automatic injection ## Retrieval vs automatic injection
@@ -175,7 +185,8 @@ Use both together: injection for baseline orientation, retrieval for focused ent
- patch after every edit - patch after every edit
- validate before architectural claims - 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 - watch for the Pi extension warning about dirty packages on session start
## Configuration quick reference ## Configuration quick reference
@@ -187,6 +198,7 @@ Use both together: injection for baseline orientation, retrieval for focused ent
"contextBudgetMaxTokens": 100000, "contextBudgetMaxTokens": 100000,
"tagCap": 8, "tagCap": 8,
"workflowHintCap": 5, "workflowHintCap": 5,
"reinitFullThresholdPercent": 10,
"ignorePatterns": ["node_modules", ".git", "dist", "build"] "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 - lower `contextBudgetPercent` / `contextBudgetMaxTokens` to reduce token use
- raise them if you want deeper auto-loaded context in large projects - 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 - `strict` is the safest enforcement mode; `strong` is the best default for everyday work