The ring
The recipe graph drawn as concentric arcs sweeping to a plate, one arc per workstream, generated at render from the graph and the fold — never authored per recipe.
From DESIGN.md, Signature components
Nothing may imply a fraction of a duration. The route is drawn leg by leg, one leg per step, at constant width — never a fill, never a percentage. Progress is a count of steps, because the total is the longest remaining path and the fold cannot know it.
From DESIGN.md, The ring grammar
Signature components
DESIGN.md, the whole section
- The ring. The recipe graph drawn as concentric arcs sweeping to a plate, one arc per
workstream, generated at render from the graph and the fold — never authored per recipe. It is the
carrier of "where am I", and its rules are the ring grammar.
It is a ring at any number of strands: past five it squishes further and grows taller rather
than turning into another drawing (owner, 17 October 2026, who was shown lanes above five and
picked the ring held), so a recipe cannot draw one way at the stove and another on the summary.
Past five its names are also set by a stricter rule (
ring-label-crowded.ts,ring-label-path.ts,ring-crowded.test.ts): none over any bead, a gap between any two, each whole, and none laid flat: a name follows its arc up to vertical and reads from bottom to top on either flank (owner, 21 October 2026, from the choices drawn on the six- and seven-strand maps, because a flat name among curved ones read as random). On the right flank, where the arc falls, it is written up a short path of its own. A ring of five or fewer is placed as it always was. The strand count is the recipe's, not the hob's. The Wellington runs a water bath, an oven, a fridge and a defrosting counter and no burner; the roast dinner has five strands, two on a burner. The drawing never changes shape for the count. - The step card. The ingredients ruled like a notebook first, amount in its own column; under them the step in the recipe's own sentence, set in Inter because it is an instruction; last the status line with the fixed action slot beside it — and at the line's end either the note's own control or, where Details holds more than the note, the way into Details. No bead: the surface, the lane's edge and the status word say all a mark said, and its column was 28px the sentence now has. The amount is large because the ingredient is the part of a step you actually need at the stove, and the sentence is the whole method because a headline that had dropped its figures made the cook open every card to find them (SCREENS §3).
- The group. A bracket drawn around the steps that are genuinely simultaneous, labelled "At the same time", and one scroll-snap target — so the cook meets concurrent work as one thing to read.
- The pill and the tick. The app's two small controls, and between them every "is this on?" it
asks. A pill is the shape —
.pill, one rule — worn by the rating answers, the measure switcher (on the grocery list one of its three is the app's own lemon, 1.45em in full colour whether chosen or not, the wash behind it saying which), the grocery list's three readings; a tick is.box, drawn byTickBox(components/tick-box.tsx) and worn by the grocery line, byTick, the checkbox component, and by each row ofPickSeveral, the pick-menu that ticks any number from a list (components/pick-several.tsx;PickMenupicks one). A pill is for a bounded choice you switch between over and over; one you set once and leave, or one whose count is the owner's business, takes a select instead — which is why the shopping list's readings are pills and its order is not (see SCREENS.md §1). On is the yellow, in both, whatever drives it:aria-pressed,aria-checked, a class, or a real:checked. Four triggers, one look, one place it is declared. The two checkboxes were the reason. Left native and merely given anaccent-color, they rendered as near-black squares — one of them directly beside the cream-and-yellow rating pills on the result card, so a single card spoke two design languages and the louder was not ours.Tickkeeps a real<input type="checkbox">, focusable and announced, and only takes over the paint. No control rests on white. A control at rest, which is every pill, tick and outlined button, and a switch's knob, is warm paper:--control(the card's own off-white, never--surface-lowest) under--sh-paper, the picker tiles' soft fall, edged in--control-edge, which is nothing. A surface made of the same paper (the cook screen, home's tonight card) sets--control-edgeto the faint--outline-variantso its controls are not lost on it, and the tick keeps that edge itself, being too small for a fall to show. The on state is still the yellow. A one-line field is the exception, below; so is the Google sign-in button, which keeps Google's own white face because Google's branding rules require it (owner, 13 October 2026). - The one-line field. Anything a cook types one line into (the sign-in email, first run's name, a
kitchen's or a list's name, the import link, the recipe search, a night's note, an ingredient's
note, the stove's timer minutes, a grocery line's name, amount and note, the grocery add
line) is
.fieldinglobals.css: a box on--controlunder--sh-paperwith a 1px--outlineedge all round, in--r-inner(12px) corners (owner, 13 October 2026, option A, a boxed edge; 12px everywhere on 21 October, so not the pill it first was, and one radius for every box you type in or pick into). The edge is--outlineand not--control-edgebecause a field has to be found:--outlineis 4.06:1 on the page, past the 3:1 WCAG 1.4.11 asks, and the faint edge a resting control wears is not; the owner's AA commitment of 5 October is why, andapp/field-edges.test.tsholds it. It is an<input>itself, or a wrapper (.field--lead) holding a mark and a.field__input; the search field is that wrapper written once, as theSearchFieldcomponent (the glass, the hidden label and the phone's typing hints), and every search in the app is one, never the markup again;.field--snugis the same box at the tap floor, for a row that shares its width with a button or a card with no height to spare (an ingredient's note, the stove's minutes and a grocery line's three all wear it). A screen sets only how it sits (width, flex, a tighter padding), never the box again. The list line,.field--line, is the one variant, and only the grocery list's add line wears it (option D): square corners and a 1px--outlinerule under it only, on the same paper and fall. It is a line on a list you write down, typed into in passing, so it reads as part of the list; every other field is filled once and submitted, and reads as a form box. Not this: the guided editor's underline, the open version name, and a select (a grocery line's aisle keeps its own box until the pick menu redraws it, edged in--outlinelike the rest). A many-line box is.field-area, the field's sibling inglobals.css: the stove's note on a step, the evening's note, the pasted recipe and the assistant's question are one box, warm paper under--sh-paperin--r-innercorners at--fs-field, not resizable, edged in--outlinelike the one-line field (owner, 20 October 2026, from drawings of both: it had been edged in--control-edge, which is nothing on the page and so left the box unfound). A screen sets only where it sits (a margin, a height to give back). Not the recipe page's note, which is borderless until it is written in. A pick menu's button standing as a field is that box too (PickMenu'sfield; owner, 21 October 2026, "12px everywhere"): Settings' Home kitchen and Language and the editor's unit picker take.field's edge, corners, paper, fall and 16px, Inter at 400 in--on-surface, and a chevron at the end, from the one rule inglobals.css(.pickmenu__button--field, listed with.field), and a screen sets only how wide it is. A pick button that is a word (the shelf's sort, the version menu, the kitchen line, the grocery list's title) is not a field and draws no box. - The quiet button. The second control beside the one yellow thing (the stove's Undo and Add note,
the timer panel's Start, Stop, Clear and Copy, the recap's and the share's Undo) is
.quiet-buttoninglobals.css: warm paper under--sh-paper,--taptall,--r-action, 14px at 600, the receding ink, and no edge whatever the ground, so the fall alone shows it. A screen sets only where it stands (the stove's 86px slot, the panel's 72px floor), never the box again, and the recap's and the share's Undo are one component,ResultUndo. The timer panel's four are the one exception to--taptall: 34px to look at, with a--taptarget from.tap-reach(owner, 19 October 2026: at 44px they filled their rows flush to the hairlines and crowded the minutes field). - The notebook card. How a group of things, or a choice from a few, is drawn: one warm card on the
page's own paper (
--surface) under--sh-paper, its lines ruled like a notebook, each field's name in a narrow left margin in the small label (.micro, in the brown) and its value on the line in the serif. In a choice the chosen line has the yellow tick in the margin and the light yellow wash across it. Owner, 12 October 2026, picking direction B of three over heavy cream wells ("too dull and heavy") and over a variant matched to the ingredient cards ("stick with B"). It is the Details tab's source and picker rows, the evening's three ways with Kids to mind, and the recap's questions, and not Settings or the kitchen card, which keep their own cards.components/notebookis its one home (Notebook,NotebookRow); a screen sets--nb-marginfor a narrower margin (the evening's holds only a tick),--nb-rowand--nb-padfor shorter rows where it must fit a short phone (Details', under 760px tall), and never draws its own ruled card. Its controls rest on--control, so they stand out from the card and are never white. - The choice track. A choice among a few whose parts run in an order (Light, This phone, Dark;
Small, Default, Large): one rounded warm-paper bar the card's width, equal parts until a word needs
more room (it never wraps or shrinks; the others share what is left), the chosen one filled yellow,
every part a tap high, a radio group the arrows move through, named by the card's own heading
(
labelledBy) and never by a second string. A pill stays for a set with no order (the foods you leave out) and a pick-from-a-list (PickMenu) for one that grows (Language).components/choice-track.tsxis its one home. - The pick menu. The one way to pick from a list, in the old dress (owner, 15 October 2026, over the
cream box of 12 October): a button with the pick's name and a chevron that turns up while it is open,
and a white box (
--surface-lowest) on--sh-mdwith a 1px--outline-variantedge and 12px corners, hung 0.1rem under it. Rows are a tap high with 12px corners and no rules between; names in the serif at 1.05rem in--primary, a day or count under one in small grey. The row showing is said by a tick in a 1rem gutter every row reserves (--on-surface-variant), never a yellow wash, so no name moves when the pick does; the tick-box menus (PickSeveral, the Filter and the books) have no gutter, their box is the tick. An optionalheadingis one line of small capitals over the rows, indented to the names (the kitchen menu's KITCHEN); afootis a band under a soft rule inset 0.3rem at each end (New list, Manage). The one variant isplain, for a short list of plain words (the editor's units): Inter 0.95rem rows with a--card-edgerule between, every row showing before the box scrolls. A box that shows every row (plain, and the tick-box menus) is as tall as its rows up to a cap and never taller than the room under its button:usePickBoxmeasures that from the rows' top to the foot of the page body, which is the nav's top, and writes it as--pick-room, so on a short phone the box ends above the nav and its last rows are reached by scrolling it. Every pick menu shuts on Escape, on a tap or focus outside it and before a pick acts; Up, Down, Home and End walk the rows, and opening puts focus on the row showing. A screen never draws a second menu: the top bar's kitchen menu, the editor's unit menu and the grocery list's picker are this one.components/pick-menu.tsxandpick-menu.cssare its one home. - The ask. The one question before an act that cannot be undone (owner, 22 October 2026, of the
recipe page's Delete: "codify this confirmation modal and make sure it's used everywhere"). It
stands in place of what opened it and never over it (a modal to collect one answer is ceremony, and
on a phone it hides the thing being asked about), as a named group: the thing in a question at
reading size, what goes with it in plain short sentences (the sentence that says what the act does
at full size, what it costs and what stays quieter), then the act, outlined, and the way out, a
bare word, last — so the safe answer is never the one dressed to be tapped, and a held Enter lands
on it. A further way out (a link to where the typing is) goes between the two, never after. With no
question it is the row (owner, 13 October 2026, "the buttons state their actions", on the lists,
the plan and the kitchen's people): the same two buttons, the act's accessible name carrying what it
acts on. Where it stands decides a
look(the name's menu, the kitchen's card, settings' card, the lead's seat, the row) and a look picks class names and nothing else. Not asks: a choice with no safe answer (a twist or a tweak), the assistant's consent (calm on purpose, neither answer destructive) and a refusal with an OK.components/confirm-ask.tsxis its one home; a screen words its own ask and never draws one, andhand-drawn-ask.test.tsfails the build on one that does. - The zest check. A warm 45° texture at half strength. It anchors the white cards to a linen surface; if you notice it as a pattern, it is too strong.
- The wait. What a screen shows while it has nothing to show yet: a simmer for a transition, bones shaped like what is coming for a data read. It appears on every screen, so it is a component rather than a screen — its rules are the wait.
The ring grammar
DESIGN.md, the whole section
The map's marks and the deck's states, each with exactly one meaning — because "honesty is in the
geometry" only works if the geometry can't equivocate. The reference that fixes these is
design/cooking-ring.mjs.
design/check.mjs asserts the ones a static deck can carry — the banned status words, the missing
button on a future step, the identical action slot, one named arc per workstream at each of one to
four, the plate, the group rules, and that nothing animates. It does not assert the four that
need either a running app or a second recipe to compare against: that a leg is drawn per step
rather than as a fill, that a leg's stroke says its state, that a blocked strand starts at the angle
it was freed at, and that prep arrives as a step rather than a phase. Two are tools/drive.mjs's
now — a leg counted per step, and the prep found in the list rather than before it — and two are
still nobody's: the stroke a leg wears, and the angle a freed strand starts at. Saying which is
which matters more than the count: a rule nobody checks is a rule that drifts, and pretending
otherwise is how the yellow ban died.
Nothing may imply a fraction of a duration. The route is drawn leg by leg, one leg per step, at constant width — never a fill, never a percentage. Progress is a count of steps, because the total is the longest remaining path and the fold cannot know it. This is the rule the previous design broke, and it is the reason the arcs are discrete.
The ban is on encoding an unmeasured duration, not on length itself. During a cook the future is unmeasured, so the ring's legs are constant width. Afterwards it is not: a finished cook holds a recorded
completedAtfor every step, so a length between two of them would be a fact rather than a guess. The summary drew one for a while, on a horizontal clock rule; it went for reasons of its own (see SCREENS.md §4), not because it broke this rule — and the arrival sentence it left behind is that same subtraction, said in words. The test is not "is it a length" but "can the fold produce both ends of it".A line is laid out in the order the cook will travel it, so the colour only ever advances. The segments of a strand are its steps in
cookOrderOforder — the same schedule the list beside the map runs on — never the graph's, which is the order they finish in. Where the two disagree the fill lights up partway round the arc and then comes back for an earlier segment, and a map that jumps is worse than no map: it is confidently wrong about the one thing it exists to say. This is the complaint that shaped the whole screen ("the states jump around"), answered in round two for the list and, for a while, never for the map. Asserted by replaying a cook a step at a time and measuring every line's fill, inviews/example-maps.test.ts.No remaining-time estimates. We cannot know when a pot actually went on, so a step shows the recipe's own committed range or nothing at all. "8–13 min left" is a number nobody measured.
One ink per workstream, and it never means anything else. Five inks, and above five strands they cycle — the one place two strands share a colour. Every strand is named in words on its own line, so the ink was never the only thing telling them apart — by the shortest form still unmistakable in this recipe ("Bucatini", not "Drained bucatini"), the same rule the ingredient rows use. A lane's ink is its identity everywhere it appears: the arc, the bead, the card's left edge, the status word on a running step. Legs not yet travelled draw in the lane's own ink at reduced opacity, never anonymous grey: a strand you haven't started is still that strand.
A leg's state is its stroke. Solid is travelled; dashed with butt caps is in progress (round caps swell the dashes into blobs); quiet is still to come. A lane that waits on another begins at the angle it was freed at, with a dashed hop outward — never running backwards in time.
On a group cook a live bead wears its hand's initial, and the yellow says which one is yours. Every live bead, yours included — a letter on theirs in their arc's ink, yours on the yellow — and a finished bead stays a plain tick whoever made it. On a closed map another hand's arc is a hairline in the outline ink with no bead and no name: the shape stays, so the ring is still the recipe, and where they are is theirs to know. Owner rulings, 16 September 2026; the deal behind it is KITCHENS.md's.
Strands that meet, meet on the map. A strand the joined run consumes curves into it — an on-ramp tangent to both arcs — at the joining step that folds it in, and ends there; the joined run carries on as one line to the plate, a bead per joining step, wearing the neutral ink (it is every strand at once, so no single strand's) and named for what the join makes ("Cookie dough"), read from the graph. Its name sits on the stretch past the last merge, where the line is finally alone — among the arriving ramps it reads as the strand crossing it. Two arcs riding independently to the plate after their mixtures became one dough was the drawing being wrong about the kitchen.
The half circle is dealt by steps, so every bead on the line sits one gap from the next. The strands take the share of the sweep that their host strand's steps are of all the steps on that line (35% at least, 70% at most, so a name has an arc to ride and a run keeps room), and the run's steps are spread evenly from the first join to the plate. Owner, 19 October 2026: the chicken's thirteen-bead run had sat 14.5px apart, where the current step's ring touches its neighbour under 20, and is 21px now. It counts steps and reads no figure of time, so it is ordering and the rule above stands (
ring-sweep.ts). Past five strands the old flat 55% and packed folds stay until the lanes drawing redeals them.A strand keeps its own ink right up to the meeting. The neutral belongs to the joined line and starts at the join, not at the host's last step — handing over early painted the whole approach in it, and "why do the lines turn grey prior to joining?" is exactly what that looks like.
One step, one leg, and no step may paint two. A run-up to the meeting, an on-ramp and the arrival at the plate are drawn as the tail of the last leg there was, never as legs of their own. As legs they took the state of the step they ran into, so finishing one joining step filled a segment on every arc that fed it — "the last step completes two segments, and it should fill only the end one". Finishing a strand fills its arc to the meeting; the joined line lights only when the joining step is done. Counted rather than argued, per tap, in
ring-fill.test.tsand in the drive.No stream may cross another stream. They meet, at an end, and otherwise they are simply labelled. Two lines that cross have swapped places in the reader's eye, which costs the map the one thing it claims: that a cook can see which strand is which at a glance. Concentric arcs cannot cross, so this is a rule about the joins — arcs are dealt alternately outward and inward, closing on the middle, so the joined run rides mid-ring, the latest joiner (or one that never joins) sits outermost, and on each side of the line nearer means joins earlier. A ramp curving in therefore only ever passes radii whose strands have already merged and have no line left at that angle (
ring-order.ts). It is measured rather than eyeballed:path-crossings.tsintersects the rendered path data, at every strand count both maps draw. No gate could see this before —design:checkasserts no overlap of boxes, and two curved paths crossing overlap nothing box-shaped.As strands merge, the survivors close up — one ring per merge on their own side, all moving at the same angle, so every gap between live arcs stays exactly one pitch. Whatever joins next is then always one ring from the line rather than diving across the map. A strand whose own join follows straight on a freed ring does not land on it at all: the step fuses into its on-ramp, one curve from wherever it rides down to the line, so the freed space closes in smooth wedges rather than terraces (
ring-geometry.ts).When the strands outgrow the half, the ring squishes rather than clips. Past four strands the outermost circle is wider than half the map; the arcs become ellipses — full height, narrower — sized so every strand begins on the baseline, and the centre slides right by what the right half no longer needs: by there the strands have collapsed, so only the line still arriving at the plate claims room. Past five the map also grows a pitch a strand and squishes further, so the innermost arc keeps the radius it has at five and its name still has an arc to ride; six and seven hold every name whole, none over another and none misnamed (
ring-crowded.test.ts), and no shipped recipe has either. A map that fits keeps the exact circle it shipped with.A name follows its arc, and where it cannot, the drawing says so rather than hiding it. Two ways it cannot, one home for both (
ring-label-fit.ts). Where every position on the arc runs steeper than 45° the name is set flat, because a name rotated past that is unreadable at the glance it gets — and being unable to curve is priced into the search, so flat is a last resort and never a name lying horizontal among curved siblings. (Past five strands it is not even that: no name is laid flat, see the ring.) Where the name is longer than the arc it rides, one rule, in this order: ride further out, where the same arc is longer (the search prices a name with no room, so a clear spot inside a short arc loses to a grazed one outside it); else wear a shorter form of the strand's own words (shorterIngredientNames: "Fried whole spices" is "Whole spices"); only then cut with an ellipsis, because past the end of atextPathnothing is drawn at all and the alternative is a name silently losing its tail mid-word. The limit is the arc's own length, never a character count — it moves with the radius and with where the strand joins. No recipe squeezy ships reaches the ellipsis (asserted over the whole shelf inexample-maps.test.ts), nor does a stored share (a-strand-longer-than-its-arc.test.ts).A name is nearer the line it names than any other, and that is a floor rather than a preference. A name that sits closer to a neighbouring strand is read as that strand's, which breaks the map's one promise exactly as a crossing does. The label solver scores candidates against obstacles — a foreign bead, another name, a position too steep to curve along — and every one of those weights answers what is in the way while none answers whose name this is, so a spot clear of obstacles and closer to the neighbour won. A ground that fails ownership is now excluded, not priced (
label-ownership.ts).Measured on the ink, not the anchor, and that distinction is what makes the rule bite: text grows upward from its baseline, so a name lifted 14 above its own arc reaches ten further towards whatever is on the other side. At the old arc-to-arc pitch of 30 that left 10 of air below the name and 5.5 above it — every inner strand's name tucked under the arc outside it, which is what "why is the label for dressed pasta closer to the arc for veggies?" looks like. The pitch is therefore set by the names rather than by the lines: wide enough that a name at its first lift is nearer its own arc, asserted in
ring-order.test.tsso it cannot quietly be tightened back.A name keeps out from under the unit switcher. Cups and Grams stand over the cooking screen's top-left corner, where the outermost arc peaks and its name rides, so the solver prices a name there like a name over a name and looks between its usual positions before it settles for one that shares only the tap box's edge (
SWITCHER_CORNER,ring-frame.ts). The deck holds that no pill sits over a name.A job that feeds the join without riding a strand hangs off the ring: a mark on a hairline stalk just outside the line, never a segment of it. Grating the pecorino is on no strand's way to anywhere, and drawn along the line it claimed the dish had travelled that far — a black done-stripe across a sauce that had not met its pasta. A single bench step met after the first join is such a job, and hangs where it is used, not at the head: the oven by the roast, the yogurt by the spoon, the dill by the scatter, so the joined run begins at the first join. One met at the first join (the dry bowl, the marinade) is a beginning and keeps its arc. Its bead and its card wear the ink of the line the stalk hangs from: the host strand's before the first join, the joined run's green after it. A card's many-colour edge belongs only to the step where strands arrive, and carries only those that do (owner, 19 October 2026: "the line's colour").
The brief's ring shows the lines, not the steps. Its dormant ring draws arcs, joins and names with no beads at all — beads belong to the cooking screen, where each mark has a card to answer to. Label placement is identical in both, because the solver never looks at state.
Green now carries three meanings, and each gets its own shade.
--plateand--joinedare the map's green, and both mean arrival — the strands landed, not that anything is good — spanning only the lines that actually arrive, which after a join is the joined run and any strand nothing consumed. The third,--done-soft/--done-ink, is off the map entirely: an ingredient row's act, once tapped, answers in it (see SCREENS.md §13) and means recorded — a line landed on the shopping list, or a tweak was noted for next time — never arrival, and it never rides a lane or a bead.Yellow means live — either it is cooking without you or your hands are on it. Everything else is history or not yours yet. Live cards carry the yellow surface and the button; a future step carries no button at all, because offering an action you cannot take is the same lie as locking one. The one exception is a step the stove holds back, which can be done: it keeps the future card's surface and takes Done in the quiet control Undo wears, never the yellow.
Three statuses, and only three: Done, In progress, Todo. "Locked" and "not yet" are both banned — a step you cannot start yet is simply further down the list, which is what the order was already saying.
A step occupies identical space in every state. Height is a function of content, never of state, and the action slot is the same box whether or not it holds a button. The list must not move under a wet thumb. A mark on a card is a state, so a step carrying a note is the same box as one that is not — measured, because the way this breaks is invisible: two inline sizes on one baseline make a line box taller than either of them, and the first draft of the mark grew the card by 1.5px.
"At the same time" means genuinely simultaneous — everything cooking without you, plus the one job in your hands, and only where they already sit next to each other in the list's fixed order. Steps that merely share a graph depth are not simultaneous, and grouping them was the single most misleading thing the earlier design did. The bracket never re-sorts: the first version put passives first inside it, and that reordering is what made the list jump under a cook's thumb. "Set them going first" is carried by the statuses instead — a runnable pot is already In progress.
Prep is not a phase. It is a step at the head of the lane that needs it, carrying the ingredient in the form you buy it — a cup of diced tomatoes is not what you brought home.
No decorative animation. The wait breathes in opacity and a leaving thing fades (both below); a kitchen screen is glanced at, not watched.
The list is the schedule
The yellow is the next open attended line, and the glide after a completion only goes down.
From STATUS.md, The list is the schedule
Three statuses, and only three: Done, In progress, Todo. "Locked" and "not yet" are both banned — a step you cannot start yet is simply further down the list, which is what the order was already saying.
From DESIGN.md, The ring grammar
Height is a function of content, never of state, and the action slot is the same box whether or not it holds a button. The list must not move under a wet thumb.
From DESIGN.md, The ring grammar
The list is the schedule
STATUS.md, the whole section
cookOrderOf orders steps by when the plan would start them: earliest start under one pair of hands,
unattended first among ties, work nothing waits for behind work something does, longest lead next. The
yellow is the next open attended line, and the glide after a completion only goes down. ringShapeOf
lays lines out in the same order, so a colour fills forward only. The drive asserts every other card
keeps its size and position across a tap. SCREENS §3.
Never invent a number
A figure is the recipe’s, or it does not exist — no number here is ever widened, narrowed or rounded
From COOKING.md, What assumed means
No remaining-time estimates. We cannot know when a pot actually went on, so a step shows the recipe’s own committed range or nothing at all.
From DESIGN.md, The ring grammar
five untimed steps once collapsed the Amatriciana brief’s stated total from a checked 29 minutes to "about 8 min", a confident wrong figure in the one place a cook plans an evening from.
From COOKING.md, What assumed means
What assumed means
COOKING.md, the whole section
A DurationEstimate whose assumed flag is true carries a number that is squeezy's, not the
recipe's — the owner's amendment to "never invent a number" (CLAUDE.md), reached because the rule as
first written could not survive an importer. Every example here was hand-authored with a time in it, so
the gap never showed until a real page hit "cook until golden" — five untimed steps once collapsed the
Amatriciana brief's stated total from a checked 29 minutes to "about 8 min", a confident wrong figure in
the one place a cook plans an evening from.
A figure is the recipe's, or it does not exist — no number here is ever widened, narrowed or
rounded (the one reading that rounds is a card's sum of the steps, below; it never
touches a figure). What assumed marks is the one case that is not that: a step whose duration is a condition
("until the fat has rendered") with no figure in it at all, or whose condition the import left in
completionCondition with no duration (on a step the cook does not stand at: a hands-on step's window is
counted where the time is added up and stays out of the schedule). Rather than leave the schedule nothing to
place the step by, squeezy may supply a range, never a point — "20 to 30 min" says what it is, an
expectation with room in it, where "25 min" would claim a precision nobody has. assumedDurationRange
(libs/core/src/import/source-figures.ts) is the one place a range is built, from the step's own verb,
the food it names (and, where a row turns on how the food was bought, the ingredient lines the step is
made from) and the condition's own words ("until fragrant"); where the verb is not one it has evidence for, it supplies nothing, on the same
reasoning as an unparsed duration — an absence a cook can notice beats a number that looks like a good
one. It supplies nothing, too, where the condition hands the end of the wait to the step that wants its
result ("leave the chicken to marinate until needed"): that wait has no length of its own, and a guessed
half hour the schedule took for one held the korma chicken behind the blend. endsWhenWanted
(libs/core/src/import/ends-when-wanted.ts) is the table of those words, and a recipe that already
carries such a window is read as if it did not. IMPORT-PIPELINE.md owns where in
the pipeline this runs.
A step that handles each piece is the second thing assumed marks. The owner, 21 October 2026:
"some recipes like meatballs or chicken nuggets where each piece needs to be handled individually take
longer and thats not taken into account." Shaping 24 meatballs is not shaping 8, and a page states the
count and never the time. The technique table (libs/core/src/knowledge/techniques.ts) carries a range
of seconds for each piece an act handles (perPiece, the reason beside each row), and the step's range
is that rate times the count, assumed and a range like any other, with no sourceText, since the page
said nothing of the time and "into 24 balls" is not the step's time. The rate is squeezy's reckoning,
not a published figure, and the count is the recipe's: piecesHandled
(libs/core/src/import/pieces-handled.ts) reads it from the step's own words, then from the first food
the step names when its line is a whole number with no measure, then from the page's yield for a step
that handles several, and from nowhere else: no count is ever estimated from a weight or a size, a
figure under four or over 300 is not read, and with none stated the step keeps no duration, as before.
A fry or a sear multiplies its window by the number of turns only where the step states the pan's room
("in batches of 6"). A figure the recipe stated, a condition's own words, and the cook's own times
(estimateStepTime's you tier) all outrank it. A recipe stored before this keeps its shape until it
is read again. IMPORT-PIPELINE.md owns where in the pipeline this runs.
Three things an assumed range must never do, all consequences of the same fact — it is a guess, not a claim:
- It never replaces the condition.
typestaysuntil-conditionandsourceTextstays the source's exact words; the range isminimumMs/maximumMsadded beside them, and no screen may show the range in the treatment it gives a figure the recipe actually stated. - It never survives a real figure.
estimateStepTime'syoutier — the cook's own history — already outranks anything published, assumed or not; the moment a dish has been cooked once, the guess is gone from the schedule that matters. - It never happens where the recipe committed to a number, or to nothing at all: an active step
with no condition (
op_serve) gets no invented wait, the same "an absent duration is still better than a bad one" reasoning the rest of this file uses for every other unparsed field. The one exception is the import's range for hands-on work (knife and hand work the page never times), which is the model's judgement of what the step is done to and only a range (IMPORT-PIPELINE.md); a step that leaves nothing is still given none.
estimateStepTime answers "how long does this take" from the most specific source that has an answer,
and always says which one it used:
- you — the median of your kitchen's past cooks of that step. "You" is the household, not the member: whose the evenings are is settled once, in Kitchens
- everyone — the median across all cooks (the function takes it and no caller supplies one, left absent rather than faked)
- recipe — what the author claimed
Median rather than mean, so one interrupted cook doesn't move the number much. A step is timed by the gap since the last step you finished, so timing costs the cook no taps; an explicit start/stop wins whenever one exists.
The you tier is read on the cooking screen only, behind Details — "You took 4 min last time",
information rather than instruction. It does not plan the dish. The brief once re-planned the
whole recipe from these medians the moment you cooked it once, so a single slow evening silently
retimed every step; the ask was "way more chill", and what a cook's own history changes now is
one opted-into total (below).
estimateStepTime itself reads no version at all — it trusts the id. gatherYourDurations keys
purely on OperationNode.id, so whether a duration measured last month still answers for a step
today rests entirely on whether that id still means the same job — see
What an operation id promises. Today's only caller narrows
the evidence it hands in to sessions of the same recipe version (useCookHistory's evidenceFrom,
apps/web/src/hooks/cook-evidence.ts) as a belt-and-braces policy on top, not because the function
needs it.
A range is not a number
squeezy may supply a range, never a point — "20 to 30 min" says what it is, an expectation with room in it, where "25 min" would claim a precision nobody has.
From COOKING.md, What assumed means