Implement layered maps and context retrieval

This commit is contained in:
2026-06-11 12:56:18 +02:00
parent 010e4b83eb
commit c6064f8d94
35 changed files with 4410 additions and 383 deletions
+166 -21
View File
@@ -1,9 +1,8 @@
import { discoverProject, type DirectoryEntry } from "./discover.js";
import {
renderPackageMap,
type PackageMapData,
type FileEntry,
} from "./format.js";
export { discoverProject };
import type { FileEntry } from "./directory-model.js";
import { renderDirectoryIndex, renderDirectoryMap } from "./format.js";
import { extractFileLLM, extractPackageLLM } from "./llm/llm-extract.js";
import { extractFileAST } from "./ast/ast-extract.js";
import { mergeFileData } from "./merge.js";
@@ -11,6 +10,13 @@ import { processFiles } from "./llm/llm-batch.js";
import { writeFileSync } from "fs";
import { join } from "path";
import type { LLMClient } from "./llm/llm-client.js";
import {
createDirectoryModel,
type DirectoryArtifactModel,
type RoutingMetadataOptions,
} from "./directory-model.js";
import { populateRoutingMetadata } from "./routing-metadata.js";
import { loadConfig } from "./config.js";
export interface ProgressInfo {
message: string;
@@ -25,6 +31,8 @@ export interface InitOptions {
llmClient?: LLMClient;
cacheDir?: string;
onProgress?: (info: ProgressInfo) => void;
tagCap?: number;
workflowHintCap?: number;
}
export async function initProject(
@@ -35,22 +43,40 @@ export async function initProject(
const totalFiles = entries.reduce((sum, e) => sum + e.files.length, 0);
let globalCompleted = 0;
// Load project config and apply defaults when not overridden in options
const config = loadConfig(rootPath);
const routingOpts: RoutingMetadataOptions = {
tagCap: options.tagCap ?? config.tagCap,
workflowHintCap: options.workflowHintCap ?? config.workflowHintCap,
};
options.onProgress?.({
message: `Scanning ${entries.length} directories (${totalFiles} files)...`,
completed: 0,
total: totalFiles,
});
// Build directory relationships
const dirSet = new Set(entries.map((e) => e.relativePath));
const parentMap = buildParentMap(entries);
const childrenMap = buildChildrenMap(entries);
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
await generateDirectoryMap(
await generateDirectoryArtifacts(
entry,
{
dirSet,
parentMap,
childrenMap,
isRoot: entry.relativePath === ".",
},
options.llmClient,
options.cacheDir,
(info) => {
globalCompleted = info.completed + entries
.slice(0, i)
.reduce((sum, e) => sum + e.files.length, 0);
globalCompleted =
info.completed +
entries.slice(0, i).reduce((sum, e) => sum + e.files.length, 0);
options.onProgress?.({
...info,
completed: globalCompleted,
@@ -58,26 +84,83 @@ export async function initProject(
dir: entry.relativePath,
});
},
routingOpts,
);
}
options.onProgress?.({
message: `Generated ${entries.length} .pi-map.md files`,
message: `Generated ${entries.length} directory map/index pairs`,
completed: totalFiles,
total: totalFiles,
});
if (options.verbose !== false) {
console.log(`Generated ${entries.length} .pi-map.md files`);
console.log(`Generated ${entries.length} directory map/index pairs`);
}
}
export async function generateDirectoryMap(
export interface DirectoryContext {
dirSet: Set<string>;
parentMap: Map<string, string | undefined>;
childrenMap: Map<string, string[]>;
isRoot: boolean;
}
export type ArtifactWriteMode = "both" | "map" | "index";
export function buildDirectoryContext(
entries: DirectoryEntry[],
targetEntry: DirectoryEntry,
): DirectoryContext {
return {
dirSet: new Set(entries.map((e) => e.relativePath)),
parentMap: buildParentMap(entries),
childrenMap: buildChildrenMap(entries),
isRoot: targetEntry.relativePath === ".",
};
}
function buildParentMap(
entries: DirectoryEntry[],
): Map<string, string | undefined> {
const map = new Map<string, string | undefined>();
for (const entry of entries) {
const rel = entry.relativePath;
if (rel === ".") {
map.set(rel, undefined);
} else {
const lastSep = rel.lastIndexOf("/");
map.set(rel, lastSep >= 0 ? rel.slice(0, lastSep) : ".");
}
}
return map;
}
function buildChildrenMap(entries: DirectoryEntry[]): Map<string, string[]> {
const map = new Map<string, string[]>();
for (const entry of entries) {
map.set(entry.relativePath, []);
}
for (const entry of entries) {
const rel = entry.relativePath;
if (rel === ".") continue;
const lastSep = rel.lastIndexOf("/");
const parent = lastSep >= 0 ? rel.slice(0, lastSep) : ".";
const siblings = map.get(parent);
if (siblings) {
siblings.push(rel);
}
}
return map;
}
export async function buildDirectoryArtifactModel(
entry: DirectoryEntry,
ctx: DirectoryContext,
llmClient?: LLMClient,
cacheDir?: string,
onProgress?: (info: ProgressInfo) => void,
): Promise<FileEntry[]> {
// Process files in parallel (4 concurrent) with retry logic
routingOpts?: RoutingMetadataOptions,
): Promise<DirectoryArtifactModel> {
const fileData = await processFiles(
entry.files,
async (file) => {
@@ -105,23 +188,85 @@ export async function generateDirectoryMap(
cacheDir,
);
const mapData: PackageMapData = {
path: entry.relativePath,
const model: DirectoryArtifactModel = createDirectoryModel({
dir: entry.relativePath,
role: packageData.role,
files: fileData,
arch: packageData.arch,
parent: ctx.parentMap.get(entry.relativePath),
children: ctx.childrenMap.get(entry.relativePath) ?? [],
isRoot: ctx.isRoot,
dirty: "-",
};
});
const outPath = join(entry.dirPath, ".pi-map.md");
writeFileSync(outPath, renderPackageMap(mapData));
return fileData;
populateRoutingMetadata(model, routingOpts);
return model;
}
export function writeDirectoryArtifacts(
entry: DirectoryEntry,
model: DirectoryArtifactModel,
writeMode: ArtifactWriteMode = "both",
): void {
const mapPath = join(entry.dirPath, ".pi-map.md");
const indexPath = join(entry.dirPath, ".pi-map.index.md");
if (writeMode === "both" || writeMode === "map") {
writeFileSync(mapPath, renderDirectoryMap(model));
}
if (writeMode === "both" || writeMode === "index") {
writeFileSync(indexPath, renderDirectoryIndex(model));
}
}
export async function generateDirectoryArtifacts(
entry: DirectoryEntry,
ctx: DirectoryContext,
llmClient?: LLMClient,
cacheDir?: string,
onProgress?: (info: ProgressInfo) => void,
routingOpts?: RoutingMetadataOptions,
writeMode: ArtifactWriteMode = "both",
): Promise<FileEntry[]> {
const model = await buildDirectoryArtifactModel(
entry,
ctx,
llmClient,
cacheDir,
onProgress,
routingOpts,
);
writeDirectoryArtifacts(entry, model, writeMode);
return model.files;
}
export async function reinitPath(
path: string,
options: InitOptions = {},
): Promise<void> {
// Full regeneration clears all dirty markers by overwriting every .pi-map.md
// Full regeneration clears all dirty markers by overwriting every map/index pair
await initProject(path, options);
}
// Backward-compatible wrapper for patch/validate compatibility
export async function generateDirectoryMap(
entry: DirectoryEntry,
llmClient?: LLMClient,
cacheDir?: string,
): Promise<FileEntry[]> {
const entries = [entry];
const ctx = buildDirectoryContext(entries, entry);
const config = loadConfig(entry.dirPath);
const routingOpts: RoutingMetadataOptions = {
tagCap: config.tagCap,
workflowHintCap: config.workflowHintCap,
};
return generateDirectoryArtifacts(
entry,
ctx,
llmClient,
cacheDir,
undefined,
routingOpts,
);
}