Implement layered maps and context retrieval
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import type {
|
||||
DirectoryArtifactModel,
|
||||
FileEntry,
|
||||
RoutingMetadataOptions,
|
||||
WorkflowHint,
|
||||
} from "./directory-model.js";
|
||||
|
||||
const DEFAULT_TAG_CAP = 8;
|
||||
const DEFAULT_WORKFLOW_HINT_CAP = 5;
|
||||
|
||||
const STOP_WORDS = new Set([
|
||||
"the",
|
||||
"and",
|
||||
"for",
|
||||
"with",
|
||||
"from",
|
||||
"into",
|
||||
"onto",
|
||||
"this",
|
||||
"that",
|
||||
"then",
|
||||
"than",
|
||||
"when",
|
||||
"where",
|
||||
"what",
|
||||
"how",
|
||||
"why",
|
||||
"who",
|
||||
"which",
|
||||
"while",
|
||||
"during",
|
||||
"before",
|
||||
"after",
|
||||
"above",
|
||||
"below",
|
||||
"between",
|
||||
"among",
|
||||
"through",
|
||||
"over",
|
||||
"under",
|
||||
"again",
|
||||
"further",
|
||||
"once",
|
||||
"here",
|
||||
"there",
|
||||
"all",
|
||||
"any",
|
||||
"both",
|
||||
"each",
|
||||
"few",
|
||||
"more",
|
||||
"most",
|
||||
"other",
|
||||
"some",
|
||||
"such",
|
||||
"only",
|
||||
"own",
|
||||
"same",
|
||||
"so",
|
||||
"too",
|
||||
"very",
|
||||
"can",
|
||||
"will",
|
||||
"just",
|
||||
"should",
|
||||
"now",
|
||||
"use",
|
||||
"using",
|
||||
"used",
|
||||
"via",
|
||||
"based",
|
||||
"build",
|
||||
"built",
|
||||
"used",
|
||||
"file",
|
||||
"files",
|
||||
"module",
|
||||
"modules",
|
||||
"function",
|
||||
"functions",
|
||||
"class",
|
||||
"classes",
|
||||
"export",
|
||||
"exports",
|
||||
"import",
|
||||
"imports",
|
||||
]);
|
||||
|
||||
export function populateRoutingMetadata(
|
||||
model: DirectoryArtifactModel,
|
||||
opts: RoutingMetadataOptions = {},
|
||||
): void {
|
||||
const tagCap = opts.tagCap ?? DEFAULT_TAG_CAP;
|
||||
const workflowHintCap = opts.workflowHintCap ?? DEFAULT_WORKFLOW_HINT_CAP;
|
||||
|
||||
model.tags = generateTags(model.files, tagCap);
|
||||
model.symbols = generateSymbols(model.files, tagCap);
|
||||
model.workflows = generateWorkflows(model, workflowHintCap);
|
||||
}
|
||||
|
||||
function generateTags(files: FileEntry[], cap: number): string[] {
|
||||
const scores = new Map<string, number>();
|
||||
|
||||
for (const file of files) {
|
||||
// Score words from file purpose
|
||||
for (const word of extractWords(file.purpose)) {
|
||||
scores.set(word, (scores.get(word) ?? 0) + 1);
|
||||
}
|
||||
|
||||
// Score words from export names (camelCase split)
|
||||
for (const exp of file.exports) {
|
||||
const cleanExp = exp.replace(/^(class|func|method):/, "").split("(")[0];
|
||||
for (const word of splitCamelCase(cleanExp)) {
|
||||
if (word.length > 1) {
|
||||
scores.set(word, (scores.get(word) ?? 0) + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Score dep path segments
|
||||
for (const dep of file.deps) {
|
||||
for (const segment of dep.split(/[/\-.]/)) {
|
||||
const word = segment.toLowerCase();
|
||||
if (word.length > 1 && !STOP_WORDS.has(word)) {
|
||||
scores.set(word, (scores.get(word) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Score from file name (stem, no extension)
|
||||
const stem = file.name.replace(/\.[^.]+$/, "");
|
||||
for (const word of splitCamelCase(stem)) {
|
||||
if (word.length > 1) {
|
||||
scores.set(word, (scores.get(word) ?? 0) + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(scores.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([word]) => word)
|
||||
.slice(0, cap);
|
||||
}
|
||||
|
||||
function generateSymbols(files: FileEntry[], cap: number): string[] {
|
||||
const scores = new Map<string, number>();
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const file of files) {
|
||||
for (const exp of file.exports) {
|
||||
// Prefer clean identifiers over encoded DSL entries when possible
|
||||
let symbol = exp;
|
||||
let score = 1;
|
||||
|
||||
if (exp.startsWith("class:")) {
|
||||
symbol = exp.slice(6).split(" ")[0];
|
||||
score = 3;
|
||||
} else if (exp.startsWith("func:")) {
|
||||
symbol = exp.slice(5).split("(")[0];
|
||||
score = 2;
|
||||
} else if (exp.startsWith("method:")) {
|
||||
symbol = exp.slice(7).split("(")[0];
|
||||
score = 2;
|
||||
}
|
||||
|
||||
if (!seen.has(symbol)) {
|
||||
seen.add(symbol);
|
||||
scores.set(symbol, (scores.get(symbol) ?? 0) + score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(scores.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([name]) => name)
|
||||
.slice(0, cap);
|
||||
}
|
||||
|
||||
function generateWorkflows(
|
||||
model: DirectoryArtifactModel,
|
||||
cap: number,
|
||||
): WorkflowHint[] {
|
||||
const workflows: WorkflowHint[] = [];
|
||||
const dirName = model.dir.split("/").pop() || model.dir;
|
||||
const baseName = dirName === "." ? "project" : dirName;
|
||||
|
||||
// Only generate workflows when we have reasonable structural confidence
|
||||
const hasSourceFiles = model.files.some((f) =>
|
||||
/\.(ts|tsx|js|jsx|py|go|rs|java)$/.test(f.name),
|
||||
);
|
||||
if (!hasSourceFiles || model.files.length === 0) {
|
||||
return workflows;
|
||||
}
|
||||
|
||||
const sourceFiles = model.files.filter(
|
||||
(f) =>
|
||||
!/\.(test|spec)\./.test(f.name) && !/\.(md|json|yaml|yml)$/.test(f.name),
|
||||
);
|
||||
const testFiles = model.files.filter((f) => /\.(test|spec)\./.test(f.name));
|
||||
const configFiles = model.files.filter(
|
||||
(f) =>
|
||||
f.name.includes("config") ||
|
||||
/\.(json|yaml|yml|toml)$/.test(f.name) ||
|
||||
f.name === ".env",
|
||||
);
|
||||
const cliFiles = model.files.filter(
|
||||
(f) => f.name.includes("cli") || f.name.includes("command"),
|
||||
);
|
||||
|
||||
if (sourceFiles.length > 0) {
|
||||
workflows.push({
|
||||
task: `change ${baseName} behavior`,
|
||||
read: sourceFiles.slice(0, 3).map((f) => f.name),
|
||||
});
|
||||
}
|
||||
|
||||
if (testFiles.length > 0) {
|
||||
workflows.push({
|
||||
task: `update ${baseName} tests`,
|
||||
read: testFiles.slice(0, 3).map((f) => f.name),
|
||||
});
|
||||
}
|
||||
|
||||
if (cliFiles.length > 0) {
|
||||
workflows.push({
|
||||
task: `change ${baseName} CLI`,
|
||||
read: cliFiles.slice(0, 3).map((f) => f.name),
|
||||
});
|
||||
}
|
||||
|
||||
if (configFiles.length > 0) {
|
||||
workflows.push({
|
||||
task: `change ${baseName} config`,
|
||||
read: configFiles.slice(0, 3).map((f) => f.name),
|
||||
});
|
||||
}
|
||||
|
||||
// Add a directory-navigation workflow for non-leaf directories
|
||||
if (model.children.length > 0) {
|
||||
workflows.push({
|
||||
task: `explore ${baseName} subdirectories`,
|
||||
index: model.children
|
||||
.slice(0, 3)
|
||||
.map((child) => `${child}/.pi-map.index.md`),
|
||||
});
|
||||
}
|
||||
|
||||
return workflows.slice(0, cap);
|
||||
}
|
||||
|
||||
function extractWords(text: string): string[] {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9]+/)
|
||||
.filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
||||
}
|
||||
|
||||
function splitCamelCase(str: string): string[] {
|
||||
return str
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||
.replace(/[_-]+/g, " ")
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((w) => w.length > 1 && !STOP_WORDS.has(w));
|
||||
}
|
||||
Reference in New Issue
Block a user