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:
96
.claude/spec/cli-view.md
Normal file
96
.claude/spec/cli-view.md
Normal 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 2–6 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
126
.claude/spec/core-parser.md
Normal 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 (1–6) 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.
|
||||
Reference in New Issue
Block a user