// parseConf — read a `.conf` (TOML 1.0) pipeline configuration into a raw // RunConfig. The `.conf` grammar is a text projection of RunConfig: JSON stays // canonical, this is the human-authoring surface. Every framework section is // strict (an unknown section or a typo'd reserved key is an error, never // silently ignored), and every failure mode gets a coded ConfIssue. // // parseConf returns the *raw* config (block params not yet expanded through // their schemas, no levelset version stamp) — callers compose it with // fillRunConfig / runFromConfig. With a registry it additionally runs // validateGraph and the shared referential checks (checkRunConfigRefs), so a // weights/objective/collector reference that doesn't resolve fails here too. import { z } from "zod"; import { parse as parseToml, TomlError } from "smol-toml"; import { validateGraph } from "../level-blocks/graph.ts"; import type { BlockDef, BlockRegistry } from "../level-blocks/registry.ts"; import { checkRunConfigRefs, runConfigSchema, validateSweepConfig, type RunConfig, } from "../level-blocks/runConfig.ts"; import type { ConfIssue } from "./issues.ts"; export type ParseConfResult = | { ok: true; config: RunConfig; issues: ConfIssue[] } | { ok: false; issues: ConfIssue[] }; export interface ParseConfOptions { readonly registry?: BlockRegistry | undefined; } /** Keys peeled off a `[blocks.]` table as instance fields rather than * params. A block whose schema declares a param of one of these names can't * round-trip through conf (the reserved reading always wins) — E305. */ export const RESERVED_BLOCK_KEYS = [ "code", "enabled", "provenance", "shadow", ] as const; const ALLOWED_SECTIONS = new Set([ "levelset", "resolve", "pipeline", "blocks", "weights", "objective", "sweep", ]); const LEVELSET_KEYS = new Set(["format", "version", "seed"]); const PIPELINE_KEYS = new Set(["systemCode"]); // Named-property views over the loosely-typed TOML tables — so the known // sections read as `doc.levelset`, not index-signature access. interface ConfDoc { levelset?: unknown; resolve?: unknown; pipeline?: unknown; blocks?: unknown; weights?: unknown; objective?: unknown; sweep?: unknown; [key: string]: unknown; } interface LevelsetTbl { format?: unknown; version?: unknown; seed?: unknown; [key: string]: unknown; } interface PipelineTbl { systemCode?: unknown; [key: string]: unknown; } interface AssembledConfig { formatVersion: 1; levelset?: unknown; seed?: unknown; resolve?: unknown; pipeline?: unknown; weights?: unknown; objective?: unknown; sweep?: unknown; } interface BlockInstanceDraft { instanceId: string; code: unknown; params: unknown; provenance?: unknown; shadow?: unknown; enabled?: unknown; } function isPlainObject(value: unknown): value is Record { return ( typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) ); } /** Render an arbitrary TOML value for an issue message. Scalars print bare; * a table or array prints as JSON rather than "[object Object]". */ function describeValue(value: unknown): string { if (typeof value === "string") return value; return JSON.stringify(value) ?? String(value); } /** The block schema's top-level property names — the conf param names — used * to detect reserved-key collisions. */ export function blockParamNames(def: BlockDef): string[] { try { const js = z.toJSONSchema(def.block.schema.input, { io: "input" }) as { properties?: Record; }; return Object.keys(js.properties ?? {}); } catch { return []; } } /** Reserved conf keys a block's params collide with (empty when clean). */ export function reservedParamCollisions(def: BlockDef): string[] { const params = new Set(blockParamNames(def)); return RESERVED_BLOCK_KEYS.filter((k) => params.has(k)); } function unknownSectionMessage(key: string): string { if (key === "search") { return `[search] is not a conf section — search is a block; add it under [blocks.] with code = "levelset.SerialSGS.v1"`; } if (key === "profiles") { return `[profiles] is not a conf section — schedule profiles are runtime memoization, not config state`; } return `Unknown top-level section [${key}] — the conf grammar allows only levelset, resolve, pipeline, blocks, weights, objective, sweep`; } /** Deep-scan for TOML datetime literals (E304) and non-finite numbers (E306), * which valid TOML admits but a config forbids. Reports each with its path. */ function scanValues(value: unknown, path: string, issues: ConfIssue[]): void { if (value instanceof Date) { issues.push({ code: "LEVELSET_E304", path, message: `${path}: TOML datetime literals are not accepted — quote the date as an ISO string (e.g. "2026-01-01")`, }); return; } if (typeof value === "number" && !Number.isFinite(value)) { issues.push({ code: "LEVELSET_E306", path, message: `${path}: non-finite number (${String(value)}) — configs must be finite; model "unbounded" as an absent optional param`, }); return; } if (Array.isArray(value)) { value.forEach((v, i) => scanValues(v, `${path}[${String(i)}]`, issues)); return; } if (isPlainObject(value)) { for (const [k, v] of Object.entries(value)) scanValues(v, path === "" ? k : `${path}.${k}`, issues); } } /** Translate a zod issue path (over the assembled RunConfig) into a * conf-oriented dotted path, so a param error reads `blocks..max` * rather than `pipeline.blocks.0.params.max`. */ function confPath( path: readonly PropertyKey[], blocks: readonly { instanceId: string }[], ): string { const seg = path.map(String); if (seg[0] === "pipeline" && seg[1] === "blocks") { const idx = Number(seg[2]); const id = blocks[idx]?.instanceId ?? seg[2]; const rest = seg.slice(seg[3] === "params" ? 4 : 3); return ["blocks", id, ...rest].join("."); } if (seg[0] === "formatVersion") return "levelset.format"; if (seg[0] === "seed") return "levelset.seed"; if (seg[0] === "levelset") return "levelset.version"; return seg.join("."); } /** * Parse `.conf` text into a raw RunConfig. Returns `{ ok: true, config }` when * the text is a well-formed config, else `{ ok: false, issues }`. Pass a * registry to additionally validate the graph and its scorer references. */ export function parseConf( text: string, opts?: ParseConfOptions, ): ParseConfResult { let doc: ConfDoc; try { doc = parseToml(text); } catch (err) { if (err instanceof TomlError) { return { ok: false, issues: [ { code: "LEVELSET_E301", message: `TOML syntax error: ${err.message.split("\n")[0] ?? err.message}`, pos: { line: err.line, column: err.column }, }, ], }; } throw err; } // Datetime / non-finite scan first: these are otherwise-valid TOML values a // config can't hold, and reporting them here beats a confusing downstream // shape error. const valueIssues: ConfIssue[] = []; scanValues(doc, "", valueIssues); if (valueIssues.length > 0) return { ok: false, issues: valueIssues }; const issues: ConfIssue[] = []; // Strict top level: unknown sections are errors. for (const key of Object.keys(doc)) { if (!ALLOWED_SECTIONS.has(key)) { issues.push({ code: "LEVELSET_E302", path: key, message: unknownSectionMessage(key), }); } } // [levelset] — required, `format = 1` required and authoritative for both // runConfig.formatVersion and pipeline.formatVersion. const levelsetTbl: LevelsetTbl | undefined = isPlainObject(doc.levelset) ? doc.levelset : undefined; if (!levelsetTbl || levelsetTbl.format === undefined) { issues.push({ code: "LEVELSET_E303", path: "levelset.format", message: `[levelset] format is required — add \`[levelset]\` with \`format = 1\``, }); } else { if (levelsetTbl.format !== 1) { issues.push({ code: "LEVELSET_E303", path: "levelset.format", message: `Unsupported [levelset] format ${describeValue(levelsetTbl.format)} — this engine supports format 1`, }); } for (const key of Object.keys(levelsetTbl)) { if (!LEVELSET_KEYS.has(key)) { issues.push({ code: "LEVELSET_E302", path: `levelset.${key}`, message: `Unknown key [levelset].${key} — allowed: format, version, seed`, }); } } } // Assemble a RunConfig. formatVersion is forced to its only legal value (1) // so the `format` marker's own validation (E303) is the sole authority and // zod never double-reports it. const assembled: AssembledConfig = { formatVersion: 1 }; if (levelsetTbl?.version !== undefined) assembled.levelset = levelsetTbl.version; if (levelsetTbl?.seed !== undefined) assembled.seed = levelsetTbl.seed; if (doc.resolve !== undefined) assembled.resolve = doc.resolve; // [pipeline] carries only systemCode; formatVersion comes from [levelset], // blocks from [blocks.*]. const pipelineTbl: PipelineTbl | undefined = isPlainObject(doc.pipeline) ? doc.pipeline : undefined; if (pipelineTbl) { for (const key of Object.keys(pipelineTbl)) { if (!PIPELINE_KEYS.has(key)) { issues.push({ code: "LEVELSET_E302", path: `pipeline.${key}`, message: `Unknown key [pipeline].${key} — a block goes under [blocks.], not [pipeline]`, }); } } } // [blocks.] tables, in document order. const blocks: BlockInstanceDraft[] = []; const blocksTbl = isPlainObject(doc.blocks) ? doc.blocks : undefined; if (blocksTbl) { for (const [instanceId, raw] of Object.entries(blocksTbl)) { if (!isPlainObject(raw)) { issues.push({ code: "LEVELSET_E302", instanceId, path: `blocks.${instanceId}`, message: `[blocks.${instanceId}] must be a table`, }); continue; } const { code, enabled, provenance, shadow, ...params } = raw; if (code === undefined) { issues.push({ code: "LEVELSET_E302", instanceId, path: `blocks.${instanceId}.code`, message: `[blocks.${instanceId}] is missing the required \`code\` (its registry block code)`, }); continue; } const instance: BlockInstanceDraft = { instanceId, code, params }; if (provenance !== undefined) instance.provenance = provenance; if (shadow !== undefined) instance.shadow = shadow; if (enabled !== undefined) instance.enabled = enabled; blocks.push(instance); // E305: a registry-resolved block whose schema declares a reserved-named // param — the value the user gave was silently peeled as an instance // field, so the param is unreachable. if (typeof code === "string") { const def = opts?.registry?.get(code); if (def) { for (const collision of reservedParamCollisions(def)) { issues.push({ code: "LEVELSET_E305", instanceId, path: `blocks.${instanceId}.${collision}`, message: `block "${instanceId}" (${code}) declares a param "${collision}" that collides with the reserved conf key "${collision}" — it cannot be set through the conf format`, }); } } } } } assembled.pipeline = { formatVersion: 1, ...(pipelineTbl?.systemCode !== undefined ? { systemCode: pipelineTbl.systemCode } : {}), blocks, }; // [weights] — number, or the string sentinel "report" ⇄ null (report-only). if (doc.weights !== undefined) { const wTbl = isPlainObject(doc.weights) ? doc.weights : undefined; if (!wTbl) { issues.push({ code: "LEVELSET_E302", path: "weights", message: `[weights] must be a table`, }); } else { const weights: Record = {}; for (const [name, value] of Object.entries(wTbl)) { if (value === "report") weights[name] = null; else if (typeof value === "number") weights[name] = value; else { issues.push({ code: "LEVELSET_E302", path: `weights.${name}`, message: `weights.${name} must be a number or the string "report" (report-only)`, }); } } assembled.weights = weights; } } if (doc.objective !== undefined) assembled.objective = doc.objective; if (doc.sweep !== undefined) assembled.sweep = doc.sweep; // Shape validation. formatVersion is pre-forced; everything else is checked // here (types, strict resolve/objective sections, required fields). const parsed = runConfigSchema.safeParse(assembled); if (!parsed.success) { for (const issue of parsed.error.issues) { const path = confPath(issue.path, blocks); issues.push({ code: "LEVELSET_E302", ...(path !== "" ? { path } : {}), message: `${path}: ${issue.message}`, }); } } if (issues.length > 0) return { ok: false, issues }; const config = parsed.success ? parsed.data : (assembled as RunConfig); // Registry validation (E308): graph shape + cardinality + scorer references. if (opts?.registry) { const validated = validateGraph(opts.registry, config.pipeline); if (!validated.ok) { return { ok: false, issues: validated.issues.map((i) => graphIssueToConf(i)), }; } const refIssues = checkRunConfigRefs(config, opts.registry, validated); if (refIssues.length > 0) { return { ok: false, issues: refIssues.map((i) => graphIssueToConf(i)) }; } const sweepIssues = validateSweepConfig(config, opts.registry, validated); if (sweepIssues.length > 0) { return { ok: false, issues: sweepIssues.map((i) => graphIssueToConf(i)) }; } } return { ok: true, config, issues: [] }; } function graphIssueToConf(issue: { instanceId?: string; path?: (string | number)[]; message: string; }): ConfIssue { return { code: "LEVELSET_E308", message: issue.message, ...(issue.instanceId !== undefined ? { instanceId: issue.instanceId } : {}), ...(issue.path !== undefined ? { path: issue.path.map(String).join(".") } : {}), }; }