Implement layered maps and context retrieval
This commit is contained in:
+531
-31
@@ -1,3 +1,6 @@
|
||||
import type { DirectoryArtifactModel, FileEntry } from "./directory-model.js";
|
||||
import { PROJECT_MAP_PROTOCOL_LINES } from "./directory-model.js";
|
||||
|
||||
export interface PackageMapData {
|
||||
path: string;
|
||||
role: string;
|
||||
@@ -6,20 +9,64 @@ export interface PackageMapData {
|
||||
dirty?: string;
|
||||
}
|
||||
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
purpose: string;
|
||||
exports: string[];
|
||||
deps: string[];
|
||||
export function renderPackageMap(data: PackageMapData): string {
|
||||
const model = convertPackageMapToModel(data);
|
||||
return renderDirectoryMap(model);
|
||||
}
|
||||
|
||||
export function renderPackageMap(data: PackageMapData): string {
|
||||
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(`# ${data.path}`);
|
||||
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(data.role);
|
||||
lines.push(model.role);
|
||||
|
||||
lines.push(`## files`);
|
||||
for (const file of data.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(", ")}` : "";
|
||||
@@ -28,28 +75,88 @@ export function renderPackageMap(data: PackageMapData): string {
|
||||
if (dep) parts.push(dep);
|
||||
lines.push(parts.join(" | "));
|
||||
}
|
||||
|
||||
lines.push(`## arch`);
|
||||
lines.push(data.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(data.dirty || "-");
|
||||
lines.push(model.dirty || "-");
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export function parsePackageMap(markdown: string): PackageMapData {
|
||||
export function parseDirectoryMap(markdown: string): DirectoryArtifactModel {
|
||||
const lines = markdown.split("\n").map((l) => l.trimEnd());
|
||||
const result: PackageMapData = {
|
||||
path: "",
|
||||
const result: DirectoryArtifactModel = {
|
||||
dir: "",
|
||||
role: "",
|
||||
files: [],
|
||||
arch: "",
|
||||
dirty: "-",
|
||||
isRoot: false,
|
||||
parent: undefined,
|
||||
children: [],
|
||||
tags: [],
|
||||
symbols: [],
|
||||
workflows: [],
|
||||
};
|
||||
|
||||
let section: "none" | "role" | "files" | "arch" | "dirty" = "none";
|
||||
let section:
|
||||
| "none"
|
||||
| "role"
|
||||
| "files"
|
||||
| "arch"
|
||||
| "tags"
|
||||
| "symbols"
|
||||
| "workflows"
|
||||
| "dirty" = "none";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("# ")) {
|
||||
result.path = line.slice(2).trim();
|
||||
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") {
|
||||
@@ -64,6 +171,18 @@ export function parsePackageMap(markdown: string): PackageMapData {
|
||||
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;
|
||||
@@ -83,6 +202,70 @@ export function parsePackageMap(markdown: string): PackageMapData {
|
||||
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;
|
||||
@@ -92,10 +275,115 @@ export function parsePackageMap(markdown: string): PackageMapData {
|
||||
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 = withoutPrefix.split(" | ").map((p) => p.trim());
|
||||
const parts = splitFieldsRespectingNesting(withoutPrefix).map((p) =>
|
||||
p.trim(),
|
||||
);
|
||||
|
||||
if (parts.length < 2) return null;
|
||||
|
||||
@@ -107,23 +395,235 @@ function parseFileLine(line: string): FileEntry | null {
|
||||
for (let i = 2; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (part.startsWith("exp: ")) {
|
||||
exports.push(
|
||||
...part
|
||||
.slice(5)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
exports.push(...splitRespectingNesting(part.slice(5)).filter(Boolean));
|
||||
} else if (part.startsWith("dep: ")) {
|
||||
deps.push(
|
||||
...part
|
||||
.slice(5)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user