Pull a reduced end-to-end smoke forward into the slice so the view flow is exercised over a real on-disk recipe, not just a temp file. Add fixtures/prose.md — a recipe using only this slice's modelled constructs (frontmatter, headings, plain paragraphs) so it renders losslessly. integration_test now runs `kitchen view fixtures/prose.md` as a subprocess and asserts the frontmatter, separator, headings, paragraphs, and flat document order end-to-end. smoke_test covers `kitchen --help` exiting 0 with the view command listed. The full fixtures/basic.md smoke remains task 0007, which needs 0004's richer nodes before that fixture renders losslessly.
80 lines
6.3 KiB
Markdown
80 lines
6.3 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.
|
||
|
||
### Tests that are green on arrival
|
||
|
||
A few required-coverage tests document behaviour that the minimal implementation already satisfies and so pass without a preceding red (frontmatter empty/absent, flat document order, and the level-distinctness/suppression renderer test).
|
||
They assert real observable behaviour against independent literals rather than restating the implementation.
|
||
|
||
### Review follow-up applied
|
||
|
||
The `runCli` and `stripAnsi` test helpers were extracted into `packages/bin/src/test-support.ts` to remove duplication the review flagged across the bin test files.
|
||
The scaffold's remaining `test.todo` placeholders (future block/inline types, annotations) are left intact for their owning tasks.
|
||
|
||
### End-to-end coverage pulled forward
|
||
|
||
A reduced end-to-end smoke was pulled forward so the vertical slice is validated the way a user runs it.
|
||
`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.
|
||
`integration_test.ts` runs `kitchen view fixtures/prose.md` as a subprocess and asserts the ANSI-stripped output: exit 0, frontmatter YAML before the body, the separator, every heading and paragraph, and flat document order.
|
||
`smoke_test.ts` covers `kitchen --help` exiting 0 with the `view` command listed.
|
||
This is a subset of task 0007's smoke, which still owns the full `fixtures/basic.md` end-to-end once 0004's richer nodes make that fixture render losslessly.
|