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
|
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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 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:
|
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 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
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user