← All decisions

Extend tools/decisions with a roadmap subcommand to validate and manage docs/development-plan.jsonl (RM-028)

toolingdecisionsdevelopment-planroadmap

Context

RM-028, tracked as a deferred roadmap item since decision 0009, named this gap explicitly: docs/development-plan.jsonl has been hand-edited since it was introduced, with no equivalent validation or ID-assignment tooling to the one docs/decisions.jsonl already has (tools/decisions/). This session made the gap concrete, not just theoretical: RM-033 (Breakout's own roadmap entry) was hand-edited twice via ad hoc Python one-liners to update its status/resolution/decision_refs -- exactly the kind of unchecked, error-prone hand-edit tools/decisions/ was built to prevent for decisions.jsonl in the first place (decision 0009's own original motivation). With Breakout's roadmap now fully closed out and no other active work item queued, this was picked as the next concrete piece of work.

Options considered

Rename/restructure tools/decisions/ into a new, differently-named package now that it manages two files, per RM-028's own original phrasing ('expanding the tool's name, schema, and CLI surface') — rejected
RM-028's description was written speculatively, before implementation, and 'a reasonable future direction' isn't binding on the implementation's actual shape. A rename would touch every place tools/decisions/ is referenced by path -- CI-adjacent docs, this project's own decisions.jsonl history (decision 0009's own references field), ARCHITECTURE.md -- for uncertain benefit: both files are the same kind of thing (a validated, checked-in project record), just with one needing an update capability the other structurally never does. Kept the directory/package name `decisions`, only broadened its module set and CLI surface (a `roadmap` subcommand alongside the renamed `decision` one).
Generalize decisions/id.gleam into one shared, prefix-and-width-parametrized opaque id type reused by both decision ids (4-digit, no prefix) and roadmap ids (RM-, 3-digit) — rejected
Would carry the prefix/width as runtime data rather than in the type itself, making a decision id and a roadmap id the exact same Gleam type -- silently interchangeable (comparable, substitutable) wherever the compiler currently guarantees they can't be, a real regression against this project's own standing 'maximize type safety' practice. Built a second small, distinct opaque type (decisions/roadmap_id.gleam) instead, matching this project's existing precedent of keeping structurally-similar-but-domain-distinct concepts as separate small modules rather than one generic abstraction (cooldown.gleam/stopwatch.gleam over glemy's own games, same reasoning).
Make roadmap update's Patch fields optional-meaning-unchanged (only supply the fields actually being changed) — rejected
Requires distinguishing 'field absent' from 'field present but null' during JSON decoding, adding real decode-layer complexity for a four-field record. Patch instead requires all four fields (status/resolution/decision_refs/doc_refs) always present, mirroring Draft's own established 'nothing implicit' convention -- a caller not changing decision_refs still writes its current value into the patch file, an explicit small cost in exchange for zero decode-layer ambiguity.
Cross-validate doc_refs (file existence) the same way decision_refs are cross-validated against decisions.jsonl — rejected
A decision_ref is an exact id checked against a closed, structured set (decisions.jsonl's own ids) -- an exact, reliable check. A doc_ref is a file path optionally followed by a #section fragment, resolved relative to a repo root this tool has no reliable way to locate from an arbitrary invocation directory, and validating only the file-path portion (not the fragment) would be a partial, misleading guarantee. decisions.jsonl's own free-form `references` field is already left unvalidated for the identical reason -- doc_refs staying unvalidated is consistent with that existing precedent, not a new gap.

Decision

Added decisions/roadmap_id.gleam (an RM-NNN opaque id type, 3-digit, mirroring decisions/id.gleam's own shape and guarantees), decisions/roadmap_schema.gleam (Status/RoadmapItem/Draft/Patch types plus decoders/encoders matching docs/development-plan.md's schema exactly, including a status vocabulary decoder for planned/in_progress/completed/deferred/superseded), and decisions/roadmap_log.gleam (read/next_id/append mirroring decisions/log.gleam's own contracts, plus a new update(path, id, patch) -> Result(Nil, LogError) capability decisions.jsonl's own append-only log structurally never needs, and validate_decision_refs for the decisions.jsonl cross-check). Renamed the CLI's bare-positional decision-append invocation to an explicit `decision` subcommand, and added `roadmap new`/`roadmap update` subcommands with matching draft/patch-file conventions. Every decision_refs entry supplied to either roadmap subcommand is validated against a real read of decisions.jsonl before anything is written. Dogfooded immediately: used `roadmap update` (not another hand-edit) to update RM-028's own status.

Verification

gleam test in tools/decisions passes -- 104 tests (up from 61 before this change: 12 new roadmap_id tests, 15 new roadmap_schema tests, 16 new roadmap_log tests, plus the pre-existing 61 all still passing unchanged). gleam build in tools/decisions is warning-free. Manually verified the CLI end-to-end against scratch copies of the real decisions.jsonl/development-plan.jsonl (not the real files) before touching anything real: roadmap update succeeded and wrote exactly the patched fields, leaving id/phase/phase_title/title/description untouched; roadmap update correctly rejected an unknown decision_refs id (DecisionRefNotFound) and an unknown item id (UnknownId), in both cases leaving the file byte-for-byte unchanged; roadmap new correctly appended a new item at the next free RM-NNN; the renamed `decision` subcommand still appends to decisions.jsonl correctly, and an unrecognized command prints the multi-line usage message. The main glemy repo's own gleam build/gleam test/deno task check-warnings all remain green and unaffected (tools/decisions is a wholly separate Gleam package, not part of the main package's build graph).

Consequences

docs/development-plan.jsonl status changes (the actual, recurring need this session hand-rolled twice) now go through the same validated, canonicalizing write path decisions.jsonl already had -- a malformed status value, a typo'd decision_refs id, or an accidental edit to a field that should stay fixed (phase/phase_title/title/description) is now a command failure instead of a silent, checked-in mistake. RM-028 itself is the first roadmap item ever updated via this tool rather than by hand. docs/decisions.md and docs/development-plan.md were both updated to document the new/changed invocation syntax. A third project record needing this same append-or-append-and-patch shape would extend this tool's module set the same way, not start a new one from scratch.

References