Build a type-safe Gleam CLI tool for decisions.jsonl instead of ad-hoc Python
Context
decisions.jsonl was being appended to by hand-written, one-off Python scripts each time (json.dumps calls constructed inline). The user flagged this as impractical long-term and set one hard requirement for whatever replaced it: the best available type safety for validating input fields, enums, dates, and JSON formatting, both at compile-time and runtime, making malformed entries impossible to write.
Options considered
Decision
Built tools/decisions/ as a separate Gleam project (its own gleam.toml, kept out of the main glemy game package's dependency graph). decisions/date.gleam and decisions/id.gleam are opaque types constructible only via a validating parse() (real calendar-date validation including leap years; exactly-4-digit ids). decisions/schema.gleam defines Status/Verdict as proper sum types (no raw strings escape into the rest of the program) and a Decision record decodable only via gleam/dynamic/decode combinators -- so a value of type Decision is a static guarantee it was already validated. decisions/log.gleam reads and validates every line of the log before any write, refuses to write if the existing file is already corrupt, computes the next sequential id automatically, and automatically links supersedes/superseded_by in both directions when a new entry supersedes an old one. The CLI (src/decisions.gleam, Erlang-target only) takes a draft JSON file (the Decision schema minus id/superseded_by, which the tool assigns) and either appends cleanly or fails loudly without touching the file.
Verification
61 gleeunit tests across date/id/schema/log modules, covering non-happy-path cases specifically: invalid date formats, Feb 30, Feb 29 in non-leap years, all four leap-year century rules (divisible by 4/100/400), malformed/wrong-length/negative ids, unknown enum values, wrong JSON types, missing required fields, duplicate ids (both on read and on append), supersedes pointing at a nonexistent id, and appending to an already-corrupt file -- all confirmed to fail without modifying the file (verified by diffing file contents before/after). End-to-end CLI testing covered all four argument/error paths (no args, missing draft file, malformed JSON draft, and the full happy path) plus a real supersedes-linking round trip, checking the actual file contents after each run, not just exit codes.
Consequences
docs/decisions.jsonl is now managed by `cd tools/decisions && gleam run -- <draft.json>` rather than hand-run Python. The file's formatting is canonicalized (re-encoded from scratch) on every append, so accidental hand-edits to old entries get normalized back to the canonical compact single-line form the next time anything is appended. Every future decision, including this one, goes through the tool -- entries 0001-0008 were validated as readable by this tool's own log.read before this entry was appended through it.