From cb581f44b95f1ad26d593def9057df78e9c7bca8 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 16 Jun 2026 11:46:48 +0000 Subject: [PATCH] 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. --- SKILL.md | 11 ++- pi-extension.ts | 27 ++++-- src/cli/cli.ts | 4 +- src/config.ts | 3 + src/init.ts | 135 +++++++++++++++++++++++++- src/patch.ts | 20 +--- src/retrieve.ts | 2 +- tests/integration.test.ts | 187 ++++++++++++++++++++++++++++++++++++- tests/pi-extension.test.ts | 3 +- troubleshooting.md | 9 +- usage-guide.md | 37 +++++--- 11 files changed, 384 insertions(+), 54 deletions(-) diff --git a/SKILL.md b/SKILL.md index 9c1cf9c..5a17e55 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 ` (tool) or `project-map patch ` (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 ` (tool) or `project-map context ` (CLI) -7. in `strict` mode, only bypass the protocol-path guard with an explicit marker: `[PI_MAP_BYPASS: ]` +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 ` (tool) or `project-map context ` (CLI) +9. in `strict` mode, only bypass the protocol-path guard with an explicit marker: `[PI_MAP_BYPASS: ]` ## 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 ` | 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 ` | Retrieve a ranked context bundle for a natural-language query. | diff --git a/pi-extension.ts b/pi-extension.ts index 308f855..8f8328c 100644 --- a/pi-extension.ts +++ b/pi-extension.ts @@ -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 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", ); } diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 7bf5ac1..836703e 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -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, diff --git a/src/config.ts b/src/config.ts index 931df96..9eef908 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 { diff --git a/src/init.ts b/src/init.ts index 334c770..3f481f8 100644 --- a/src/init.ts +++ b/src/init.ts @@ -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 { - // 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([ + ...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 diff --git a/src/patch.ts b/src/patch.ts index 64b3c04..130985a 100644 --- a/src/patch.ts +++ b/src/patch.ts @@ -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, - 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, "/"); diff --git a/src/retrieve.ts b/src/retrieve.ts index 537e63b..15dac26 100644 --- a/src/retrieve.ts +++ b/src/retrieve.ts @@ -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"); } diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 40766be..f37b855 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -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", + ); + }); }); diff --git a/tests/pi-extension.test.ts b/tests/pi-extension.test.ts index 6256213..b593093 100644 --- a/tests/pi-extension.test.ts +++ b/tests/pi-extension.test.ts @@ -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", ); }); diff --git a/troubleshooting.md b/troubleshooting.md index b4323ea..58cc5b8 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -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 ` +2. if the list is small and localized, run `project_map_patch ` 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 `` is present in outgoing context - check whether root artifacts changed on disk - confirm payload/message serialization still exposes marker text +- prefer `project_map_patch ` for edits and `project_map_validate` with `fix=true` for localized discrepancies ### Symptom `advisory` mode shows a reminder but no maps are loaded. diff --git a/usage-guide.md b/usage-guide.md index 30229c8..9881ed8 100644 --- a/usage-guide.md +++ b/usage-guide.md @@ -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 ` / `project-map patch ` | | 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 ` / `project-map context ` | ## 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 --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 ` 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