|
| 1 | +// src/ingest.typ |
| 2 | +// TOML ingestion layer. Parses TOML strings into core constructor dicts. |
| 3 | +// |
| 4 | +// Public symbol: from-toml(raw) |
| 5 | +// |
| 6 | +// This is a core module — it imports sibling src/primitives.typ directly |
| 7 | +// (same layer as render.typ and validate.typ). It calls constructors to |
| 8 | +// ensure all validation fires on the parsed data. |
| 9 | +// |
| 10 | +// Partial TOML: missing sections produce missing keys in the result dict. |
| 11 | +// Full assembly: when all required sections are present, builds a prompt |
| 12 | +// via p-prompt(). Metadata ([rationale], constraint severity) is preserved |
| 13 | +// in the result but never rendered. |
| 14 | + |
| 15 | +#import "primitives.typ": p-context, p-schema, p-checkpoint, p-prompt |
| 16 | + |
| 17 | + |
| 18 | +// ───────────────────────────────────────────── |
| 19 | +// from-toml |
| 20 | +// |
| 21 | +// raw: string — TOML-encoded prompt data |
| 22 | +// |
| 23 | +// Returns a dictionary with optional keys: |
| 24 | +// context, schema, constraints, steps, inputs, checkpoints, |
| 25 | +// aspect, prompt, meta, constraints-meta |
| 26 | +// |
| 27 | +// Missing TOML sections → absent keys (no panic). |
| 28 | +// Present sections with invalid data → panic via constructor validation. |
| 29 | +// ───────────────────────────────────────────── |
| 30 | + |
| 31 | +#let from-toml(raw) = { |
| 32 | + let data = toml(bytes(raw)) |
| 33 | + let result = (:) |
| 34 | + |
| 35 | + // ── aspect metadata ── |
| 36 | + let aspect-data = data.at("aspect", default: none) |
| 37 | + if aspect-data != none { |
| 38 | + result.insert("aspect", aspect-data) |
| 39 | + } |
| 40 | + |
| 41 | + // ── context ── |
| 42 | + let ctx-data = data.at("context", default: none) |
| 43 | + if ctx-data != none { |
| 44 | + let ctx = p-context( |
| 45 | + id: ctx-data.at("id"), |
| 46 | + entries: ctx-data.at("entries"), |
| 47 | + ) |
| 48 | + result.insert("context", ctx) |
| 49 | + } |
| 50 | + |
| 51 | + // ── schema ── |
| 52 | + let schema-data = data.at("schema", default: none) |
| 53 | + if schema-data != none { |
| 54 | + let sch = p-schema( |
| 55 | + id: schema-data.at("id"), |
| 56 | + fields: schema-data.at("fields"), |
| 57 | + ) |
| 58 | + result.insert("schema", sch) |
| 59 | + } |
| 60 | + |
| 61 | + // ── constraints (extract text, preserve severity as metadata) ── |
| 62 | + let constraints-data = data.at("constraints", default: none) |
| 63 | + if constraints-data != none { |
| 64 | + let constraint-texts = constraints-data.map(c => c.at("text")) |
| 65 | + result.insert("constraints", constraint-texts) |
| 66 | + |
| 67 | + // Preserve per-constraint metadata (severity, etc.) |
| 68 | + let has-meta = constraints-data.any(c => c.keys().len() > 1) |
| 69 | + if has-meta { |
| 70 | + let meta-list = constraints-data.map(c => { |
| 71 | + let m = (:) |
| 72 | + for key in c.keys() { |
| 73 | + if key != "text" { |
| 74 | + m.insert(key, c.at(key)) |
| 75 | + } |
| 76 | + } |
| 77 | + m |
| 78 | + }) |
| 79 | + result.insert("constraints-meta", meta-list) |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + // ── steps (extract text) ── |
| 84 | + let steps-data = data.at("steps", default: none) |
| 85 | + if steps-data != none { |
| 86 | + let step-texts = steps-data.map(s => s.at("text")) |
| 87 | + result.insert("steps", step-texts) |
| 88 | + } |
| 89 | + |
| 90 | + // ── inputs ── |
| 91 | + let inputs-data = data.at("inputs", default: none) |
| 92 | + if inputs-data != none { |
| 93 | + result.insert("inputs", inputs-data) |
| 94 | + } |
| 95 | + |
| 96 | + // ── checkpoints ── |
| 97 | + let checkpoints-data = data.at("checkpoints", default: none) |
| 98 | + if checkpoints-data != none { |
| 99 | + let cps = checkpoints-data.map(c => p-checkpoint( |
| 100 | + id: c.at("id"), |
| 101 | + after-step: c.at("after-step"), |
| 102 | + assertion: c.at("assertion"), |
| 103 | + on-fail: c.at("on-fail"), |
| 104 | + )) |
| 105 | + result.insert("checkpoints", cps) |
| 106 | + } |
| 107 | + |
| 108 | + // ── rationale (metadata, not rendered) ── |
| 109 | + let rationale-data = data.at("rationale", default: none) |
| 110 | + if rationale-data != none { |
| 111 | + let meta = result.at("meta", default: (:)) |
| 112 | + meta.insert("rationale", rationale-data) |
| 113 | + result.insert("meta", meta) |
| 114 | + } |
| 115 | + |
| 116 | + // ── full prompt assembly ── |
| 117 | + // Only when all required sections are present. |
| 118 | + let has-aspect = result.at("aspect", default: none) != none |
| 119 | + let has-context = result.at("context", default: none) != none |
| 120 | + let has-constraints = result.at("constraints", default: none) != none |
| 121 | + let has-steps = result.at("steps", default: none) != none |
| 122 | + let has-inputs = result.at("inputs", default: none) != none |
| 123 | + let has-schema = result.at("schema", default: none) != none |
| 124 | + |
| 125 | + if has-aspect and has-context and has-constraints and has-steps and has-inputs and has-schema { |
| 126 | + let a = result.at("aspect") |
| 127 | + let prompt = p-prompt( |
| 128 | + id: a.at("id"), |
| 129 | + version: a.at("version"), |
| 130 | + role: a.at("role"), |
| 131 | + ctx: result.at("context"), |
| 132 | + constraints: result.at("constraints"), |
| 133 | + steps: result.at("steps"), |
| 134 | + inputs: result.at("inputs"), |
| 135 | + schema: result.at("schema"), |
| 136 | + checkpoints: result.at("checkpoints", default: ()), |
| 137 | + ) |
| 138 | + result.insert("prompt", prompt) |
| 139 | + } |
| 140 | + |
| 141 | + result |
| 142 | +} |
0 commit comments