# 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.md` references 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.md` and `bundle/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`](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`](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`](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
  own `A0` timer; either may finish first without unblocking
  `liveCook`.
- `liveCook` ≻ `prepCook` and `liveCook` ≻ `preCook` (strictly
  downstream). `liveCook` is unblocked only when the LAST of the
  declared upstream phases has fired its `A0` time-up alarm.
  `liveCook` never overlaps `prepCook` or `preCook`.

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 form `YYYY-MM-DDTHH:MM:SSZ` — UTC ("Z" suffix), second
  precision (no fractional seconds), no timezone offset other than
  `Z`. 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 include
  `issuedAt`; 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.signature` AND
  `cookpit.attestation.fileFingerprint` to the empty string `""`. All
  other `cookpit.attestation` fields (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, is
  `cookpit-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:

1. Reading `cookpit.attestation`, requiring `status: "authenticated"`.
2. Confirming `issuer` matches the consumer's pinned canonical issuer.
3. Confirming `keyId` resolves to a public key the consumer trusts.
4. Re-canonicalising the file with `signature` cleared and recomputing
   the SHA-256 digest. The digest MUST equal `fileFingerprint`.
5. Verifying the signature over the canonical bytes using the trusted
   public key.
6. Optionally checking the published revocation list and the consumer's
   accepted `validatorVersion` policy.

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.md` fails.
- 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.
