Fold the view command module (viewFile, the error type, the read adapter) into index.ts so the command functions sit beside the guarded CLI, and generalise formatViewError into formatError matching over a CliError union that new fallible commands extend. Align the test files with ADR 0009's tiers by name: - index_test.ts now holds the in-process integration tests of the command functions (formerly view_test.ts) - end_to_end_test.ts holds the black-box subprocess e2e (formerly index_test.ts)
78 lines
6.7 KiB
Markdown
78 lines
6.7 KiB
Markdown
---
|
||
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
|
||
|
||
- [x] `parse` returns a `DocumentAST` `{ frontmatter, blocks, diagnostics }`; frontmatter is a plain object (empty when absent), diagnostics empty in the normal case
|
||
- [x] Core types live in a dedicated types module and are re-exported from the package entry point alongside `parse`
|
||
- [x] Headings (levels 1–6) and paragraphs are modelled as `HeadingBlock` and `ParagraphBlock`, with paragraph content as an inline array of `TextNode`
|
||
- [x] Blocks are flat and in document order (a heading is a sibling of the following paragraph, not its parent)
|
||
- [x] remark types do not appear in core's public API
|
||
- [x] `kitchen view <file>` reads the file, calls `parse`, and prints the rendered output
|
||
- [x] `render(ast)` is pure (no I/O, no side effects) and returns an ANSI-styled string
|
||
- [x] Frontmatter renders as raw YAML before the body, followed by a visual separator
|
||
- [x] Headings render bold and distinct by level; paragraphs render prose followed by a blank line
|
||
- [x] chalk styling is suppressed automatically when stdout is not a TTY
|
||
- [x] 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
|
||
- [x] Renderer unit tests (ANSI stripped) cover frontmatter, the separator, headings, and paragraphs
|
||
- [x] 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 1–6, 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 1–3, 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.
|