chore: add Biome and document design decisions from grill session

- Add @biomejs/biome 2.5.3 with 2-space indent, double quotes, trailing commas
- Add lint and format scripts to root package.json
- Update CONTEXT.md: resolve Structured Output shape, add Document AST, Block, Inline Node terms
- Add ADR 0002 (remark as internal Markdown parser)
- Add ADR 0003 (core defines own AST types)
- Add spec: core-parser (Parser implementation and Document AST)
- Add spec: cli-view (kitchen view <file> command)
This commit is contained in:
2026-07-08 23:29:09 -04:00
parent d37988ec59
commit 5841ae5de8
8 changed files with 390 additions and 5 deletions

96
.claude/spec/cli-view.md Normal file
View File

@@ -0,0 +1,96 @@
## Problem Statement
`@kitchen-md/bin` has an empty CLI entry point.
Users have no way to view a Recipe File in the terminal — they must open a text editor or Markdown viewer to read a recipe.
The `kitchen` binary exists in the scaffold but accepts no commands and produces no output.
## Solution
Implement the `kitchen view <file>` subcommand.
It reads a Recipe File from disk, passes the content to `@kitchen-md/core`'s `parse` function, and walks the resulting Document AST through a renderer that produces ANSI-styled terminal output.
Markdown structure is styled for readability; KitchenMD annotations are rendered as their raw source text for now.
## User Stories
1. As a CLI user, I want to run `kitchen view <file>` and see the recipe rendered in the terminal so that I can read it without opening a text editor.
2. As a CLI user, I want headings styled distinctly by level so that I can scan the recipe's structure at a glance.
3. As a CLI user, I want bold and italic text preserved in the terminal output so that emphasis in the recipe prose is visible.
4. As a CLI user, I want frontmatter shown as raw YAML before the recipe body so that I can see the recipe's metadata.
5. As a CLI user, I want wikilinks rendered distinctly from surrounding prose so that cross-references are visually identifiable.
6. As a CLI user, I want a clear error message when the file does not exist so that I am not left with a cryptic failure.
7. As a CLI user, I want the binary to exit with a non-zero code on error so that shell scripts can detect failure.
8. As a CLI user, I want ANSI styling suppressed automatically when output is piped so that downstream tools receive clean text.
## Implementation Decisions
- commander is used for argument parsing.
The `view` subcommand takes a single required positional argument: the path to a Recipe File.
No flags or options beyond the file path are implemented at this stage.
- The entry point reads the file from disk, passes the content string to `parse` from `@kitchen-md/core`, and passes the returned `DocumentAST` to the renderer.
File I/O is confined to the entry point.
- The renderer is a pure function `render(ast: DocumentAST): string`.
It walks the Document AST and returns an ANSI-styled string.
It has no filesystem access and no side effects.
The separation between the entry point (file I/O + commander) and the renderer (pure AST walk) allows the renderer to be tested without invoking a subprocess.
- **Rendering rules:**
- Frontmatter: serialised back to YAML and printed before the document body, followed by a visual separator.
- `HeadingBlock`: bold text; level 1 is the most visually prominent, with levels 26 progressively less so.
- `ParagraphBlock`: inline-rendered text followed by a blank line.
- `ListBlock`: each item on its own line, preceded by a bullet character (unordered) or sequential number (ordered).
- `CodeBlock`: literal text content, rendered as-is with no syntax highlighting.
- `ThematicBreakBlock`: a horizontal rule character string.
- `TextNode`: plain string output.
- `EmphasisNode` (italic): chalk italic.
- `StrongNode` (bold): chalk bold.
- `CodeSpanNode`: a visually distinct style (e.g. chalk dim or inverse).
- `LinkNode`: rendered as the inline content only; href is not shown.
- `WikilinkNode`: display text (or target if no display text), rendered with an underline or distinct colour.
- `TransclusionNode`: rendered as its raw source text (e.g. `![[file#section:1]]`).
- `IngredientNode`, `CookwareNode`, `TimerNode`: rendered as their raw annotation source text (e.g. `@flour{200 g}`, `$pan{}`, `~2-3 mins`).
Distinct annotation styling is out of scope for this spec.
- chalk is used for all ANSI styling.
chalk auto-detects whether stdout is a TTY and disables colour codes when output is piped, satisfying user story 8 without an explicit flag.
- **Error handling:**
- If the file path argument is missing, commander prints usage to stderr and exits with code 1.
- If the file does not exist or cannot be read, the entry point prints a human-readable error message to stderr and exits with code 1.
- No other error conditions are handled at this stage; parse errors from `@kitchen-md/core` are not expected (the parser is resilient to any valid Markdown string).
## Testing Decisions
- Two seams are tested:
**Renderer unit tests** — given a `DocumentAST` constructed inline in the test, assert the `render` function's output with ANSI codes stripped.
Tests cover every Block type, every Inline Node type, frontmatter output, and the visual separator between frontmatter and body.
No subprocess invocation; no filesystem access.
**Smoke tests** — invoke the `kitchen view` binary as a subprocess, capture stdout, strip ANSI codes, and assert the plain text content.
Smoke tests use `fixtures/basic.md` as input and assert that the output contains the expected rendered text for the fixture's headings, paragraphs, frontmatter, and wikilinks.
Smoke tests use Bun's subprocess API, consistent with the project scaffold's smoke test approach.
- ANSI colour and weight choices are visual decisions verified by inspection, not automated test assertions.
Assertions operate on stripped plain text only.
- Tests are co-located with source and follow the naming conventions established in the project scaffold.
## Out of Scope
- Distinct styling for KitchenMD annotation nodes (Ingredient, Cookware, Timer).
Annotations render as raw source text in this spec.
- Pager support.
- Multiple file arguments.
- `--no-color` flag (chalk auto-detects TTY).
- Syntax highlighting for code blocks.
- Formatted frontmatter display (title, servings, tags as a styled header).
- Any subcommand other than `view`.
## Further Notes
- Annotation raw source text (`@flour{200 g}`, `$pan{}`, `~2-3 mins`) reads naturally as plain text in the terminal.
This matches how Recipe Files render in Obsidian without a plugin — the format is designed to be readable without tooling.
- The renderer's purity is intentional.
Keeping file I/O in the entry point and rendering in a pure function is the minimum seam needed for deterministic unit tests without subprocess overhead.

