import { describe, it, expect } from "vitest"; import { renderPackageMap, parsePackageMap, renderDirectoryMap, parseDirectoryMap, renderDirectoryIndex, parseDirectoryIndex, type PackageMapData, } from "../src/format.js"; import { createDirectoryModel } from "../src/directory-model.js"; const sampleData: PackageMapData = { path: "pkg/auth", role: "Auth layer: JWT issuance, validation, refresh. Stateless. Dep: pkg/crypto, pkg/db.", files: [ { name: "tokens.ts", purpose: "JWT gen/val", exports: ["issueToken", "verifyToken", "refreshToken"], deps: ["crypto/hmac", "db/sessions"], }, { name: "middleware.ts", purpose: "HTTP auth guard", exports: ["requireAuth", "requireRole"], deps: ["tokens/verifyToken"], }, { name: "types.ts", purpose: "shared auth types", exports: ["AuthToken", "UserClaims", "Role"], deps: [], }, ], arch: "Guard pattern on routes. Tokens short-lived (15m), refresh long-lived (7d). Rotation on every use.", dirty: "-", }; describe("format", () => { it("renders package map correctly", () => { const output = renderPackageMap(sampleData); expect(output).toContain("# pkg/auth"); expect(output).toContain("## role"); expect(output).toContain("## files"); expect(output).toContain( "- tokens.ts | JWT gen/val | exp: issueToken, verifyToken, refreshToken | dep: crypto/hmac, db/sessions", ); expect(output).toContain("## arch"); expect(output).toContain("## tags"); expect(output).toContain("## symbols"); expect(output).toContain("## workflows"); expect(output).toContain("## dirty"); expect(output).toContain("-"); }); it("round-trips parse and render", () => { const rendered = renderPackageMap(sampleData); const parsed = parsePackageMap(rendered); expect(parsed.path).toBe(sampleData.path); expect(parsed.role).toBe(sampleData.role); expect(parsed.arch).toBe(sampleData.arch); expect(parsed.dirty).toBeUndefined(); // "-" is normalized to undefined expect(parsed.files).toHaveLength(3); const tokens = parsed.files.find((f) => f.name === "tokens.ts"); expect(tokens).toBeDefined(); expect(tokens!.purpose).toBe("JWT gen/val"); expect(tokens!.exports).toEqual([ "issueToken", "verifyToken", "refreshToken", ]); expect(tokens!.deps).toEqual(["crypto/hmac", "db/sessions"]); }); it("handles files without exports or deps", () => { const data: PackageMapData = { path: "pkg/utils", role: "Utilities", files: [ { name: "helpers.ts", purpose: "Helpers", exports: [], deps: [] }, ], arch: "Shared helpers", dirty: "2024-01-01: patched", }; const rendered = renderPackageMap(data); const parsed = parsePackageMap(rendered); expect(parsed.files[0].exports).toEqual([]); expect(parsed.files[0].deps).toEqual([]); expect(parsed.dirty).toBe("2024-01-01: patched"); }); it("parses multiline arch", () => { const markdown = `# pkg/test ## role Test package ## files - test.ts | Test file ## arch Line one. Line two. Line three. ## tags - ## symbols - ## workflows - ## dirty - `; const parsed = parsePackageMap(markdown); expect(parsed.arch).toBe("Line one.\nLine two.\nLine three."); }); }); describe("paired artifacts", () => { it("renders and parses a directory map with tags/symbols/workflows", () => { const model = createDirectoryModel({ dir: "src/cli", role: "CLI entrypoint and command parsing", files: [ { name: "cli.ts", purpose: "CLI entrypoint", exports: ["main"], deps: ["commander"], }, ], arch: "Command-line interface built on commander.js", }); model.tags = ["cli", "entrypoint"]; model.symbols = ["main"]; model.workflows = [{ task: "add command", read: ["cli.ts"] }]; const rendered = renderDirectoryMap(model); expect(rendered).toContain("# src/cli"); expect(rendered).toContain("## tags"); expect(rendered).toContain("cli, entrypoint"); expect(rendered).toContain("## symbols"); expect(rendered).toContain("- main"); expect(rendered).toContain("## workflows"); expect(rendered).toContain("- add command"); const parsed = parseDirectoryMap(rendered); expect(parsed.dir).toBe("src/cli"); expect(parsed.tags).toEqual(["cli", "entrypoint"]); expect(parsed.symbols).toEqual(["main"]); expect(parsed.workflows).toHaveLength(1); expect(parsed.workflows[0].task).toBe("add command"); }); it("renders and parses a directory index with parent/children/links", () => { const model = createDirectoryModel({ dir: "src", role: "Core source code", files: [ { name: "index.ts", purpose: "Main exports", exports: [], deps: [] }, ], arch: "Source code root", parent: ".", children: ["src/cli", "src/lib"], }); const rendered = renderDirectoryIndex(model); expect(rendered).toContain("# src (index)"); expect(rendered).toContain("dir: src"); expect(rendered).toContain("## parent"); expect(rendered).toContain("index: ./.pi-map.index.md"); expect(rendered).toContain("map: ./.pi-map.md"); expect(rendered).toContain("## children"); expect(rendered).toContain("- src/cli"); expect(rendered).toContain("index: src/cli/.pi-map.index.md"); expect(rendered).toContain("map: src/cli/.pi-map.md"); expect(rendered).toContain("## links"); expect(rendered).toContain("index: src/.pi-map.index.md"); expect(rendered).toContain("map: src/.pi-map.md"); const parsed = parseDirectoryIndex(rendered); expect(parsed.dir).toBe("src"); expect(parsed.parent).toBe("."); expect(parsed.children).toEqual(["src/cli", "src/lib"]); }); it("renders a root index with Project Map Protocol", () => { const model = createDirectoryModel({ dir: ".", role: "Project root", files: [], arch: "Root architecture", isRoot: true, }); const rendered = renderDirectoryIndex(model); expect(rendered).toContain("# . (index)"); expect(rendered).toContain("dir: ."); }); it("round-trips an empty leaf index", () => { const model = createDirectoryModel({ dir: "src/utils", role: "Utilities", files: [ { name: "helpers.ts", purpose: "Helpers", exports: [], deps: [] }, ], arch: "Shared helpers", }); const rendered = renderDirectoryIndex(model); expect(rendered).toContain("# src/utils (index)"); expect(rendered).toContain("## children"); expect(rendered).toContain("-"); const parsed = parseDirectoryIndex(rendered); expect(parsed.dir).toBe("src/utils"); expect(parsed.children).toEqual([]); expect(parsed.files).toHaveLength(1); expect(parsed.files[0].name).toBe("helpers.ts"); }); it("round-trips workflows with read continuations in map", () => { const model = createDirectoryModel({ dir: "src/cli", role: "CLI entrypoint", files: [{ name: "cli.ts", purpose: "CLI", exports: ["main"], deps: [] }], arch: "CLI", }); model.workflows = [ { task: "add command", read: ["cli.ts", "commands.ts"] }, { task: "update flags", read: ["cli.ts"] }, ]; const rendered = renderDirectoryMap(model); expect(rendered).toContain("- add command"); expect(rendered).toContain(" read: cli.ts, commands.ts"); expect(rendered).toContain("- update flags"); expect(rendered).toContain(" read: cli.ts"); const parsed = parseDirectoryMap(rendered); expect(parsed.workflows).toHaveLength(2); expect(parsed.workflows[0].task).toBe("add command"); expect(parsed.workflows[0].read).toEqual(["cli.ts", "commands.ts"]); expect(parsed.workflows[1].task).toBe("update flags"); expect(parsed.workflows[1].read).toEqual(["cli.ts"]); }); it("round-trips workflows with read continuations in index", () => { const model = createDirectoryModel({ dir: "src/cli", role: "CLI entrypoint", files: [{ name: "cli.ts", purpose: "CLI", exports: ["main"], deps: [] }], arch: "CLI", }); model.workflows = [ { task: "add command", read: ["cli.ts", "commands.ts"] }, ]; const rendered = renderDirectoryIndex(model); expect(rendered).toContain("- add command"); expect(rendered).toContain(" read: cli.ts, commands.ts"); const parsed = parseDirectoryIndex(rendered); expect(parsed.workflows).toHaveLength(1); expect(parsed.workflows[0].task).toBe("add command"); expect(parsed.workflows[0].read).toEqual(["cli.ts", "commands.ts"]); }); it("round-trips workflows with index, map, and files continuations", () => { const model = createDirectoryModel({ dir: "src", role: "Source root", files: [{ name: "index.ts", purpose: "Exports", exports: [], deps: [] }], arch: "Source", children: ["src/auth", "src/cli"], }); model.workflows = [ { task: "explore subdirectories", index: ["src/auth/.pi-map.index.md", "src/cli/.pi-map.index.md"], map: ["src/auth/.pi-map.md"], files: ["src/index.ts"], }, ]; const mapRendered = renderDirectoryMap(model); expect(mapRendered).toContain("- explore subdirectories"); expect(mapRendered).toContain( " index: src/auth/.pi-map.index.md, src/cli/.pi-map.index.md", ); expect(mapRendered).toContain(" map: src/auth/.pi-map.md"); expect(mapRendered).toContain(" files: src/index.ts"); const mapParsed = parseDirectoryMap(mapRendered); expect(mapParsed.workflows).toHaveLength(1); expect(mapParsed.workflows[0].index).toEqual([ "src/auth/.pi-map.index.md", "src/cli/.pi-map.index.md", ]); expect(mapParsed.workflows[0].map).toEqual(["src/auth/.pi-map.md"]); expect(mapParsed.workflows[0].files).toEqual(["src/index.ts"]); const indexRendered = renderDirectoryIndex(model); expect(indexRendered).toContain("- explore subdirectories"); expect(indexRendered).toContain( " index: src/auth/.pi-map.index.md, src/cli/.pi-map.index.md", ); const indexParsed = parseDirectoryIndex(indexRendered); expect(indexParsed.workflows).toHaveLength(1); expect(indexParsed.workflows[0].index).toEqual([ "src/auth/.pi-map.index.md", "src/cli/.pi-map.index.md", ]); }); it("renderer includes dir and index preamble directly", () => { const model = createDirectoryModel({ dir: "src/core", role: "Core logic", files: [], arch: "Core", }); const rendered = renderDirectoryMap(model); expect(rendered).toContain("dir: src/core"); expect(rendered).toContain("index: src/core/.pi-map.index.md"); }); it("root renderer includes Project Map Protocol directly", () => { const model = createDirectoryModel({ dir: ".", role: "Project root", files: [], arch: "Root", isRoot: true, }); const mapRendered = renderDirectoryMap(model); expect(mapRendered).toContain("## Project Map Protocol"); expect(mapRendered).toContain( "Trust boundary: index routes, map orients, source decides.", ); const indexRendered = renderDirectoryIndex(model); expect(indexRendered).toContain("## Project Map Protocol"); }); it("parses file lines with commas inside signatures", () => { const mapText = `# src ## files - format.ts | Renders maps | exp: PackageMapData, func:renderPackageMap(data: PackageMapData) → string, call:convertPackageMapToModel, func:renderDirectoryIndex(model: DirectoryArtifactModel) → string | dep: ./model.js ## arch Test ## dirty - `; const parsed = parseDirectoryMap(mapText); expect(parsed.files).toHaveLength(1); expect(parsed.files[0].exports).toEqual([ "PackageMapData", "func:renderPackageMap(data: PackageMapData) → string", "call:convertPackageMapToModel", "func:renderDirectoryIndex(model: DirectoryArtifactModel) → string", ]); expect(parsed.files[0].deps).toEqual(["./model.js"]); }); it("parses file lines with pipes and commas inside type signatures", () => { const mapText = `# src ## files - llm-cache.ts | Cache helpers | exp: func:getCached(hash: string, cacheDir: string) → string | undefined, func:setCached(hash: string, result: string, cacheDir: string) → void | dep: fs, path - user.ts | User helpers | exp: User, func:createUser(data: Omit) → User, func:serializeUser(user: User) → string | dep: ../utils/validation.js ## arch Test ## dirty - `; const parsed = parseDirectoryMap(mapText); expect(parsed.files).toHaveLength(2); expect(parsed.files[0].exports).toEqual([ "func:getCached(hash: string, cacheDir: string) → string | undefined", "func:setCached(hash: string, result: string, cacheDir: string) → void", ]); expect(parsed.files[1].exports).toEqual([ "User", 'func:createUser(data: Omit) → User', "func:serializeUser(user: User) → string", ]); }); });