Cookpit v3.2 — Governing Rules
The concise, numbered ruleset the AI Chef must obey when generating v3.2 JSON-LD cooking files. Each rule is independently checkable. The validator in
validation.mdreferences these rule numbers verbatim.The AI Chef writing the file is a rebel chef detective: a confident working chef who reads the source recipe as a body of evidence and deduces the optimal schedule that satisfies it. The persona and stance are defined in
bundle/v3.2/prompt.mdandbundle/v3.2/lexicon.md §0. The rules below are the constraints that deduction must satisfy.
A. The three central principles
A1. Optimal. Every task time is the optimal moment an expert chef would commit to that action so the dish reaches its proper outcome. Times are factual culinary commitments deduced from the source's evidence, never random, cosmetic, or evenly spaced. The plan does not transcribe the source method line by line; it deduces the schedule the source implies.
A2. Closed. The plan is bounded by the resources declared in this file.
Every reference must resolve to a declared resource of the matching type.
The plan does not depend on tools, ingredients or sundries that are not
declared. Equipment is declared at the heat-level abstraction the cook
commits to, not at the heat-source provenance: hob is a hob, regardless
of whether it is gas, electric, induction, charcoal, wood-fire or a
volcano.
A3. Static. The plan does not adapt at runtime. The Chef app manages time and progress against the plan; the plan itself remains unchanged. Do not soften, pad, or stretch timings to make them more achievable, and do not insert filler tasks to fill quiet minutes between deduced commitments.
A0. The four lifecycle stages
A v3.2 cooking file passes through four stages, each with a single responsible actor and a single integrity question. The stages are strictly sequential; nothing in v3.2 mutates a file once a downstream stage has consumed it.
[source recipe] ── (1) generation ──▶ [U file] ── (2) validation ──▶ [verdict]
│
├── pass ── (3) attestation ──▶ [A file] ── (4) consumption ──▶ chef app runs
└── fail ──▶ report ──▶ user repairs ──▶ resubmit at stage 2
A0.1. Stage 1 — Generation. The AI Chef reads the source recipe and
emits a v3.2 file. The file MUST carry a cookpit.quantitativeFingerprint
that records the source's active-number sequence (section K), and a
cookpit.attestation block whose status is unauthenticated (section
R). The filename uses the U flag (section O). The file at this stage is
the AI's claim that it has faithfully transcribed the source. It has not
yet been confirmed by anyone.
A0.2. Stage 2 — Validation. The validator runs the candidate file
through every hard criterion in validation.md, including
source-faithfulness checks against the source recipe (V-FINGERPRINT-B,
V-SOURCE-COVERAGE, V-SOURCE-TEMPS, V-METHOD-ORDER). The validator never
mutates the file. The output is a verdict: pass or fail. A failed file
goes back to the user (or the AI repair loop) and may be resubmitted.
A passed file proceeds to stage 3.
A0.3. Stage 3 — Attestation. The validator stamps the passed file. It
canonicalises the file body with cookpit.attestation.signature cleared,
computes the SHA-256 file fingerprint, signs the canonical payload with
its private key, replaces the cookpit.attestation block with the
authenticated form (section R), and renames the file to use the A flag
(section O). Only the canonical validator may issue an A file.
A0.4. Stage 4 — Consumption. A downstream consumer (the Chef app, a
recipe library, an integrity audit) loads the file and verifies it. The
consumer trusts only the cryptographic binding: parse, schema-conform,
re-canonicalise, recompute the file fingerprint, verify the signature
against the published public key, optionally check a revocation list,
optionally enforce a minimum validatorVersion. The consumer does NOT
re-run source-faithfulness checks (it does not have the source recipe);
those were settled at stage 2 and certified at stage 3.
A0.5. Single actor per stage. The AI is the actor at stage 1; the validator at stages 2 and 3; the consumer at stage 4. The validator is the only actor that touches both the source-faithfulness check (stage 2) and the file-content certification (stage 3) — it is the handoff point between the two integrity questions.
A0.6. Two distinct fingerprints, two distinct stages.
| Fingerprint | Hashes what | Stage | Computed by | Verified by |
|---|---|---|---|---|
Source fingerprint (cookpit.quantitativeFingerprint) | The source recipe's active-number sequence (section K) | 1 → 2 | AI at stage 1; validator re-checks at stage 2 | Validator (V-FINGERPRINT-B) |
File fingerprint (cookpit.attestation.fileFingerprint) | The canonicalised file body with signature cleared (section R) | 3 → 4 | Validator at stage 3 | Consumer (V-FILE-FINGERPRINT, V-SIGNATURE) |
The source fingerprint catches AI errors of fidelity to the source. The file fingerprint catches post-attestation tampering with the cooking file. Each detects a class of failure the other cannot. They are not redundant.
A0.7. Filename flag is decorative. The filename's A/U flag
(section O) is a human-readable cue. It is NOT a security signal.
Consumers MUST verify trust through cookpit.attestation.status, the
file fingerprint, and the signature — never through the filename alone.
Filename / internal-status disagreement is a hard validation failure
(V-ATTESTATION-CONSISTENCY).
B. File identity
B1. The top-level @type includes both Recipe and cookpit:CookingFile.
B2. The cookpit.version is exactly 3.2.0.
B3. The $schema URL identifies the v3.2 cooking file schema.
B4. The cookpit.id is type-prefixed with f (file) and matches the global
ID pattern (see G1).
B5. cookpit.courses is a non-empty array drawn from starter, main,
dessert, with no duplicates and at most three entries.
B6. cookpit.difficulty is exactly one of easy, medium, hard, expert.
C. Source timing and phase durations
C1. cookpit.sourceTiming preserves the source recipe's timing fields:
prepTime, cookTime, totalTime when the source uses ISO 8601 durations,
or prepTimeText / cookTimeText when the source uses prose or ranges.
Source facts are recorded as the source states them.
C2. Phase durations. Each declared phase carries its own
nominalDuration (HH:MM:SS) in the phase block (cookpit.prepCook .nominalDuration, cookpit.preCook.nominalDuration,
cookpit.liveCook.nominalDuration). The liveCook duration is
derived from the source's cook time the source assigns to the
final-assembly window — never the source's total time. The
prepCook and preCook durations are derived from the source's
explicit timed prep / pre-cook windows. There is no top-level
nominalCookDuration in v3.2; references throughout the rules to
"the cook duration" mean the duration of the phase the rule speaks
about.
C3. Range resolution. When the source's cook time or a method-body duration is a range, the canonical resolution is determined by the following priority order. The same rules apply within any phase (prepCook, preCook, liveCook):
C3.1. Method-body specificity wins over header-range estimates. When
the source header gives a wide range (e.g. Cook: 10 to 30 mins) and
the method body specifies a precise duration (Bake for 15 minutes),
the method-body value drives the relevant phase's nominalDuration
and the timingBasis of any task using it is sourceExactDuration.
The header range is preserved verbatim in
cookpit.sourceTiming.cookTimeText.
C3.2. Single range, no competing total: range minimum. A method-body
range like simmer for 20-25 minutes with no contradicting source
total resolves to range minimum. The task uses
timingBasis.basis: "sourceRangeMinimum".
C3.3. Two parallel ranges + a competing total: range maximum. When
two long parallel processes share a window WITHIN A PHASE and the
source states a total time for that window, taking the minimum of
each range may undershoot the source's total. In this case, BOTH
parallel ranges resolve to range MAXIMUM to align with the source's
stated total. The pork-fillet-braised-cheeks-and-pork-belly file is
the canonical case: in the preCook phase, source 6-8 hours cheek
braise + 7-8 hours confit run concurrently, with source-stated
preCook total 8 hours. Range minima would undershoot; range maxima
align. Both tasks use sourceRangeMinimum basis with the source
phrase preserved in timingBasis.source; the rationale for the
range-maximum departure is documented in a prerequisites.notes[]
item.
C3.4. Source explicitly invites the upper bound: range target. When
the source phrases the range as a quality-driven choice (bake until golden, 10-12 minutes; the longer the bake the deeper the colour),
the task uses timingBasis.basis: "sourceRangeTarget" and resolves
to the target value the canonical profile names — typically the
upper bound for outcome quality.
C3.5. Open lower bound (Over X hours, At least X minutes):
compositional sum. When the source's cook time is an open lower
bound rather than a closed range, the chef-detective derives the
relevant phase's nominalDuration by summing method-body explicit
durations plus canonicalProcessEstimate fills for any source step
without a stated duration. The chosen value MUST satisfy the open
lower bound. The pork-fillet-braised-cheeks-and-pork-belly file is
again the canonical case: the source's Cook: Over 2 hours is the
composition of an 8-hour preCook plus a ~1 h 45 min liveCook.
In all cases, the chosen value is recorded in the phase block's
nominalDuration and the source range or open bound is preserved
verbatim in cookpit.sourceTiming.cookTimeText.
C4. cookpit.orchestration.timingBasis is cookTime. prepHandling is
preStartChecklist. runtimeOverruns is appOwned.
C5. Residual end-buffer is permitted within any phase. When the source's stated duration for a phase exceeds the deduced sequence duration of that phase's tasks, the phase MAY carry a residual buffer between the last task and the phase's time-up alarm. Filling the buffer with invented tasks (per A3 "do not pad or stretch timings") is forbidden. Residual buffers are honest representations of sources that overstate duration relative to actual sequence duration; the active corpus shows a 30-second buffer in boeuf bourguignon as the canonical example.
D. Lane model
D1. The cookpit.laneModel block is the fixed primary/secondary/tertiary
course lanes block as published in the v3.2 spec, used verbatim.
D2. A0 is the global alarm lane and is used only for global alarms,
klaxons, warnings and whole-session milestones (start, remaining-time
warnings, time-up, major transitions).
D3. Course-scoped lanes carry only that course's prompts:
S1/S2/S3 carry only starter prompts, M1/M2/M3 carry only main prompts,
D1/D2/D3 carry only dessert prompts.
D4. Lanes are intra-minute publication slots. A course's three lanes
(S1/S2/S3, M1/M2/M3, D1/D2/D3) provide three slots within every
minute, spaced five seconds apart. Use them in either of two valid ways:
(a) parallel workstreams — the primary lane carries the main workstream
of the course, secondary and tertiary lanes carry simultaneous workstreams
that run alongside it; or (b) tight intra-minute sequences — when the
source crowds a moment with multiple sub-actions in a single minute, the
secondary and tertiary lanes carry those sub-actions in their source order
five seconds apart. Either use is conformant. Filler use to spread tasks
across minutes is not.
D5. The seconds component of every task time matches its lane:
A0=:00, S1=:15, S2=:20, S3=:25, M1=:30, M2=:35, M3=:40,
D1=:45, D2=:50, D3=:55.
E. Required global alarms (per phase)
The alarm rules below apply independently to each declared phase
(prepCook, preCook, liveCook). Each phase has its own clock that
runs from 00:00:00 to that phase's nominalDuration; alarms are
mechanical features of that local clock. The Chef app starts a fresh
A0 timer for each phase the file declares.
E1. Each phase's tasks[] contains exactly one start-cooking alarm on
A0 at 00:00:00.
E2. Each phase's tasks[] contains exactly one time-up alarm on A0
at the value of that phase's nominalDuration. For liveCook this
alarm is the serve cue; for prepCook and preCook it is the
phase-complete cue that hands off to the next phase.
E3. When a phase's nominalDuration is at least 10 minutes, that
phase's tasks[] contains a 10-minutes-remaining alarm on A0 at
nominalDuration − 00:10:00.
E4. A 5-minutes-remaining alarm on A0 at nominalDuration − 00:05:00
is included within a phase when it is useful for that phase; if
included it must be on A0 and at the correct minute.
E5. Standard global alarms have kind: "alarm" and do not require a
timingBasis; they are derived mechanically from the phase's
nominalDuration.
F. Prep, prerequisites and the three phases
F1. File-level prerequisites are the entry checklist. Recipe prep
that the user confirms once before any phase begins lives in
cookpit.prerequisites. This is the file's only checklist — there
are no per-phase prerequisite blocks. Static prep that satisfies its
deadline trivially (chopping onions ahead, bruising garlic, grating
cheese, beating eggs, dissolving stock) is the default home for prep.
Items that need to be done in advance carry a leadTime ISO 8601
duration (P1D for overnight marination, PT3H for cooling stock).
F2. Promote prep to a live timed task on a course lane only when it must run live. Live placement is required when one of the following holds: the action must happen inside a phase's cook window for quality, freshness, kitchen-flow or temperature reasons (deglazing, finishing herbs into a sauce, mounting butter at the end, slicing meat off the bone before serving, tempering chocolate, melting butter into a hot pan); the source explicitly schedules it inside a phase with a "while X cooks" or "meanwhile" cue; or its outcome state cannot be held without quality loss until the live moment of use. Otherwise, prefer prerequisites.
F3. Phase selection is by source evidence, not authoring preference. A timed phase block is declared only when the source establishes a discrete timed window:
| Phase | Declare when the source describes |
|---|---|
prepCook | A timed active prep window with stated duration — pressing meat under weights, salt-curing, marinating with active monitoring, mise that genuinely takes long enough to warrant a runtime timer. |
preCook | Cooking of mainstay components ahead of final assembly — long braises whose product becomes part of the final dish, bake-then-cool meringue bases, poach-and-shred salmon for a pâté. |
liveCook | The final-assembly cook ending in serving. Always declared. |
A file with no source evidence for prepCook or preCook declares only
liveCook. The vast majority of recipes are liveCook-only.
F4. Phase activation. allPrerequisitesConfirmed enables BOTH
cookpit.prepCook (if declared) and cookpit.preCook (if
declared) at the same moment — the Chef app starts each declared
phase's A0 timer when the user confirms file-level prerequisites.
prepCook and preCook may run concurrently or sequentially; the
file does not encode that runtime choice. cookpit.liveCook
becomes available only when the LAST of {prepCook, preCook}
fires its A0 time-up alarm — liveCook never overlaps prepCook
or preCook. The file does not encode runtime checkbox state.
F5. Live prep stays live. The presence of a cookpit.prepCook
block does NOT relocate prep out of cookpit.liveCook. Prep that
F2 places inside the live cook window — deglazing, finishing herbs
into a sauce, mounting butter at the end, slicing meat off the
bone before plating, tempering chocolate, melting butter into a
hot pan, "while X cooks" / "meanwhile" actions, prep whose outcome
state cannot be held without quality loss — remains in liveCook.
prepCook is the home only for source-stated active timed prep
windows that happen BEFORE the cook day (presses, salt-cures,
active-monitored marinades). F2 is the sole authority on whether
prep is live or pre-Start; the three-phase model does not weaken
it.
G. Type-prefixed deterministic IDs
G1. Every entity ID matches the global pattern ^[a-z][0-9a-f]{10}$: a single
lowercase type letter followed by 10 lowercase hex characters.
G2. Type prefixes:
| Entity | Prefix |
|---|---|
cooking file / liveCook (cookpit.id) | f |
| ingredient | i |
| equipment | e |
| utensil | u |
| sundry | s |
| prerequisite item (any group) | q |
| process | p |
| task | t |
| hotspot | h |
prepCook phase (cookpit.prepCook.id) | y |
preCook phase (cookpit.preCook.id) | z |
G3. IDs are deterministic, not random. The required derivation is:
<typePrefix> + first 10 hex of SHA-256("v3.2|" + entityType + "|" + canonicalContent + "|" + canonicalPosition)
The canonical generation profile (cookpit-ai-canonical-v3.2) defines
the exact entityType, canonicalContent and canonicalPosition
strings per entity type, with self-test vectors. See
bundle/v3.2/canonical-id-derivation.md
for the full profile.
G4. IDs are unique within the file.
G5. Cross-references match by exact string. A reference whose prefix does not match the referenced entity's type is a hard validation failure.
H. Resource closure
H1. Every ingredientRefs id exists in cookpit.ingredients[].id and starts
with i.
H2. Every equipmentRefs id exists in cookpit.equipment[].id and starts
with e.
H3. Every utensilRefs id exists in cookpit.utensils[].id and starts with
u.
H4. Every sundryRefs id exists in cookpit.sundries[].id and starts with
s.
H5. Every processRefs id on a task exists in the processes[] array
of the SAME phase that owns the task and starts with p. Cross-phase
process references are forbidden — each phase has its own roster.
H6. Process startTask and endTask ids exist in the tasks[] array
of the SAME phase that owns the process and start with t.
H7. Hotspot taskRefs ids exist in the tasks[] array of any
declared phase and start with t. Hotspots are file-level metadata
and may target tasks in any phase.
H8. Task action text does not depend on equipment, utensils, ingredients or
sundries that are absent from the declared resource lists.
H9. Every declared resource is referenced by at least one task, process or prerequisite. (Soft warning only: a recipe may legitimately list a "to serve" item that is not in the timed plan.)
I. Tasks
I1. tasks is a non-empty array. Each task has id, time, kind,
action. Tasks whose kind is not alarm additionally have timingBasis.
I2. kind is one of alarm, alert, update. alarm is reserved for the
A0 lane. alert and update are placed on course lanes.
I3. time matches ^([0-9]{2}):([0-9]{2}):([0-9]{2})\.(A0|S[1-3]|M[1-3]|D[1-3])$.
I4. The seconds component of time matches the trailing lane label per D5.
I5. The hours/minutes component of every task time is at most the
nominalDuration of the phase that owns the task. No task is
scheduled past its phase's time-up.
I6. Tasks are ordered by time. When two tasks share the same HH:MM:SS
component, ordering is time then lane then id.
I7. action text is culinary and direct. UI verbs (tap, swipe,
confirm, press, done) are forbidden in action.
I8. For every non-alarm task, timingBasis records the kind of evidence
the chef-detective used to choose the time. basis is one of:
| basis | use it when |
|---|---|
sourceExactDuration | the source states a single fixed duration ("for 10 minutes"). |
sourceRangeMinimum | the source states a range; you took the canonical minimum ("25–30 minutes" → 25). |
sourceRangeTarget | the source states a range; the canonical generation profile selected the target rather than the minimum. |
sourceCookTimeEndpoint | the time is derived mechanically from the phase's nominalDuration (start, time-up, remaining-time alarm). |
sourceOrder | the source places this action at a specific point in the narrative order with no explicit duration; the time records that order. |
sourceMeanwhile | the source explicitly schedules this action inside another action's window ("while the spaghetti is cooking…"). |
sourceOutcomeCue | the time is set by an outcome cue rather than a clock ("until deep golden", "until juices run clear"). |
sourceImpliedDeadline | the source gives no time for the prep itself but states a downstream consumer ("add the chopped onion"); the prep is placed at deadline − prep duration. Record the consumer task in offsetFrom and the prep duration in offset (negative ISO 8601, e.g. -PT2M). |
canonicalProcessEstimate | the source is silent on duration and there is no downstream deadline; the time reflects competent professional practice for that action and outcome, with a one-line professional rationale in source. |
The source field is the specific source line whose evidence justifies
the chosen time — not the source line that happens to mention the action.
I9. No task time is random, cosmetic or used as filler. Every non-alarm
task time has a defensible derivation in timingBasis.
J. Processes and outcome cues
J1. Each process has id (p…), label, course, startTask,
endTask, duration and completion. Processes live INSIDE a phase
block: cookpit.<phase>.processes[]. startTask and endTask
reference task ids that exist in the SAME phase's tasks[].
J2. The interval between startTask.time and endTask.time matches
the process's duration.target to within rounding allowed by the
lane model. Both endpoints are in the same phase clock, so the
interval is straightforward subtraction.
J3. completion uses one of the spec's allowed types (timed,
sensory, temperature, compound). Completion cues describe
outcome, not runtime state.
J4. Within a phase, processes are listed in the order their
startTask.time occurs.
K. Source faithfulness and the strict quantitative fingerprint
K1. Every numeric fact in the source recipe (ingredient quantities,
durations, temperatures, gas marks, ranges) appears either in
cookpit.ingredients or in the tasks / processes plan, with the same
value(s) the source states.
K2. No quantity, duration, temperature or range is silently changed, rounded or omitted. Range minima may be selected per C3 but the original range is preserved in source-text fields.
K3. cookpit.quantitativeFingerprint is present, of type: "strict", with
basis: "ingredients-and-method-active-numbers" and
normalization: "cookpit-active-number-sequence-v3.2.0".
K4. quantitativeFingerprint.sequence is the dash-separated sequence of
active numbers extracted from the source ingredient lines and method, in
source order, normalised per the canonical generation profile
(cookpit-active-number-sequence-v3.2.0), matching
^[0-9]+(-[0-9]+)*$. See
bundle/v3.2/canonical-fingerprint-normalisation.md
for the full tokenisation rules and worked examples.
K5. quantitativeFingerprint.hash.algorithm is sha256.
quantitativeFingerprint.hash.value is the lowercase 64-hex SHA-256 of the
sequence string. The hash is the file's identifying key for its source
numeric skeleton.
L. Forbidden runtime fields
L1. The file does not author any runtime state. Forbidden anywhere in the
file: urgency, priority, progress, completionState, checkboxState,
actualStartTime, actualFinishTime, overdue, overdueState,
focusTask, bannerColor, timerColor, overrunState, queuePosition,
scrollPosition, and any field whose name implies runtime mutation or
visual UI state.
L2. The file does not contain dynamically extended times or any timing field that varies based on user pace.
M. Generation metadata
M1. cookpit.generation.profile identifies the canonical generation profile
(default: cookpit-ai-canonical-v3.2).
M2. cookpit.generation.idPolicy is
deterministic-type-prefixed-10-hex.
M3. cookpit.generation.timingPolicy is
source-derived-deterministic-optimal.
M4. cookpit.generation.resourcePolicy is
closed-world-declared-resources.
M5. cookpit.generation.randomTimingAllowed is false.
N. Harvested fields
N1. Optional ingredients. An ingredient may carry optional: true for
"to serve" / decorative items. Optional ingredients are not required to be
referenced by a task or process.
N2. Equipment power and notes. Equipment items may carry an optional
power (electric, gas, induction, none) and a free-text notes
field. The chef-AI may use power to make timing decisions where the source
is silent on heat-up time.
N3. Per-task sound override. Tasks may carry an optional sound chosen
from bell, klaxon, chime, tick. When omitted, the lane's default
sound applies. Sound is not a substitute for severity.
N4. Prerequisite leadTime. Prerequisite items may carry an optional
leadTime ISO 8601 duration for make-ahead steps (e.g. P1D for overnight
marination). The live timer still starts at 00:00:00; leadTime is
informational metadata for the Chef app's pre-cooking reminders.
N5. Prerequisite resource refs. Prerequisite items may link to the
resources they concern via ingredientRefs (each i…),
equipmentRefs (each e…), utensilRefs (each u…) and
sundryRefs (each s…). All targets must exist in the corresponding
cookpit.<group>[].id (closure rule applies). The validator's
V-REFS-COVERAGE soft check recognises each as a legitimate consumer
of its target resource — so a knife declared in cookpit.utensils[]
and referenced from a prereq item's utensilRefs is no longer
flagged as "unreferenced".
N6. Source-stated alternatives. Ingredients may carry an alternative
sub-object preserving a substitute the source recipe explicitly offers
as a primary-plus-fallback (e.g. "vanilla pod, or vanilla extract").
The plan is still authored against the primary; alternative is
metadata only.
N7. Source-stated equivalents (choices[]). When the source recipe
offers a set of EQUALLY VALID same-role options ("cod, haddock or
pollock"; "pearl onions OR 24 baby onions"; "chicken or pork stock";
"deep fryer or deep saucepan"), the ingredient (or equipment) carries
a choices[] array. Each choice records its own quantity+unit pair.
The chef picks one. Distinct from alternative, which encodes a
single fallback. Use choices[] when none of the listed options is
"primary" — they are equivalents.
N8. Ingredient splits (splits[]). When a single declared
ingredient is partitioned across multiple tasks at distinct fractions
(e.g. carbonara's "most of the cheese for the egg-mix, a small handful
for the topping"; boeuf's "half the butter for cooking, half for
finishing"), the parent ingredient carries a
splits[] array. Each split has its own i… id and a fraction
(0 < f ≤ 1). Tasks reference the split's id like any other ingredient
ref; the closure rule treats each split as a valid usage of the
parent. The Chef app uses split metadata to show portion-aware
pre-cook prep and run-time tracking.
O. Filename and media-type methodology
O1. The filename pattern is <slug>.<schema-version>.<dialect>.<status>.jsonld,
where <status> is the single-character lifecycle flag A
(authenticated — the file has been stamped by the canonical validator) or
U (unauthenticated — the file has not been stamped). The flag is a
human-readable cue; it is NOT a security signal (see A0.7 and section R).
O1.1. AI Chef output (stage 1) MUST use <status> = U and MUST embed
cookpit.attestation.status: "unauthenticated".
O1.2. Validator output (stage 3) flips the filename's <status> to A
when (and only when) every hard criterion in validation.md passes and
the validator embeds an authenticated cookpit.attestation block per
section R. The filename flag and the internal cookpit.attestation.status
MUST agree at every stage (V-ATTESTATION-CONSISTENCY).
O2. The slug is derived deterministically from the recipe name by Unicode
NFKD normalization, dropping combining marks, lowercasing, replacing any
non-[a-z0-9] character with the chosen separator (either - or _),
collapsing repeats, trimming, and truncating to 80 characters at a
separator boundary. The chosen separator is project-wide and applied
consistently; mixing - and _ within a single filename is forbidden.
The existing in-repo convention is _; new projects may elect -.
See canonical-patterns.md §6.
O3. <schema-version> is the major-minor schema version: v3.2.
O4. <dialect> is cpt (Cookpit).
O5. The file extension is always .jsonld (W3C-recognised).
O6. When served over HTTP, the file uses
Content-Type: application/ld+json; profile="https://cookpit.org/spec/v3.2".
O7. Examples.
spaghetti_carbonara.v3.2.cpt.U.jsonld— AI Chef output, before validation.spaghetti_carbonara.v3.2.cpt.A.jsonld— same file after stage-3 attestation.
O8. Backward-compatibility note. Files predating this rule revision
that lack the <status> segment (e.g. the in-repo corpus's
spaghetti_carbonara.v3.2.cpt.jsonld) are treated as U for stage-4
consumer purposes. New tooling SHOULD rename such files to add the
explicit U flag at the next opportunity. The flag is required for any
file that has been put through the validator; an A file MUST carry the
flag.
P. Lexicon and chef voice
The chef-language and terminology guide is published alongside this rules
list as bundle/v3.2/lexicon.md. The lexicon defines the persona, voice,
verb taxonomy, heat language, sensory vocabulary, forbidden terms,
allowed informalisms, source-faithful exceptions and regional defaults.
P1. Persona. Every active cooking instruction is written in the rebel
chef voice defined in lexicon.md §0: confident, easy-going, concise,
plain English by default, specialist terms only when they earn their
place. The persona is uniform across every v3.2 file generated from this
bundle.
P2. Scope. The lexicon applies to every field that carries an active
cooking instruction (tasks[].action, tasks[].completion.cue,
processes[].label, processes[].completion.cue, and prerequisite items
whose text describes a cooking action). The lexicon's tone applies more
lightly to skills, hotspots, notes and equipment notes; it does not apply
to declarative naming of ingredients, equipment, utensils or sundries.
P3. Imperative form. Active cooking instructions are imperative,
present-tense, second-person pronouns omitted. Sentence fragments are
allowed (Off heat. Tent it. Rest ten.); one-word commands are not the
goal.
P4. Verb selection. Cooking verbs are chosen per lexicon.md §3 with
their kitchen-precise meaning. Specialist terms (brunoise, chiffonade,
mantecare, tadka) are used only when the source recipe uses them or
plain English loses precision.
P5. Heat language. Heat words follow the calibrated meanings in
lexicon.md §4. Source temperatures, gas marks and °F are preserved in
timingBasis.source and rendered as °C in canonical action text.
P6. Time language. Active task time is exact (5 minutes,
20 minutes). "A little while", "a few minutes", "a moment" are
forbidden in active instructions. Adverbs of manner (quickly, gently,
patiently) are allowed but never as substitutes for a number.
P7. Sensory completion cues. Every completion.cue carries at least
one sensory token from lexicon.md §6 (or a clear synonym).
completion.type: timed is permitted without a sensory cue; all other
completion types must have sensory specificity.
P8. Forbidden language. Active cooking instructions never contain
hedgers (a little, as desired, you'll want to, make sure to),
recipe-blog warmth (lovely, perfect, delicious, amazing,
wonderfully, beautifully), filler (now, go ahead and, simply),
or vague outcomes without a sensory companion (until done, until cooked through).
P9. Allowed informalisms. Working-chef phrasings listed in
lexicon.md §7.1 are encouraged where they fit (low and slow, off heat, back off, tip in, pull from heat, rest ten, no colour,
don't crowd the pan, scatter, tent it).
P10. Source-faithful exceptions. When the source recipe uses
culinarily precise specialist terms (per lexicon.md §8), preserve them.
When the source already reads in canonical chef voice, do not paraphrase.
P11. Process-label grammar. A processes[].label is a short
present-continuous noun phrase (Reducing the sauce, Resting the meat),
not an imperative.
P12. Regional default. UK English by default; source-region usage preserved when the source is regionally specific.
Q. Phase blocks (prepCook, preCook, liveCook)
The three-phase model is v3.2's mechanism for honestly representing recipes that contain timed prep windows, pre-cooked mainstay components, or both, before final assembly. The rules below constrain how phases compose. Per-phase content rules (alarms, tasks, processes, lanes) are inherited from sections E, I, J, D respectively.
Q1. liveCook is required; prepCook and preCook are optional.
cookpit.liveCook is always present. cookpit.prepCook and
cookpit.preCook are declared only when the source establishes a
discrete timed window for them (per F3). A liveCook-only file is
the canonical default; the active corpus shows varying phase
counts (carbonara, goulash, boeuf and roast chicken with cider and
sage: liveCook only; pork-fillet-braised-cheeks-and-pork-belly:
prepCook + preCook + liveCook).
Q2. Phase identity. cookpit.prepCook.id is type-prefixed y…,
cookpit.preCook.id is type-prefixed z…. cookpit.liveCook has
no id field — its identity is the file's cookpit.id (f…).
Phase ids are deterministic per canonical-id-derivation.md.
Q3. Phase shape. Each declared phase carries label,
nominalDuration, tasks[], optional processes[] and optional
completion. The phase's tasks[] and processes[] are
self-contained — every task and every process in the phase lives
within that phase's local clock (00:00:00 to nominalDuration).
Q4. Phase ordering. Two distinct ordering relations govern the three phases at runtime:
prepCook⊥preCook(independent). When both are declared, they may run concurrently or sequentially; the chef chooses at runtime. The file does not encode that choice. Each carries its ownA0timer; either may finish first without unblockingliveCook.liveCook≻prepCookandliveCook≻preCook(strictly downstream).liveCookis unblocked only when the LAST of the declared upstream phases has fired itsA0time-up alarm.liveCooknever overlapsprepCookorpreCook.
The JSON document order in cookpit is fixed (prepCook,
preCook, liveCook) regardless of runtime choice — this is a
human-readability convention, not a runtime sequencing claim. The
Chef app's UI offers the chef the option to start prepCook and
preCook in parallel or in series.
Q5. No cross-phase task or process references. A task's
processRefs[] may name only processes declared in the SAME phase.
A process's startTask/endTask may name only tasks declared in
the SAME phase. Cross-phase continuity is encoded structurally by
phase ordering (Q4), not by reference.
Q6. File-level prerequisites cover the whole file.
cookpit.prerequisites is confirmed once before the FIRST declared
phase begins. There is no per-phase prerequisites block. Items
needed only for a later phase still live in
cookpit.prerequisites and may carry a leadTime if make-ahead is
required.
Q7. File-level resources cover the whole file.
cookpit.ingredients[], cookpit.equipment[], cookpit.utensils[]
and cookpit.sundries[] are the file-wide closed-world resource
lists. Every phase's tasks and processes resolve their refs against
these lists. The closure rules in section H apply across phases.
Q8. Phase completion cue. The optional cookpit.<phase>.completion
records the sensory cue that signals the phase is done. liveCook's
completion cue is the dish-on-plate cue. preCook's is the
cooked-component cue ("cheeks tender to a knife tip; meringue dry
on top"). prepCook's is the prep-done cue ("belly compressed to
half its starting depth").
Q9. Phase fingerprint scope. cookpit.quantitativeFingerprint
remains file-scoped: it covers all numbers from all phases plus
ingredients, in the source order they appear in the source recipe.
Per-phase fingerprints are not declared.
R. Attestation and lifecycle integrity
Section R defines the cookpit.attestation block and the cryptographic
binding that allows a stage-4 consumer to verify a file was stamped by
the canonical validator and has not been altered since. The block is
authored by the AI in its unauthenticated form at stage 1 and replaced
by the validator in its authenticated form at stage 3.
R1. Block presence. Every v3.2 file carries cookpit.attestation,
regardless of stage. AI Chef output (stage 1) MUST emit it with
status: "unauthenticated". The validator's stamping operation
(stage 3) replaces it with the authenticated form.
R2. Unauthenticated form (stage 1, stage 2 input). The block has a single required field:
status: "unauthenticated".
It MAY carry advisory selfReported data (rulesSelfChecked,
validatorRun, etc.). It MUST NOT carry a signature,
fileFingerprint or keyId. A stage-1 file claiming any of these
fields is malformed and the validator rejects it (V-ATTESTATION-SHAPE).
R3. Authenticated form (stage 3 output). The block has the following required fields:
status: "authenticated".issuer— the canonical validator's issuer URL (e.g.https://cookpit.spec/v3.2/validate).validatorVersion— exact validator version string.issuedAt— UTC timestamp of stamping. The format is RFC 3339 in the formYYYY-MM-DDTHH:MM:SSZ— UTC ("Z" suffix), second precision (no fractional seconds), no timezone offset other thanZ. The regex is^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$. The format is pinned because the signed canonical bytes includeissuedAt; two validators producing different formats for the same logical instant would produce non-interoperable signatures.canonicalization— the canonicalisation profile name (default:RFC8785).keyId— identifier of the public key used to sign.fileFingerprint— lowercase 64-hex SHA-256 of the canonicalised file body computed per R5.signature— base64-encoded cryptographic signature over the canonical signed payload defined in R6.
It MAY carry an audit sub-object holding the validator's report
summary (hardFailures, softWarnings, infos) and any non-trust
metadata. The signature covers the audit sub-object so that audit
data is tamper-evident even though it is not trust-bearing.
R4. AI Chef contract. The AI MUST emit the unauthenticated form per
R2. The AI MUST NOT emit the authenticated form, MUST NOT invent a
signature, fileFingerprint, issuer, keyId, validatorVersion
or issuedAt, and MUST NOT claim status: "authenticated" under any
circumstance. The AI is not a trust authority.
R5. Canonicalisation and file fingerprint. The file fingerprint is computed as follows:
- Take the file's JSON body.
- Set both
cookpit.attestation.signatureANDcookpit.attestation.fileFingerprintto the empty string"". All othercookpit.attestationfields (status, issuer, validatorVersion, issuedAt, canonicalization, keyId, audit) remain in place. (Setting the two fields to empty rather than removing them keeps the signed payload's shape identical to the eventual authenticated payload, so consumers do not need to mutate field structure during verification. Both fields must be cleared together because the fingerprint is the hash of the bytes the signature is computed over, and a populated fileFingerprint would otherwise change those bytes recursively.) - Serialise the result with the canonicalisation profile named in
canonicalization(default RFC 8785 JCS): lexicographic key ordering, normalised numeric forms, no insignificant whitespace, fixed string escaping. The validator's portable fallback profile, used when RFC 8785 tooling is unavailable, iscookpit-canonical-v3.2.0— NFC-normalised strings, lexicographic key ordering at every level, ASCII-only escaping, tight separators. - Compute the lowercase 64-hex SHA-256 digest of the canonical bytes.
That digest is cookpit.attestation.fileFingerprint.
R6. Signature payload and verification. The signature is computed
over the same canonical bytes the fingerprint is computed from in R5
— i.e. over the entire canonicalised file with both signature and
fileFingerprint cleared. The fingerprint is therefore the SHA-256
of the exact bytes the signature is over; consumers do a single
canonicalisation pass and use the result for both checks. The
signature still binds the file end-to-end: tampering with any field
elsewhere — issuer, validatorVersion, issuedAt, canonicalization,
keyId, audit, the cooking plan body — alters the canonical bytes and
invalidates both the fingerprint match and the signature.
A stage-4 consumer verifies a file by:
- Reading
cookpit.attestation, requiringstatus: "authenticated". - Confirming
issuermatches the consumer's pinned canonical issuer. - Confirming
keyIdresolves to a public key the consumer trusts. - Re-canonicalising the file with
signaturecleared and recomputing the SHA-256 digest. The digest MUST equalfileFingerprint. - Verifying the signature over the canonical bytes using the trusted public key.
- Optionally checking the published revocation list and the consumer's
accepted
validatorVersionpolicy.
Any step's failure is a hard rejection.
R7. One-way transition. The unauthenticated → authenticated
transition is one-way and irreversible by editing. A consumer that
finds an authenticated block whose signature does not verify MUST
treat the file as untrusted; it does NOT downgrade to U semantics
silently. Re-validation requires the user to strip the attestation
block back to the unauthenticated form and resubmit at stage 2.
R8. Validator refusal modes. The canonical validator MUST refuse to issue an authenticated block when:
- Any hard criterion in
validation.mdfails. - The submitted file already carries an authenticated attestation block (it must be stripped back to unauthenticated and resubmitted).
- The submitted file claims
status: "authenticated"without a valid signature, fingerprint and key id (malformed).
In all refusal cases the validator returns the unmodified file plus the report.
R9. Forbidden runtime fields excluded. cookpit.attestation is plan
metadata, not runtime state. Section L's runtime-state heuristics
explicitly exempt the attestation block; trust metadata does not
mutate at runtime.
R10. Filename flag agreement. The filename's <status> segment
(section O) MUST agree with cookpit.attestation.status. A file
named …cpt.A.jsonld whose internal status is unauthenticated, or
named …cpt.U.jsonld whose internal status is authenticated, is
malformed and rejected by the validator (V-ATTESTATION-CONSISTENCY).
The filename remains decorative; the cryptographic binding remains
the load-bearing trust signal.
R11. Create-and-consume decoupling. The attestation block is created by the validator at stage 3 and consumed by downstream chef apps (and other authenticated-file readers) at stage 4. The two operations are decoupled in time: a v3.2 file may sit indefinitely between stage 3 and stage 4. The file's authenticity claims remain valid as long as the consumer can verify the signature against a trusted public key. There is no separate timing or expiry semantics in the attestation block; revocation, when it exists, is a runtime concern of the consumer's trust- anchor management and is out of scope for this section.
Conformance
A v3.2 file is conformant if and only if it satisfies every rule above and
validates against the v3.2 JSON Schema. Any failure of any rule is a hard
validation failure. The validator in validation.md enumerates how each rule
is checked.
A v3.2 file is authenticated if, in addition, it has been stamped by
the canonical validator at stage 3 per section R, carries the A
filename flag per section O, and verifies under the consumer-side
verification flow R6 against the published public key.