Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
repositories/aaf-holdings/hq01/lib/content/fs.ts
4.7 KB
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
import { CONTENT_ROOT, IGNORED_DIRS } from "./config";
/** Recursively collect files beneath `root` that match `predicate`. */
export function walkFiles(
root: string,
predicate: (filePath: string) => boolean,
maxDepth = 12,
): string[] {
const out: string[] = [];
if (!fs.existsSync(root)) return out;
const visit = (dir: string, depth: number) => {
if (depth > maxDepth) return;
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (entry.name.startsWith(".") && entry.name !== ".") continue;
if (IGNORED_DIRS.has(entry.name)) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(full, depth + 1);
} else if (entry.isFile() && predicate(full)) {
out.push(full);
}
}
};
visit(root, 0);
return out.sort();
}
export function readText(filePath: string): string {
return fs.readFileSync(filePath, "utf8");
}
/** Path relative to the workspace root, used as a stable id/source label. */
export function relPath(filePath: string): string {
return path.relative(CONTENT_ROOT, filePath).split(path.sep).join("/");
}
export interface ParsedDoc {
title: string;
meta: Record<string, string>;
lists: Record<string, string[]>;
blocks: Record<string, string>;
body: string;
frontmatter: Record<string, unknown>;
}
const HEADER_RE = /^([A-Za-z][A-Za-z0-9 /_&()-]{0,40}):\s?(.*)$/;
const BULLET_RE = /^[-*]\s+(.+)$/;
function keyOf(label: string): string {
return label.trim().toLowerCase();
}
/**
* Parse a markdown document into a structured shape. Supports two conventions
* used across the AAF repository:
* - YAML frontmatter (--- … ---), merged into `meta`.
* - Inline `Label: value` fields and `Section:` blocks (list or prose).
*/
export function parseDoc(raw: string): ParsedDoc {
const fm = matter(raw);
const frontmatter = (fm.data ?? {}) as Record<string, unknown>;
const content = fm.content.replace(/^/, "");
const lines = content.split(/\r?\n/);
let title = "";
const meta: Record<string, string> = {};
const lists: Record<string, string[]> = {};
const blocks: Record<string, string> = {};
// Seed meta with stringy frontmatter values.
for (const [k, v] of Object.entries(frontmatter)) {
if (typeof v === "string" || typeof v === "number") {
meta[keyOf(k)] = String(v);
} else if (Array.isArray(v)) {
lists[keyOf(k)] = v.map((x) => String(x));
}
}
type Section = { key: string; inline: string; lines: string[] };
const sections: Section[] = [];
let current: Section | null = null;
for (const rawLine of lines) {
const line = rawLine.trimEnd();
const h1 = line.match(/^#\s+(.+)$/);
if (h1) {
if (!title) title = h1[1].replace(/\s*#*\s*$/, "").trim();
current = null;
continue;
}
if (/^#{2,}\s+/.test(line)) {
// Sub-heading inside prose — keep it in the current block.
if (current) current.lines.push(line);
continue;
}
const header = line.match(HEADER_RE);
if (header && !line.includes("://")) {
current = { key: keyOf(header[1]), inline: header[2].trim(), lines: [] };
sections.push(current);
continue;
}
if (current) {
if (line.trim() === "" && current.lines.length === 0 && !current.inline) {
continue;
}
current.lines.push(line);
}
}
for (const section of sections) {
const bullets = section.lines
.map((l) => l.match(BULLET_RE))
.filter((m): m is RegExpMatchArray => Boolean(m))
.map((m) => m[1].trim());
if (bullets.length > 0) {
lists[section.key] = bullets;
}
const prose = [section.inline, ...section.lines.filter((l) => !BULLET_RE.test(l))]
.join("\n")
.trim();
if (prose) {
blocks[section.key] = prose;
// A single-line section is also a simple field.
if (!section.lines.length && section.inline) {
meta[section.key] = section.inline;
}
}
}
if (!title) {
title = (meta["name"] as string) || (meta["id"] as string) || "";
}
return { title, meta, lists, blocks, body: content.trim(), frontmatter };
}
/** First defined value among the candidate keys. */
export function pick(
record: Record<string, string>,
...keys: string[]
): string | undefined {
for (const k of keys) {
const v = record[keyOf(k)];
if (v) return v;
}
return undefined;
}
export function pickList(
record: Record<string, string[]>,
...keys: string[]
): string[] {
for (const k of keys) {
const v = record[keyOf(k)];
if (v && v.length) return v;
}
return [];
}
root · /srv/aaf