Inside CookSpec: compiling any recipe into one merge table
A recipe notation from 2004 that solved the format problem and was then ignored for twenty years. I rebuilt it as an extraction pipeline: paste a TikTok, a Reel, an article, or a photo, get back a single table with the ingredients on the left folding column by column into the finished dish, every unit normalized and checked against a density table. This is the architecture and the decisions that carried it.
By Russ. Thesis, 2026.
The inspiration
Someone posted a screenshot of a brownie recipe with the line, roughly: I cannot believe someone solved recipe design decades ago and everyone decided to ignore it.
The screenshot was Michael Chu's tabular notation from Cooking for Engineers, which he has been publishing at cookingforengineers.com since 2004. Ingredients run down the left edge. Operations sit in cells to the right, each one spanning the rows it consumes, merging column by column until a single cell on the far right holds the finished dish. It is a dependency graph rendered as a table.
The format answers the two questions a recipe page normally buries. What goes with what, and in what order. You read the merge structure in one look instead of reconstructing it from twelve numbered paragraphs. It is dense, it prints on one page, and it survives being glanced at with wet hands.
It never spread. Every recipe surface built since has moved the other way: longer preambles, more photography, a video where the quantities are spoken once and never written down. The format was correct and the distribution went to the opposite format.
The interesting part is not that the notation is good. It is that producing it by hand is real work, which is exactly why it never spread, and that work is now cheap. Reading an unstructured source and emitting a dependency graph is a task language models are genuinely suited to. The bottleneck that killed the format in 2004 is a solved problem in 2026.
The thesis in one line: a format fails when the labor to produce it exceeds the value it delivers. Change the cost of the labor and the format becomes viable without changing anything about the format.
What it does
CookSpec takes a recipe from wherever the user found it and returns one card. Accepted inputs are TikTok links, Instagram Reels, YouTube videos, article URLs, pasted text, photos of a recipe or a cookbook page, photos of a finished dish, and uploaded video files.
It does two things beyond restructuring. It normalizes every quantity to grams where a density is known, and it checks the source's own arithmetic. The brownie recipe that started this listed one third of a cup of cocoa as 80 grams. Hershey's label puts cocoa at 5 grams per tablespoon, which makes a full cup about 80 grams and a third of a cup about 27. The card shows 27, strikes the 80, and states the reasoning on tap. Silently fixing it would have been worse, because the user would never learn the tool was worth using.
Every quantity carries a status: stated, converted, corrected, estimated, or researched. Nothing renders without one. A validation strip at the top of the card counts them, and each count filters the table to the cells it covers.
Six decisions
1. Model the recipe as a tree, render the table from the tree
The card is not the data structure. The data structure is a set of ingredients and a set of operations, each operation naming the inputs it merges. That is a tree with the finished dish at the root. The renderer walks it and computes the row spans and column positions.
Keeping those separate is what made the second projection possible later. Six operation columns are unreadable on a phone, and most first sessions arrive from TikTok on a phone. Rather than inventing a second recipe format, the phone gets a second projection of the same tree: one operation at a time, full width, with the ingredients feeding it and a small map showing position in the merge. Same data, different traversal.
2. The hard layout constraint belongs in code, not in the prompt
The notation requires that every operation's inputs occupy contiguous rows. Otherwise a cell would need to span a gap, which a table cannot do. My first version asked the model to order ingredients so this held. It failed constantly, and the failures were tedious to repair.
The constraint has a closed-form solution. Because the graph is a tree, a depth-first walk from the root emits every subtree's ingredient leaves consecutively. So the model stopped being asked about ordering entirely, the instruction came out of the prompt, and a twelve-line function orders the list after extraction. That failure class disappeared.
This is the pattern the whole system is built on, and it recurs below in a more interesting form.
3. Cheap model plus a validation loop, not an expensive model
Structuring runs on a small fast model. The output goes through schema validation, then tree validation, then a rule that catches recipes claiming a staple was already cooked. A failure is not an error, it is a message back into the loop naming exactly what was wrong. Three attempts, and the third one escalates to a different model as the reliability anchor.
A typical compile is about four tenths of a cent and two to five seconds for an article. The validation loop, not the model tier, is doing most of the quality work.
4. Structured data first, prose second
Most recipe sites embed schema.org Recipe markup. When it is present, the pipeline reads ingredients and instructions directly and hands the model clean structured input rather than a page full of navigation and life story. When it is absent it falls back to readable text extraction.
Two consequences. Article compiles are faster and more accurate than video compiles by a wide margin. And on the sites that publish it, that markup doubles as ground truth for evaluation, which is what later made it possible to measure ingredient coverage without hand-labeling anything.
5. Media in one call, not two
The first video path was two calls: a vision model described the video, then the text model structured the description. It worked and it was slow, and the intermediate description lost timing detail.
The current path sends the media and the structuring rules in a single call and gets the tree back directly. The two-step version survives as the fallback when the single call fails validation twice. YouTube ingestion needs no scraper at all, since the model accepts the video URL directly. That discovery removed a whole vendor dependency from the roadmap.
6. Deduplicate on the canonical URL, not the raw one
A viral video gets pasted by many people, with different tracking parameters attached each time. The pipeline canonicalizes first, hashes the canonical URL, and checks the database before spending anything. A repeat compile returns in about sixty milliseconds at zero marginal cost, and every compile becomes a permanent public page, so the library grows as a byproduct of use rather than as a seeding project.
Where it runs
Next.js on Cloudflare Workers through the OpenNext adapter. Supabase for Postgres, auth, and storage, with row-level security. Gemini for vision and fast structuring, DeepSeek as the reliability anchor, a scraper vendor for the two platforms that require one. Deploys run from GitHub Actions on push. The card is a real DOM table, never an image, because an image loses selection, search, print, and screen readers.
It is live at cookspec.xyz.
What this project is actually about
The cooking is incidental. The transferable problem is turning unstructured multimodal sources into a strict schema that downstream code can render and validate, cheaply enough to run on every request, with honest provenance on every field and a refusal path when the source does not contain what the user asked for.
That is the same problem as extracting terms from a lease, figures from an operating statement, or obligations from a contract. The domain here is forgiving, which is exactly why it is a good place to develop the machinery: when the extraction is wrong, dinner is worse, and nobody underwrites a loan against it.
The second half of the story is what happened when I ran a thousand real inputs through it and graded the output. That is the Field Note.