This repository has been archived on 2026-07-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kitchen-md/.claude/tasks/0003-view-skeleton.md
alexion ab309d3226 feat: add kitchen view command and parser skeleton (task 0003)
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.
2026-07-28 23:31:03 -04:00

6.7 KiB
Raw Permalink Blame History

spec
spec
cli-view

What to build

The walking skeleton: a runnable kitchen view <file> that reads a Recipe File, parses it, and prints it styled in the terminal. This slice cuts the first complete thread through both layers with the smallest set of node types.

In @kitchen-md/core, stand up the parse function and the types module, modelling only what this slice renders: frontmatter passthrough, HeadingBlock, ParagraphBlock, and TextNode. Set up the minimal internal remark pipeline needed for these (parse + frontmatter), with the translation layer from remark's output to core's own types. parse is a pure, total function returning a DocumentAST of shape { frontmatter, blocks, diagnostics }.

In @kitchen-md/bin, implement the view subcommand with commander taking one required file-path argument. The entry point owns file I/O: it reads the file, calls parse, and passes the DocumentAST to a pure render(ast) function. render walks the AST and returns an ANSI-styled string using chalk, which auto-suppresses colour when stdout is not a TTY. Frontmatter prints as raw YAML followed by a visual separator, then the body: headings styled by level, paragraphs as prose with blank-line spacing.

The demoable outcome: kitchen view <recipe.md> shows metadata, headings, and prose; a missing file prints a human-readable error to stderr and exits non-zero.

Acceptance criteria

  • parse returns a DocumentAST { frontmatter, blocks, diagnostics }; frontmatter is a plain object (empty when absent), diagnostics empty in the normal case
  • Core types live in a dedicated types module and are re-exported from the package entry point alongside parse
  • Headings (levels 16) and paragraphs are modelled as HeadingBlock and ParagraphBlock, with paragraph content as an inline array of TextNode
  • Blocks are flat and in document order (a heading is a sibling of the following paragraph, not its parent)
  • remark types do not appear in core's public API
  • kitchen view <file> reads the file, calls parse, and prints the rendered output
  • render(ast) is pure (no I/O, no side effects) and returns an ANSI-styled string
  • Frontmatter renders as raw YAML before the body, followed by a visual separator
  • Headings render bold and distinct by level; paragraphs render prose followed by a blank line
  • chalk styling is suppressed automatically when stdout is not a TTY
  • A missing file path prints commander usage to stderr and exits 1; an unreadable/nonexistent file prints a human-readable error to stderr and exits 1
  • Renderer unit tests (ANSI stripped) cover frontmatter, the separator, headings, and paragraphs
  • Core unit tests cover frontmatter passthrough (arbitrary fields, empty, absent), headings at every level, and paragraphs with TextNode content

Implementation Notes

All acceptance criteria are met. The following decisions and scope boundaries are worth recording.

Scope boundaries carried by the slice

Only HeadingBlock, ParagraphBlock, and TextNode are modelled, as the slice specifies. The remark→core translation therefore skips any block that is not a heading or paragraph (lists, blockquotes, code, thematic breaks), and translateInline keeps only text nodes, dropping every other inline node type. The drop is by whole node, so emphasised or linked text is currently lost, not merely unstyled. This is within task 0003's stated scope; the lossless RawInline/RawBlock fallback and the typed EmphasisNode/StrongNode/LinkNode land in task 0004, which also makes the translation recurse into container children.

Malformed-frontmatter handling is out of scope here and owned by task 0007. This slice parses well-formed frontmatter and returns {} for the empty and absent cases; a genuinely malformed YAML block would currently throw from the YAML parser. The total-function guarantee for that case (returning {} plus an invalid-frontmatter diagnostic) arrives with 0007.

Types defined ahead of full use

Diagnostic, Point, and Position are defined in the types module because DocumentAST.diagnostics is typed Diagnostic[], even though only the empty case (diagnostics: []) is produced in this slice. This keeps the public shape stable; 0007 populates the channel.

Rendering decisions

Headings render distinct-by-level via chalk, tapering from bold at level 1 toward dim at level 6; a heading is followed by a single newline and a paragraph by a blank line, which is what visually separates them once ANSI is stripped. Per the cli-view spec, the specific colour and weight choices are visual decisions verified by inspection, not asserted in tests — the renderer tests assert ANSI-stripped text, spacing, and the presence/absence of styling, not particular colours. The frontmatter separator is a dimmed 40-character box-drawing rule.

Test suite structured on ADR 0009's tiers

The suite is organised by the three tiers ADR 0009 defines, each seam tested at exactly one tier. Because task 0003's code was already built, these are characterization tests — green on arrival — asserting observable behaviour against independent literals rather than restating the implementation.

Unit tests cover the two pure seams. packages/core/src/parse_test.ts asserts parse through the package barrel: frontmatter passthrough for arbitrary, empty, and absent blocks, headings at every level 16, paragraphs with TextNode content, flat document order, and empty diagnostics. packages/bin/src/render_test.ts asserts render on ANSI-stripped output: headings at every level, paragraph blank-line spacing, the frontmatter YAML with its separator, and their absence when frontmatter is empty.

The view command function and the CLI share packages/bin/src/index.ts: viewFile, formatError, and the CliError union are value-returning and tested in-process, while the thin program.parse() dispatch is guarded behind import.meta.main so importing the module for a test never runs the CLI. Integration tests (packages/bin/src/index_test.ts) assert that command function's returned Result in-process: ok with rendered output for a readable file, frontmatter passthrough, a read-failed error for a missing path, and formatError's message.

End-to-end tests (packages/bin/src/end_to_end_test.ts) drive the kitchen binary as a subprocess, asserting exit codes, stream routing, document order, ANSI suppression on a pipe, the missing-file and missing-argument errors, and --help. fixtures/prose.md is a committed recipe using only the constructs this slice models — frontmatter, headings at levels 13, and plain-text paragraphs — so it renders losslessly today. The full fixtures/basic.md end-to-end remains task 0007's, once 0004's richer nodes make that fixture render losslessly.