126
.claude/spec/core-parser.md Normal file
View File

@@ -0,0 +1,126 @@
## Problem Statement
`@kitchen-md/core` has an empty entry point.
There is no Parser implementation, no public API, and no AST types.
Tooling that needs to extract structured data from Recipe Files — the CLI, and eventually an Obsidian plugin — has nothing to build on.
## Solution
Implement the Parser in `@kitchen-md/core`: a pure function that accepts a Recipe File string and returns a Document AST.
The Parser uses remark internally to handle CommonMark and OFM syntax, runs a custom annotation transform to surface Ingredient, Cookware, and Timer nodes, and exposes a clean public API using core's own types — no remark internals leak through.
## User Stories
1. As a tooling user, I want to call `parse` with a Recipe File string and receive a Document AST so that I can build structured tooling without writing a parser myself.
2. As a tooling user, I want frontmatter passed through as a plain object so that I can read whatever fields I need without the parser imposing a schema.
3. As a tooling user, I want heading blocks in the Document AST so that I can render or reason about document structure.
4. As a tooling user, I want paragraph blocks with typed inline nodes so that I can render prose and distinguish plain text from structured content.
5. As a tooling user, I want list blocks with ordered and unordered variants so that recipe steps written as lists are represented faithfully.
6. As a tooling user, I want code blocks passed through without annotation parsing so that example syntax in documentation is not mistakenly extracted as recipe data.
7. As a tooling user, I want wikilinks represented as typed Inline Nodes so that I can render or process `[[links]]` distinctly from plain text.
8. As a tooling user, I want transclusions — including KitchenMD Step References — represented as typed Inline Nodes so that tooling can resolve them.
9. As a tooling user, I want Ingredient Annotations represented as Inline Nodes carrying name, quantity, and unit so that I can extract ingredient data from any recipe prose.
10. As a tooling user, I want Cookware Annotations represented as Inline Nodes carrying name and optional quantity so that I can extract equipment requirements.
11. As a tooling user, I want Timer Annotations represented as Inline Nodes carrying a value or range and a canonical unit so that I can extract timing information.
12. As a tooling user, I want annotations inside code spans and code blocks to be ignored so that the parser follows CommonMark inline processing rules.
13. As a future Obsidian plugin author, I want to import `@kitchen-md/core` without taking a transitive dependency on remark so that my plugin bundle does not include remark's internals.
## Implementation Decisions
- The public API exports a single `parse` function.
Given a Recipe File string, it returns a `DocumentAST`.
It is a pure function — no filesystem access, no side effects.
- `DocumentAST` top-level shape: `{ frontmatter, blocks }`.
- `frontmatter` — the raw parsed YAML metadata as a plain object, passed through without schema enforcement.
Empty object when no frontmatter is present.
- `blocks` — a flat, ordered array of Block nodes representing the document body.
The flat structure means headings are siblings of their following content, not parents of it.
See ADR 0003 and the Document AST entry in the domain glossary.
- **Block types** (initial set):
- `HeadingBlock` — level (16) and an inline content array.
- `ParagraphBlock` — an inline content array.
- `ListBlock` — ordered flag and an array of list items, each carrying an inline content array.
- `CodeBlock` — optional language identifier and a literal text string.
Annotation parsing does not run on code block content.
- `ThematicBreakBlock` — no content fields.
- `RawBlock` — carries the raw text of any remark node type not explicitly modelled above.
Ensures forward compatibility when the fixture or future files use block types not yet in the catalogue.
- **Inline Node types**:
- `TextNode` — a literal string of plain text.
- `EmphasisNode` — an inline content array (italic).
- `StrongNode` — an inline content array (bold).
- `CodeSpanNode` — a literal string.
Annotation parsing does not run on code span content.
- `LinkNode` — href string and an inline content array.
- `WikilinkNode` — target (filename without extension) and optional display text.
Covers both `[[link]]` and `[[link|display]]`.
- `TransclusionNode` — target and optional anchor string.
Covers `![[link]]`, `![[link#heading]]`, and KitchenMD Step References (`![[file#section:N]]` and `![[file#N]]`).
The anchor is passed through as-is; Step Reference resolution is out of scope for the parser.
- `IngredientNode` — name, optional quantity string, optional unit string.
- `CookwareNode` — name, optional quantity string, optional unit string.
- `TimerNode` — value string (single) or value + high strings (range), and a canonical unit abbreviation.
- remark is used internally as the CommonMark parser (see ADR 0002).
OFM wikilinks and transclusions are handled by `remark-wiki-link` and its underlying micromark and mdast-util packages.
KitchenMD annotations are processed by a custom remark transform plugin that walks mdast text nodes outside code contexts and splits them into annotation Inline Nodes.
remark types do not appear in `@kitchen-md/core`'s public API (see ADR 0003).
An internal translation layer maps remark's mdast output to core's own types before returning.
- Annotation parsing naturally respects CommonMark inline scoping: the annotation transform plugin operates only on mdast text nodes, which do not appear inside code spans or code blocks.
No additional scoping logic is required.
- Timer unit aliases are normalised to canonical abbreviations at parse time (`mins``min`, `hours``hr`, etc.).
The full alias table is defined in the Timer Annotation entry of the domain glossary.
- Quantity strings are preserved as-is (e.g. `"1 1/2"`, `"0.5"`, `"1/2"`).
No numeric coercion or arithmetic is performed at the parser level.
- Core's own types are defined in a dedicated types module within `@kitchen-md/core` and re-exported from the package entry point alongside `parse`.
## Testing Decisions
- All tests assert the public `parse` function's output.
No remark internals, no transform plugin internals, no mdast types appear in tests.
- **Unit tests** use inline raw strings only — no filesystem access.
Each test passes a Recipe File string to `parse` and asserts the returned Document AST.
Coverage must include:
- Frontmatter passthrough (arbitrary fields, empty frontmatter, no frontmatter)
- Each Block type: heading (all levels), paragraph, ordered list, unordered list, code block, thematic break
- Each Inline Node type: plain text, emphasis, strong, code span, link, WikilinkNode, TransclusionNode
- Ingredient Annotation: name, quantity, unit; multi-word name; unit-less quantity; no quantity
- Cookware Annotation: name with quantity and unit; name with no quantity; multi-word name
- Timer Annotation: single value form; range form; all supported unit aliases normalised to canonical form
- Annotations embedded mid-sentence (not at the start of a line)
- Annotations inside code spans — not extracted
- Annotations inside code blocks — not extracted
- A Step Reference transclusion — anchor passed through as-is
- **Integration tests** pass the content of `fixtures/basic.md` through `parse` and assert the complete Document AST, covering all annotation types, OFM features, and frontmatter together in a single realistic input.
- Tests are co-located with the source module and follow the `{module}_test.ts` naming convention established in the project scaffold.
## Out of Scope
- Quantity arithmetic or numeric normalisation (e.g. `1/2``0.5`).
Quantities are preserved as strings.
- Unit normalisation for Ingredient and Cookware units.
Those units are free-form and passed through as-is.
- Step counting and Step Reference resolution.
The parser surfaces `TransclusionNode` with the raw anchor; resolution is a consumer concern.
- Combined Recipe validation.
- Aisle mapping.
- Shopping list generation.
- CLI implementation (covered by the cli-view spec).
## Further Notes
- The "built from scratch" statement in the project spec refers to not forking Cooklang's parser.
Using remark as an internal dependency is consistent with that intent — see ADR 0002.
- The annotation transform plugin running only on text nodes is what enforces CommonMark scoping for free, without any explicit code-span or code-block detection logic in the plugin itself.
- `RawBlock` is a safety valve, not a target.
Implementation should aim to model all block types that appear in recipe fixtures explicitly.