Implement layered maps and context retrieval
This commit is contained in:
+250
-18
@@ -11,7 +11,7 @@ import { tmpdir } from "os";
|
||||
import { initProject } from "../src/init.js";
|
||||
import { patchFile } from "../src/patch.js";
|
||||
import { validateMaps } from "../src/validate.js";
|
||||
import { createMockFileClient, createMockPackageClient } from "./mock-llm.js";
|
||||
import { createMockFileClient } from "./mock-llm.js";
|
||||
|
||||
describe("integration", () => {
|
||||
let dir: string;
|
||||
@@ -24,14 +24,68 @@ describe("integration", () => {
|
||||
rmSync(dir, { recursive: true });
|
||||
});
|
||||
|
||||
it("init creates .pi-map.md files", async () => {
|
||||
it("init creates both .pi-map.md and .pi-map.index.md files", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
||||
const index = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8");
|
||||
expect(map).toContain("# src");
|
||||
expect(index).toContain("# src (index)");
|
||||
expect(index).toContain("dir: src");
|
||||
});
|
||||
|
||||
it("root artifacts contain Project Map Protocol", async () => {
|
||||
writeFileSync(join(dir, "package.json"), `{}\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8");
|
||||
const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8");
|
||||
|
||||
expect(rootMap).toContain("## Project Map Protocol");
|
||||
expect(rootMap).toContain("index: ./.pi-map.index.md");
|
||||
expect(rootIndex).toContain("## Project Map Protocol");
|
||||
expect(rootIndex).toContain("map: ./.pi-map.md");
|
||||
});
|
||||
|
||||
it("non-root map contains index link", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
||||
expect(map).toContain("index: src/.pi-map.index.md");
|
||||
expect(map).toContain("dir: src");
|
||||
});
|
||||
|
||||
it("index contains parent and children when applicable", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
mkdirSync(join(dir, "src", "utils"));
|
||||
writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`);
|
||||
writeFileSync(
|
||||
join(dir, "src", "utils", "helpers.ts"),
|
||||
`export const h = 1;\n`,
|
||||
);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const srcIndex = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8");
|
||||
expect(srcIndex).toContain("## parent");
|
||||
expect(srcIndex).toContain("index: ./.pi-map.index.md");
|
||||
|
||||
expect(srcIndex).toContain("## children");
|
||||
expect(srcIndex).toContain("- src/utils");
|
||||
|
||||
const utilsIndex = readFileSync(
|
||||
join(dir, "src", "utils", ".pi-map.index.md"),
|
||||
"utf8",
|
||||
);
|
||||
expect(utilsIndex).toContain("## parent");
|
||||
expect(utilsIndex).toContain("index: src/.pi-map.index.md");
|
||||
});
|
||||
|
||||
it("patch updates a file entry", async () => {
|
||||
@@ -52,29 +106,68 @@ describe("integration", () => {
|
||||
expect(map).toContain("baz");
|
||||
});
|
||||
|
||||
it("patch adds dirty marker for large packages", async () => {
|
||||
it("patch with small mode refreshes ancestor indexes but preserves ancestor maps", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
// Create 11 files so it's a "large" package
|
||||
for (let i = 0; i < 11; i++) {
|
||||
writeFileSync(
|
||||
join(dir, "src", `file${i}.ts`),
|
||||
`export const x${i} = ${i};\n`,
|
||||
);
|
||||
}
|
||||
writeFileSync(join(dir, "package.json"), `{}\n`);
|
||||
writeFileSync(join(dir, "src", "index.ts"), `export const a = 1;\n`);
|
||||
writeFileSync(join(dir, "src", "helper.ts"), `export const b = 2;\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
// Modify a file
|
||||
writeFileSync(
|
||||
join(dir, "src", "file0.ts"),
|
||||
`export const x0 = 0;\nexport const y = 99;\n`,
|
||||
join(dir, ".pi-map.md"),
|
||||
`${readFileSync(join(dir, ".pi-map.md"), "utf8")}\nSENTINEL_ROOT_MAP\n`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
`${readFileSync(join(dir, ".pi-map.index.md"), "utf8")}\nSENTINEL_ROOT_INDEX\n`,
|
||||
);
|
||||
await patchFile(join(dir, "src", "file0.ts"), client, dir);
|
||||
|
||||
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
||||
expect(map).toContain("y");
|
||||
expect(map).toContain("dirty");
|
||||
expect(map).toContain("patched");
|
||||
writeFileSync(
|
||||
join(dir, "src", "index.ts"),
|
||||
`export const a = 1;\nexport const c = 3;\n`,
|
||||
);
|
||||
await patchFile(join(dir, "src", "index.ts"), client, dir, {
|
||||
rootPath: dir,
|
||||
patchMode: "small",
|
||||
});
|
||||
|
||||
const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8");
|
||||
const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8");
|
||||
expect(rootMap).toContain("SENTINEL_ROOT_MAP");
|
||||
expect(rootIndex).not.toContain("SENTINEL_ROOT_INDEX");
|
||||
});
|
||||
|
||||
it("patch with structural mode refreshes ancestor maps and indexes", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
writeFileSync(join(dir, "package.json"), `{}\n`);
|
||||
writeFileSync(join(dir, "src", "index.ts"), `export const a = 1;\n`);
|
||||
writeFileSync(join(dir, "src", "helper.ts"), `export const b = 2;\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.md"),
|
||||
`${readFileSync(join(dir, ".pi-map.md"), "utf8")}\nSENTINEL_ROOT_MAP\n`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
`${readFileSync(join(dir, ".pi-map.index.md"), "utf8")}\nSENTINEL_ROOT_INDEX\n`,
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(dir, "src", "index.ts"),
|
||||
`export const a = 1;\nexport const c = 3;\n`,
|
||||
);
|
||||
await patchFile(join(dir, "src", "index.ts"), client, dir, {
|
||||
rootPath: dir,
|
||||
patchMode: "structural",
|
||||
});
|
||||
|
||||
const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8");
|
||||
const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8");
|
||||
expect(rootMap).not.toContain("SENTINEL_ROOT_MAP");
|
||||
expect(rootIndex).not.toContain("SENTINEL_ROOT_INDEX");
|
||||
});
|
||||
|
||||
it("validate detects new files", async () => {
|
||||
@@ -124,4 +217,143 @@ describe("integration", () => {
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("init populates routing metadata in rich maps", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
writeFileSync(
|
||||
join(dir, "src", "index.ts"),
|
||||
`export function init() {}\nexport function run() {}\n`,
|
||||
);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
||||
// Tags and symbols should be scaffolded or populated
|
||||
expect(map).toContain("## tags");
|
||||
expect(map).toContain("## symbols");
|
||||
expect(map).toContain("## workflows");
|
||||
});
|
||||
|
||||
it("leaf directory index is tiny but present", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
mkdirSync(join(dir, "src", "utils"));
|
||||
writeFileSync(
|
||||
join(dir, "src", "utils", "helpers.ts"),
|
||||
`export const h = 1;\n`,
|
||||
);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const index = readFileSync(
|
||||
join(dir, "src", "utils", ".pi-map.index.md"),
|
||||
"utf8",
|
||||
);
|
||||
// Leaf index should have empty children but still follow contract
|
||||
expect(index).toContain("## children");
|
||||
expect(index).toContain("-");
|
||||
expect(index).toContain("## parent");
|
||||
expect(index).toContain("index: src/.pi-map.index.md");
|
||||
});
|
||||
|
||||
it("non-source directories omit uncertain workflows", async () => {
|
||||
mkdirSync(join(dir, "docs"));
|
||||
writeFileSync(join(dir, "docs", "README.md"), `# README\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const map = readFileSync(join(dir, "docs", ".pi-map.md"), "utf8");
|
||||
// No source files means workflows section should be empty/omitted
|
||||
const workflowsMatch = map.match(/## workflows\n([^#]*)/);
|
||||
if (workflowsMatch) {
|
||||
expect(workflowsMatch[1].trim()).toBe("-");
|
||||
}
|
||||
});
|
||||
|
||||
it("init loads config caps and applies them to routing metadata", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
for (let i = 0; i < 20; i++) {
|
||||
writeFileSync(
|
||||
join(dir, "src", `file${i}.ts`),
|
||||
`export function fn${i}() {}\n`,
|
||||
);
|
||||
}
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ tagCap: 3, workflowHintCap: 2 }),
|
||||
);
|
||||
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
||||
const parsedMap = (await import("../src/format.js")).parseDirectoryMap(map);
|
||||
expect(parsedMap.tags.length).toBeLessThanOrEqual(3);
|
||||
expect(parsedMap.symbols.length).toBeLessThanOrEqual(3);
|
||||
|
||||
const index = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8");
|
||||
const parsedIndex = (await import("../src/format.js")).parseDirectoryIndex(
|
||||
index,
|
||||
);
|
||||
expect(parsedIndex.workflows.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("validate hard-fails when an index is missing", 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 });
|
||||
|
||||
rmSync(join(dir, "src", ".pi-map.index.md"));
|
||||
|
||||
const result = await validateMaps(dir, { verbose: false });
|
||||
expect(result.clean).toBe(false);
|
||||
expect(result.discrepancies.some((d) => d.type === "stale-index")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("validate detects broken sibling links", 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 });
|
||||
|
||||
const mapPath = join(dir, "src", ".pi-map.md");
|
||||
writeFileSync(
|
||||
mapPath,
|
||||
readFileSync(mapPath, "utf8").replace(
|
||||
"index: src/.pi-map.index.md",
|
||||
"index: src/bad-index.md",
|
||||
),
|
||||
);
|
||||
|
||||
const result = await validateMaps(dir, { verbose: false });
|
||||
expect(result.clean).toBe(false);
|
||||
expect(result.discrepancies.some((d) => d.type === "broken-link")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("validate --fix repairs the affected chain", async () => {
|
||||
mkdirSync(join(dir, "src"));
|
||||
writeFileSync(join(dir, "package.json"), `{}\n`);
|
||||
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
||||
const client = createMockFileClient();
|
||||
await initProject(dir, { llmClient: client, verbose: false });
|
||||
|
||||
rmSync(join(dir, "src", ".pi-map.index.md"));
|
||||
const result = await validateMaps(dir, {
|
||||
fix: true,
|
||||
verbose: false,
|
||||
llmClient: client,
|
||||
cacheDir: dir,
|
||||
patchMode: "small",
|
||||
});
|
||||
|
||||
expect(result.clean).toBe(false);
|
||||
expect(result.fixed).toBeGreaterThan(0);
|
||||
expect(
|
||||
readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"),
|
||||
).toContain("# src (index)");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user