import type { DirectoryArtifactModel, FileEntry } from "./directory-model.js"; import { PROJECT_MAP_PROTOCOL_LINES } from "./directory-model.js"; export interface PackageMapData { path: string; role: string; files: FileEntry[]; arch: string; dirty?: string; } export function renderPackageMap(data: PackageMapData): string { const model = convertPackageMapToModel(data); return renderDirectoryMap(model); } export function parsePackageMap(markdown: string): PackageMapData { const model = parseDirectoryMap(markdown); return convertModelToPackageMap(model); } function convertPackageMapToModel( data: PackageMapData, ): DirectoryArtifactModel { return { dir: data.path, role: data.role, files: data.files, arch: data.arch, dirty: data.dirty, isRoot: data.path === ".", parent: undefined, children: [], tags: [], symbols: [], workflows: [], }; } function convertModelToPackageMap( model: DirectoryArtifactModel, ): PackageMapData { return { path: model.dir, role: model.role, files: model.files, arch: model.arch, dirty: model.dirty, }; } export function renderDirectoryMap(model: DirectoryArtifactModel): string { const lines: string[] = []; lines.push(`# ${model.dir}`); lines.push(`dir: ${model.dir}`); lines.push(""); lines.push(`index: ${model.dir}/.pi-map.index.md`); lines.push(""); if (model.isRoot) { lines.push(...PROJECT_MAP_PROTOCOL_LINES); lines.push(""); } lines.push(`## role`); lines.push(model.role); lines.push(`## files`); for (const file of model.files) { const exp = file.exports.length > 0 ? `exp: ${file.exports.join(", ")}` : ""; const dep = file.deps.length > 0 ? `dep: ${file.deps.join(", ")}` : ""; const parts = [`- ${file.name} | ${file.purpose}`]; if (exp) parts.push(exp); if (dep) parts.push(dep); lines.push(parts.join(" | ")); } lines.push(`## arch`); lines.push(model.arch); // Slice 2: tags, symbols, workflows — scaffold empty sections for now lines.push(`## tags`); if (model.tags.length > 0) { lines.push(model.tags.join(", ")); } else { lines.push("-"); } lines.push(`## symbols`); if (model.symbols.length > 0) { for (const sym of model.symbols) { lines.push(`- ${sym}`); } } else { lines.push("-"); } lines.push(`## workflows`); if (model.workflows.length > 0) { for (const wf of model.workflows) { lines.push(`- ${wf.task}`); if (wf.read && wf.read.length > 0) { lines.push(` read: ${wf.read.join(", ")}`); } if (wf.index && wf.index.length > 0) { lines.push(` index: ${wf.index.join(", ")}`); } if (wf.map && wf.map.length > 0) { lines.push(` map: ${wf.map.join(", ")}`); } if (wf.files && wf.files.length > 0) { lines.push(` files: ${wf.files.join(", ")}`); } } } else { lines.push("-"); } lines.push(`## dirty`); lines.push(model.dirty || "-"); return `${lines.join("\n")}\n`; } export function parseDirectoryMap(markdown: string): DirectoryArtifactModel { const lines = markdown.split("\n").map((l) => l.trimEnd()); const result: DirectoryArtifactModel = { dir: "", role: "", files: [], arch: "", dirty: "-", isRoot: false, parent: undefined, children: [], tags: [], symbols: [], workflows: [], }; let section: | "none" | "role" | "files" | "arch" | "tags" | "symbols" | "workflows" | "dirty" = "none"; for (const line of lines) { if (line.startsWith("# ")) { result.dir = line.slice(2).trim(); result.isRoot = result.dir === "."; continue; } if (line.startsWith("dir: ")) { result.dir = line.slice(5).trim(); result.isRoot = result.dir === "."; continue; } if (line === "## role") { section = "role"; continue; } if (line === "## files") { section = "files"; continue; } if (line === "## arch") { section = "arch"; continue; } if (line === "## tags") { section = "tags"; continue; } if (line === "## symbols") { section = "symbols"; continue; } if (line === "## workflows") { section = "workflows"; continue; } if (line === "## dirty") { section = "dirty"; continue; } if (line === "") continue; switch (section) { case "role": result.role = line; break; case "files": { if (!line.startsWith("- ")) continue; const entry = parseFileLine(line); if (entry) result.files.push(entry); break; } case "arch": result.arch = result.arch ? `${result.arch}\n${line}` : line; break; case "tags": if (line !== "-") { result.tags.push( ...line .split(",") .map((s) => s.trim()) .filter(Boolean), ); } break; case "symbols": if (line !== "-" && line.startsWith("- ")) { result.symbols.push(line.slice(2).trim()); } break; case "workflows": { if (line !== "-" && line.startsWith("- ")) { const task = line.slice(2).trim(); result.workflows.push({ task }); } else if (line.startsWith(" read: ") && result.workflows.length > 0) { const reads = line .slice(8) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.read = reads; } else if ( line.startsWith(" index: ") && result.workflows.length > 0 ) { const indices = line .slice(9) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.index = indices; } else if (line.startsWith(" map: ") && result.workflows.length > 0) { const maps = line .slice(7) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.map = maps; } else if ( line.startsWith(" files: ") && result.workflows.length > 0 ) { const files = line .slice(9) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.files = files; } break; } case "dirty": result.dirty = line === "-" ? undefined : line; break; } } return result; } function splitTopLevel(str: string, delimiter: string): string[] { const result: string[] = []; let current = ""; let depthParen = 0; let depthBracket = 0; let depthBrace = 0; let depthAngle = 0; let inString: '"' | "'" | null = null; for (let i = 0; i < str.length; i++) { const ch = str[i]; const prev = str[i - 1]; const next = str[i + 1]; if (inString) { if (ch === inString && prev !== "\\") { inString = null; } current += ch; continue; } if (ch === '"' || ch === "'") { const isWordApostrophe = ch === "'" && /[A-Za-z0-9_$]/.test(prev ?? "") && /[A-Za-z0-9_$]/.test(next ?? ""); if (isWordApostrophe) { current += ch; continue; } inString = ch; current += ch; continue; } if (ch === "(") depthParen++; if (ch === ")") depthParen = Math.max(0, depthParen - 1); if (ch === "[") depthBracket++; if (ch === "]") depthBracket = Math.max(0, depthBracket - 1); if (ch === "{") depthBrace++; if (ch === "}") depthBrace = Math.max(0, depthBrace - 1); if (ch === "<") depthAngle++; if (ch === ">") depthAngle = Math.max(0, depthAngle - 1); if ( delimiter.length === 1 && ch === delimiter && depthParen === 0 && depthBracket === 0 && depthBrace === 0 && depthAngle === 0 ) { result.push(current.trim()); current = ""; continue; } if ( delimiter.length > 1 && str.startsWith(delimiter, i) && depthParen === 0 && depthBracket === 0 && depthBrace === 0 && depthAngle === 0 ) { result.push(current.trim()); current = ""; i += delimiter.length - 1; continue; } current += ch; } if (current.trim()) result.push(current.trim()); return result; } function splitRespectingNesting(str: string): string[] { return splitTopLevel(str, ","); } function splitFieldsRespectingNesting(str: string): string[] { const rawParts = splitTopLevel(str, " | "); const fields: string[] = []; for (const part of rawParts) { const trimmed = part.trim(); if ( fields.length < 2 || trimmed.startsWith("exp: ") || trimmed.startsWith("dep: ") ) { fields.push(trimmed); } else if (fields.length > 0) { fields[fields.length - 1] = `${fields[fields.length - 1]} | ${trimmed}`; } } return fields; } function parseFileLine(line: string): FileEntry | null { // Format: - filename | purpose | exp: ... | dep: ... const withoutPrefix = line.slice(2).trim(); const parts = splitFieldsRespectingNesting(withoutPrefix).map((p) => p.trim(), ); if (parts.length < 2) return null; const name = parts[0]; const purpose = parts[1]; const exports: string[] = []; const deps: string[] = []; for (let i = 2; i < parts.length; i++) { const part = parts[i]; if (part.startsWith("exp: ")) { exports.push(...splitRespectingNesting(part.slice(5)).filter(Boolean)); } else if (part.startsWith("dep: ")) { deps.push(...splitRespectingNesting(part.slice(5)).filter(Boolean)); } } return { name, purpose, exports, deps }; } export function renderDirectoryIndex(model: DirectoryArtifactModel): string { const lines: string[] = []; lines.push(`# ${model.dir} (index)`); lines.push(`dir: ${model.dir}`); lines.push(""); if (model.isRoot) { lines.push(...PROJECT_MAP_PROTOCOL_LINES); lines.push(""); } lines.push(`## role`); lines.push(model.role); lines.push(`## parent`); if (model.parent) { lines.push(`index: ${model.parent}/.pi-map.index.md`); lines.push(`map: ${model.parent}/.pi-map.md`); } else { lines.push("-"); } lines.push(`## children`); if (model.children.length > 0) { for (const child of model.children) { lines.push(`- ${child}`); lines.push(` index: ${child}/.pi-map.index.md`); lines.push(` map: ${child}/.pi-map.md`); } } else { lines.push("-"); } lines.push(`## files`); for (const file of model.files) { lines.push(`- ${file.name}`); } lines.push(`## links`); lines.push(`index: ${model.dir}/.pi-map.index.md`); lines.push(`map: ${model.dir}/.pi-map.md`); lines.push(`## workflows`); if (model.workflows.length > 0) { for (const wf of model.workflows) { lines.push(`- ${wf.task}`); if (wf.read && wf.read.length > 0) { lines.push(` read: ${wf.read.join(", ")}`); } if (wf.index && wf.index.length > 0) { lines.push(` index: ${wf.index.join(", ")}`); } if (wf.map && wf.map.length > 0) { lines.push(` map: ${wf.map.join(", ")}`); } if (wf.files && wf.files.length > 0) { lines.push(` files: ${wf.files.join(", ")}`); } } } else { lines.push("-"); } lines.push(`## dirty`); lines.push(model.dirty || "-"); return `${lines.join("\n")}\n`; } export function parseDirectoryIndex(markdown: string): DirectoryArtifactModel { const lines = markdown.split("\n").map((l) => l.trimEnd()); const result: DirectoryArtifactModel = { dir: "", role: "", files: [], arch: "", dirty: "-", isRoot: false, parent: undefined, children: [], tags: [], symbols: [], workflows: [], }; let section: | "none" | "role" | "parent" | "children" | "files" | "links" | "workflows" | "dirty" = "none"; for (const line of lines) { if (line.startsWith("# ")) { const title = line.slice(2).trim(); // Strip " (index)" suffix if present result.dir = title.replace(/ \(index\)$/, ""); result.isRoot = result.dir === "."; continue; } if (line.startsWith("dir: ")) { result.dir = line.slice(5).trim(); result.isRoot = result.dir === "."; continue; } if (line === "## role") { section = "role"; continue; } if (line === "## parent") { section = "parent"; continue; } if (line === "## children") { section = "children"; continue; } if (line === "## files") { section = "files"; continue; } if (line === "## links") { section = "links"; continue; } if (line === "## workflows") { section = "workflows"; continue; } if (line === "## dirty") { section = "dirty"; continue; } if (line === "") continue; switch (section) { case "role": result.role = line; break; case "parent": if (line !== "-" && line.startsWith("index: ")) { result.parent = line.slice(7).trim().replace("/.pi-map.index.md", ""); } break; case "children": if (line !== "-" && line.startsWith("- ")) { const childName = line.slice(2).trim(); result.children.push(childName); } break; case "files": if (line !== "-" && line.startsWith("- ")) { result.files.push({ name: line.slice(2).trim(), purpose: "", exports: [], deps: [], }); } break; case "links": // Parse sibling links; no-op for now break; case "workflows": { if (line !== "-" && line.startsWith("- ")) { const task = line.slice(2).trim(); result.workflows.push({ task }); } else if (line.startsWith(" read: ") && result.workflows.length > 0) { const reads = line .slice(8) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.read = reads; } else if ( line.startsWith(" index: ") && result.workflows.length > 0 ) { const indices = line .slice(9) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.index = indices; } else if (line.startsWith(" map: ") && result.workflows.length > 0) { const maps = line .slice(7) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.map = maps; } else if ( line.startsWith(" files: ") && result.workflows.length > 0 ) { const files = line .slice(9) .trim() .split(",") .map((s) => s.trim()) .filter(Boolean); const last = result.workflows[result.workflows.length - 1]; if (last) last.files = files; } break; } case "dirty": result.dirty = line === "-" ? undefined : line; break; } } return result; }