import { readdirSync, statSync, readFileSync } from 'node:fs'; import { join, dirname, basename } from 'node:path'; export interface PageFile { /** Page basename without `.md`, original case. */ slug: string; /** Directory relative to corpus root, '' for root-level pages. POSIX-ish, using OS sep. */ dir: string; /** Absolute path on disk. */ path: string; /** Last-modified time in ms (for cache revalidation). */ mtimeMs: number; } /** * Top-level dirs excluded from the graph by default. In the Karpathy wiki schema * `raw/` (verbatim ingested docs) and `sources/` (provenance summaries) are NOT * graph pages — they share basenames with canonical concept/entity pages and would * collapse into them (false collisions) and inject provenance edges. `assets/` is * binary. The graph models the canonical concept/entity network only. */ export const DEFAULT_EXCLUDE: readonly string[] = ['raw', 'sources', 'assets']; export interface ListOptions { /** Top-level dir names to skip. Defaults to {@link DEFAULT_EXCLUDE}; pass `[]` to index everything. */ exclude?: readonly string[]; } function firstSegment(dir: string): string { if (dir === '') return ''; return dir.split(/[\\/]/)[0]; } /** List every `.md` file under `corpusDir` (recursive, minus excluded dirs), with mtime. */ export function listPages(corpusDir: string, opts: ListOptions = {}): PageFile[] { const exclude = new Set(opts.exclude ?? DEFAULT_EXCLUDE); const entries = readdirSync(corpusDir, { recursive: true }) as string[]; const pages: PageFile[] = []; for (const rel of entries) { if (!rel.endsWith('.md')) continue; const dirRaw = dirname(rel); const dir = dirRaw === '.' ? '' : dirRaw; if (exclude.has(firstSegment(dir))) continue; const abs = join(corpusDir, rel); const st = statSync(abs); if (!st.isFile()) continue; pages.push({ slug: basename(rel, '.md'), dir, path: abs, mtimeMs: st.mtimeMs, }); } return pages; } export function readPageContent(page: PageFile): string { return readFileSync(page.path, 'utf8'); }