Cut the first vertical slice through both layers: a runnable `kitchen view <file>` that reads a Recipe File, parses it, and renders it styled to the terminal. @kitchen-md/core exposes a pure, total `parse(input): DocumentAST` built on a minimal remark pipeline (parse + frontmatter) with a translation layer to core's own AST types — frontmatter passthrough, HeadingBlock, ParagraphBlock, and TextNode. remark types never leak into the public API. @kitchen-md/bin's `view` subcommand (commander) reads the file and hands the DocumentAST to a pure `render(ast): string` (chalk, ANSI auto-suppressed off a TTY). Fallible file I/O is modelled as a neverthrow Result over a tagged-union CliError, matched at the boundary: stdout on success, stderr and exit 1 on failure. The command functions sit beside the import.meta.main-guarded CLI entry so they are testable in-process. Tests follow ADR 0009's tiers — unit (parse, render), integration (the view command's Result), and e2e (the binary as a subprocess) — with coverage, a path-scoped test-report generator, and a prose fixture rounding out the tooling. ADR 0008 records errors-as-values at the CLI boundary; ADR 0009 records the testing tiers.
2.2 KiB
ADR 0008 — Errors as Values at the CLI Boundary
Status: Accepted
Context
@kitchen-md/bin's view command performs a fallible operation: reading a file from disk.
Node's readFileSync signals failure by throwing.
Two idioms are available: let exceptions propagate and catch them at the entry point, or represent failure as a value the type system tracks.
@kitchen-md/core's parser already takes the second path — it is a total function that never throws and reports problems through the Document AST's diagnostics (see ADR 0004).
Decision
Model fallible operations in @kitchen-md/bin as a neverthrow Result<T, E>, with error types expressed as plain-data tagged unions (e.g. ViewError = { tag: "read-failed"; … }).
A throwing API is wrapped in a small adapter that converts the exception into an err, so nothing above the adapter leaks exceptions.
The CLI entry point matches the Result at the boundary: stdout on success, stderr plus a non-zero exit on failure.
Rationale
It keeps the bin layer consistent with core's total-function stance, so the whole codebase treats recoverable failure as data rather than control flow.
The possibility of failure becomes visible in a function's signature instead of hidden behind a throw.
The success value cannot be read without first handling the error case, which removes a class of mistakes at compile time.
A tagged-union error type gives exhaustive handling: a new failure mode is a new tag that every match must account for.
Because failure is a returned value rather than a side effect, error propagation can be asserted in-process by the integration tier, without spawning the binary (see ADR 0009).
Consequences
@kitchen-md/bin takes a dependency on neverthrow.
try/catch is confined to the thin adapters that wrap throwing APIs, and the rest of the layer is exception-free.
Must-use enforcement — that a Result is never silently dropped — is a convention (always terminate a Result at a .match at the boundary), not a lint rule, because the project uses Biome alone and does not add ESLint's eslint-plugin-neverthrow for now.
New failure modes stay additive: a new tag on the error union, handled at the boundary.