// c14 — adapter: loads + parses sama.profile.toml (the SAMA v2 profile // declaration at the repo root) and walks the source tree to feed the // v2 verifier. Layer 2 in SAMA v2 terms: this is the boundary where // external input (the TOML file on disk + the contents of src/) is // parsed into the typed SamaV2Input shape that the pure verifier in // c32_sama_v2_verify consumes. // // The TOML parser handles the subset our profile uses (string values, // string arrays, and arrays of inline tables) — not full TOML. The // alternative is depending on an external parser; the subset is small // enough that an inline implementation keeps the verifier dependency- // free and easy to inspect. import { readdirSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { PROFILE_ATOMIC_EXEMPTION_VALUES, PROFILE_LAYOUT_VALUES, PROFILE_TESTS_VALUES, type LayerNumber, type LayerSpec, type ProfileAtomicExemption, type ProfileLayout, type ProfileSpec, type ProfileTests, type SamaV2Input, type Sublayer, } from "./a31_sama_v2.ts"; // — TOML subset parser ---------------------------------------------- const stripComment = (line: string): string => { // Comments only outside string literals. Our profile keeps no '#' // inside strings so a naive split on the first '#' is fine. If the // profile ever needs that, escape via a sentinel and post-process. const idx = line.indexOf("#"); return idx === -1 ? line : line.slice(0, idx); }; const parseStringValue = (raw: string): string => { const t = raw.trim(); if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) { return t.slice(1, -1); } throw new Error(`expected quoted string, got: ${raw}`); }; const parseStringArray = (raw: string): string[] => { // Expect `[ "a", "b", ... ]` on a single line. const t = raw.trim(); if (!t.startsWith("[") || !t.endsWith("]")) { throw new Error(`expected [..] array, got: ${raw}`); } const inner = t.slice(1, -1).trim(); if (inner === "") return []; return inner.split(",").map((s) => parseStringValue(s.trim())); }; const parseInlineTable = (raw: string): Record => { // Expect `{ key = "value", key2 = "value2" }` on one line. const t = raw.trim(); if (!t.startsWith("{") || !t.endsWith("}")) { throw new Error(`expected inline table, got: ${raw}`); } const inner = t.slice(1, -1).trim(); const out: Record = {}; if (inner === "") return out; // Split on commas that aren't inside a quoted string. Our subset // doesn't use quoted commas, so a plain split is enough. for (const pair of inner.split(",")) { const eq = pair.indexOf("="); if (eq === -1) throw new Error(`malformed inline-table entry: ${pair}`); const key = pair.slice(0, eq).trim(); const value = pair.slice(eq + 1).trim(); out[key] = parseStringValue(value); } return out; }; interface ParseState { sections: Map>; } export const parseProfileToml = (text: string): ProfileSpec => { const state: ParseState = { sections: new Map() }; const top = new Map(); state.sections.set("__top__", top); // Pre-process: join continuation lines for multi-line arrays of // inline tables. Walk by char-level bracket tracking — when '[' is // open in a value, keep accumulating until the matching ']' arrives. const physLines = text.split("\n"); const logical: string[] = []; let buf = ""; let depth = 0; for (const raw of physLines) { const line = stripComment(raw); if (depth === 0) { if (buf === "") buf = line; else buf += " " + line; } else { buf += " " + line; } for (const c of line) { if (c === "[" || c === "{") depth++; else if (c === "]" || c === "}") depth--; } if (depth <= 0) { depth = 0; logical.push(buf); buf = ""; } } if (buf.trim() !== "") logical.push(buf); let currentSection = "__top__"; for (const raw of logical) { const line = raw.trim(); if (line === "") continue; if (line.startsWith("[") && line.endsWith("]")) { currentSection = line.slice(1, -1).trim(); if (!state.sections.has(currentSection)) { state.sections.set(currentSection, new Map()); } continue; } const eq = line.indexOf("="); if (eq === -1) throw new Error(`unparseable line: ${line}`); const key = line.slice(0, eq).trim(); const valueRaw = line.slice(eq + 1).trim(); let value: unknown; if (valueRaw.startsWith("[") && valueRaw.endsWith("]")) { // Array — string array or array of inline tables. Peek at the // first non-bracket char inside. const inner = valueRaw.slice(1, -1).trim(); if (inner.startsWith("{")) { // Array of inline tables. Split on commas at depth 0. const tables: Array> = []; let cur = ""; let d = 0; for (const c of inner) { if (c === "{") d++; if (c === "}") d--; if (c === "," && d === 0) { tables.push(parseInlineTable(cur)); cur = ""; } else { cur += c; } } if (cur.trim() !== "") tables.push(parseInlineTable(cur)); value = tables; } else { value = parseStringArray(valueRaw); } } else { value = parseStringValue(valueRaw); } state.sections.get(currentSection)!.set(key, value); } // Now assemble ProfileSpec. const samaVersion = top.get("sama_version") as string | undefined; const profile = top.get("profile") as string | undefined; if (typeof samaVersion !== "string" || typeof profile !== "string") { throw new Error("profile must declare `sama_version` and `profile` at the top level"); } // v2.1 optional dialect flags — see /sama/v2 §6.1–6.3 and the // ProfileSpec comment in a31_sama_v2.ts. Absent ≡ v2.0 defaults. const validateEnum = ( fieldName: string, raw: unknown, allowed: readonly T[], ): T | undefined => { if (raw === undefined) return undefined; if (typeof raw !== "string") { throw new Error( `profile field \`${fieldName}\` must be a string, got: ${typeof raw}`, ); } if (!(allowed as readonly string[]).includes(raw)) { const allowedQuoted = allowed.map((v) => JSON.stringify(v)).join(", "); throw new Error( `profile field \`${fieldName}\` has invalid value ${JSON.stringify(raw)} ` + `(expected one of: ${allowedQuoted}). ` + `See /sama/v2 §6 for the v2.1 dialect set.`, ); } return raw as T; }; const layout = validateEnum( "layout", top.get("layout"), PROFILE_LAYOUT_VALUES, ); const tests = validateEnum( "tests", top.get("tests"), PROFILE_TESTS_VALUES, ); const atomicExemption = validateEnum( "atomic_exemption", top.get("atomic_exemption"), PROFILE_ATOMIC_EXEMPTION_VALUES, ); const buildLayer = (k: LayerNumber): LayerSpec => { const sec = state.sections.get(`layers.${k}`); if (!sec) { throw new Error(`profile is missing required section [layers.${k}]`); } const sublayersRaw = sec.get("sublayers") as Array> | undefined; const prefixes = sec.get("prefixes") as string[] | undefined; const subs: Sublayer[] = []; if (sublayersRaw && sublayersRaw.length > 0) { sublayersRaw.forEach((row, index) => { if (!row.name || !row.prefix) { throw new Error(`[layers.${k}] sublayer ${index} missing name/prefix`); } subs.push({ name: row.name, prefix: row.prefix, index }); }); } else if (prefixes && prefixes.length > 0) { prefixes.forEach((prefix, index) => { subs.push({ name: "default", prefix, index }); }); } else { // Empty layer is permitted (spec §2.1: "Leave a canonical layer // empty"). The verifier just won't assign any file to it. } return { sublayers: subs }; }; return { samaVersion, profile, ...(layout !== undefined ? { layout } : {}), ...(tests !== undefined ? { tests } : {}), ...(atomicExemption !== undefined ? { atomicExemption } : {}), layers: { 0: buildLayer(0), 1: buildLayer(1), 2: buildLayer(2), 3: buildLayer(3), }, }; }; // — Filesystem I/O -------------------------------------------------- const REPO_ROOT_GUESS = process.cwd(); export const loadProfile = async ( repoRoot: string = REPO_ROOT_GUESS, ): Promise => { const path = resolve(repoRoot, "sama.profile.toml"); const text = await Bun.file(path).text(); return parseProfileToml(text); }; // Walk src/ and read every .ts (sources + test siblings) into a map // keyed by repo-relative path ("src/cXX_*.ts"). export const loadRepoFiles = ( repoRoot: string = REPO_ROOT_GUESS, ): Map => { const srcDir = resolve(repoRoot, "src"); const out = new Map(); const entries = readdirSync(srcDir, { withFileTypes: true }); for (const e of entries) { if (!e.isFile() || !e.name.endsWith(".ts")) continue; const repoPath = `src/${e.name}`; out.set(repoPath, readFileSync(resolve(srcDir, e.name), "utf8")); } return out; }; // Convenience: composes loadProfile + loadRepoFiles into the // SamaV2Input the verifier consumes. Handler code calls this then // passes the result straight to verifySamaV2. export const buildSamaV2Input = async ( repoRoot: string = REPO_ROOT_GUESS, ): Promise => ({ profile: await loadProfile(repoRoot), files: loadRepoFiles(repoRoot), });