import { basename, extname, join } from "node:path"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import type { KnowledgeBase, KnowledgeFile, KnowledgeScope, KnowledgeSection, TableSchema } from "./types.ts"; interface CacheEntry { mtime: number; knowledge: KnowledgeBase; } const cache = new Map(); export function loadKnowledge(scope: KnowledgeScope, basePath: string): KnowledgeBase { const cacheKey = `${basePath}:${scope}`; const cached = cache.get(cacheKey); if (cached && !isCacheStale(basePath, cached.mtime)) { return cached.knowledge; } const commonPath = join(basePath, "common"); const editionPath = join(basePath, scope); const knowledge: KnowledgeBase = { common: loadDirectory(commonPath), edition: loadDirectory(editionPath), tables: loadTables(editionPath), }; cache.set(cacheKey, { mtime: Date.now(), knowledge }); return knowledge; } function loadDirectory(dirPath: string): KnowledgeFile[] { if (!existsSync(dirPath)) return []; const files: KnowledgeFile[] = []; for (const entry of readdirSync(dirPath)) { const filePath = join(dirPath, entry); const stat = statSync(filePath); if (!stat.isFile()) continue; const ext = extname(entry).toLowerCase(); if (ext !== ".md") continue; const file = parseMarkdown(filePath, readFileSync(filePath, "utf8"), stat.mtime.toISOString()); files.push(file); } return files; } function parseMarkdown(filePath: string, content: string, lastModified: string): KnowledgeFile { const lines = content.split("\n"); const sections: KnowledgeSection[] = []; const tags: string[] = []; let currentSection: Partial | undefined; let title = ""; for (let i = 0; i < lines.length; i++) { const line = lines[i].replace(/\r$/, ""); const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { if (currentSection) { currentSection.lineEnd = i - 1; sections.push(currentSection as KnowledgeSection); } const level = headingMatch[1].length; const heading = headingMatch[2].trim(); if (level === 1 && !title) title = heading; if (level <= 2) tags.push(heading); currentSection = { heading, level, content: "", lineStart: i, lineEnd: i, }; } else if (currentSection) { currentSection.content = `${currentSection.content ?? ""}${line}\n`; } } if (currentSection) { currentSection.lineEnd = lines.length - 1; sections.push(currentSection as KnowledgeSection); } const fileName = basename(filePath, ".md"); tags.push(fileName); return { path: filePath, title: title || fileName, type: "markdown", sections, tags: [...new Set(tags)], lastModified, }; } function loadTables(editionPath: string): Map { const tables = new Map(); const tablesPath = join(editionPath, "tables.json"); if (!existsSync(tablesPath)) return tables; const data = JSON.parse(readFileSync(tablesPath, "utf8")) as Record; for (const [tableName, tableData] of Object.entries(data)) { const table = tableData as { name?: string; module?: string; description?: string; fields?: Array<{ name?: string; type?: string; nullable?: boolean; description?: string }>; relatedTables?: Array<{ table?: string; relation?: string; description?: string }>; }; tables.set(tableName.toUpperCase(), { name: table.name || tableName, module: table.module || "", description: table.description || "", fields: (table.fields || []).map((field) => ({ name: field.name || "", type: field.type || "", nullable: field.nullable ?? true, description: field.description || "", })), relatedTables: (table.relatedTables || []).map((related) => ({ table: related.table || "", relation: related.relation || "", description: related.description || "", })), }); } return tables; } function isCacheStale(knowledgePath: string, cachedMtime: number): boolean { try { return statSync(knowledgePath).mtimeMs > cachedMtime; } catch { return true; } } export function clearKnowledgeCache(): void { cache.clear(); }