Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
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