# Adaptive Nutrition Workflow — Consensus Revision 16

## Normative scope
Revision 16 supersedes conflicting details in 15; 13–15 remain normative otherwise. Planning only. Gate A remains shadow-only with all personalized candidate values null.

## Exact event-type flow adapter
Root `EventType` mapping:
- morning_check_in → MORNING
- nutrition_check_in → NUTRITION
- workout_check_in → WORKOUT
- trainer_session → TRAINER_SESSION
- safety_audit → SAFETY_AUDIT
- correction → invalid as root; recursively inherit flow from terminal `supersedes` root
- check_in_validated → metadata-only; references and validates a known root, never observation data
- history_imported → LEGACY_IMPORT; admitted only through an explicit import mapping that declares original flow/day and digest
- every other/unknown root → excluded from projection with typed reason; if it contains a policy-required field, block the candidate
Customer mismatch anywhere blocks the root. Safety audit unions reasons and dominates.

## Crash-consistent source-day protocol
Exact path: `data/customers/<key>/nutrition-plans/source-days.jsonl`. Strict row schema: schema_version=1.0, mapping_id, root_event_id, customer_key, mapped_flow, observation_kst_day, session_id, writer_epoch, root_preimage_digest, created_at_kst. Dedupe is root_event_id; identical replay returns receipt, conflicting replay blocks. Each row has canonical digest.

Release installs a durable `source-day-intents.jsonl` journal. Under `authority-transition.lock`, wizard finalization appends/fsyncs intent first, appends canonical root, appends/fsyncs source-day mapping, then appends intent_committed. Recovery rules: intent with no root is safely abandoned; root+intent without mapping deterministically completes mapping from intent after verifying root preimage; mapping without root is ignored and quarantined; conflicting data blocks. `source_day_cutover_event_id` is recorded in adaptive epoch state. Only roots strictly before cutover may use derived occurred_at day; post-cutover missing mapping+intent is corruption and blocks. Crash fixtures at all four boundaries assert exact rows/states.

## Canonical field/order table
Fixed scales: weight_kg 2, rates/percent/adherence/tolerances/multipliers/error_scores 2, sleep_hours/water_liters 2, g_per_kg 3, confidence ratios 4; calories/minutes/counts/grams/serving_grams/serving_count are integers. Food serving increments are integer grams. Dates are YYYY-MM-DD; KST timestamps are RFC3339 +09:00, second precision. All source/head/reason/failed-gate/food/candidate ID arrays sort ascending by canonical ID; seven-day arrays sort date ascending; alternatives sort by score then food IDs. `current.json` is exactly `data/customers/<key>/nutrition-plans/current.json`, serialized by the same NFC/UTF-8/non-ASCII/sorted compact serializer.

Observed energy: `macro_kcal = 4*C + 4*P + 9*F`; if recorded calories is zero, complete only when macro_kcal is also zero, then mark physiological anomaly/human_review. Otherwise `abs(macro_kcal-recorded)/recorded*100 <= observed_energy_tolerance_percent` using unrounded Decimal is inclusive pass. Comparison rounds nowhere.

## Atomic effective-overlay replacement
Use one append-only `adaptive_overlay_replaced` event with payload `{old_revision_id|null,new_revision_id,effective_from,effective_through,old_effective_through,new_digest,authority_snapshot_id}`. Under the transition lock, validation ensures intervals do not overlap; append+fsync is the single commit that both closes old overlay at `effective_from-1` and activates child. Crash before append leaves old; after append yields child—no gap. `adaptive_overlay_deactivated` similarly carries valid_from/reason; rollback emits deactivated plus epoch change under one transaction protocol. Supersede without replacement is deactivation.

## Cross-store historical authority
Every authority-changing action under the transition lock appends an `adaptive_authority_mirrored` event containing canonical source event/receipt ID and digest for consent, activation/disable, safety/correction head set, baseline, policy/catalog, destination, config epoch, and valid_from. Adaptive stream sequence is the global per-customer `as_of_sequence`. Source writers must append their canonical fact and mirror before releasing lock; recovery journal completes missing mirror. Historical resolver reads only mirror facts <= as_of_sequence, never current mutable stores. Current transitions additionally reload stores and require equality to latest mirrors. Before first mirror snapshot, historical adaptive resolution is unavailable/blocked.

## Lock participants and APIs
Path: `data/customers/<key>/.authority-transition.lock`. API `customer_authority_transaction(customer_root)` is implemented in shared profile utility and imported by: WizardService root/correction/safety finalization, EventStore append entrypoints for those types, customer_admin register/activate/disable/consent, adaptive store/coordinator/delivery, and per-customer config epoch fanout. Lock order remains authority → registry/config → `.events.lock` → intent/source-day/adaptive stream → delivery cache. Nested acquisition is detected/reentrant only in same process; timeout fails closed.

Global config update uses prepare/commit journal in profile runtime directory: write/fsync temp config, append `config_epoch_prepared` listing enabled customer keys/digests, atomically replace config, then under each customer lock append mirror+epoch event, then append committed. Startup: no customers permits config-only commit; for existing customers, incomplete fanout is completed when config digest matches prepared record, otherwise fail closed. Stream ahead/config behind always blocks. Independent-process deadlock/race fixtures cover every participant.

## Telegram certainty table
`confirmed_not_sent` is allowed only before provider invocation for local validation codes `wrong_topic`, `missing_destination`, `invalid_revision`, `authority_changed`, `message_too_long`; or after a separate Telegram lookup returning typed `message_absent` for the exact idempotency token. Telegram Bot API send exceptions do not prove absence. After invocation begins, timeout, cancellation, network error, 429, 5xx, 4xx, malformed JSON, missing/multiple message_id, or any exception = unknown. A normal single response with one valid message_id = delivered. There are no post-invocation send exceptions classified confirmed-not-sent. Tests assert one provider call and replacement authorization only for local pre-call failure or lookup-proven absence.

## Exact adherence and meal policy
Daily adherence deviation is `abs(observed-target)/target*100`; target<=0 is unavailable and blocks controlling adherence. A metric passes when deviation <= configured symmetric tolerance. Overall gate requires every configured controlling metric pass on >= minimum complete days; over- and under-target boundaries are golden fixtures.

Signed calorie step is +step or -step selected by goal-mode/rate decision table, then bounded as prior revisions specify. Macro redistribution keeps exact weekly calories/protein; per-day objective minimizes weighted L1 error: `abs(carbs-target_carbs)+macro_error_weight*abs(fat-target_fat)`, subject to daily exact energy and bounds. Tie order: objective, max daily deviation, category rank high→medium→low, date, lexicographic macro vector.

Versioned customer inputs live at `data/customers/<key>/nutrition-plans/meal-constraints.json`, mode 0600, with version/digest/operator approval: meal_count (1..8), budget tier, cooking/equipment enum, training time by date, preferences, allergies, restrictions, digestion exclusions. Proposal pins digest; mutation invalidates revision. Catalog rows define integer serving_min/max/step grams. Compiler bounds: <=4 foods and <=12 total serving units per Meal slot; bounded exhaustive search; score is normalized L1 macro error `sum(abs(actual-target)/max(target,1))`. Tie: score, fewer foods, total servings, food ID vector, serving vector. Equivalent swap must satisfy same slot constraints/tolerance/allergens; fallback is separately flagged convenience/eating-out row and same tolerance. No match blocks.

## Required exact fixtures and exits
Gate A additionally requires expected-byte fixtures for NFC/NFD/Decimals/order, intent/root/mapping crash states, missing/conflicting post-cutover sidecars, every EventType mapping, config fanout interruptions, authority mirror recovery, and independent writer races. Each fixture declares expected adaptive rows, current.json bytes, blocked reason, and transport count=0.

Later gates require overlay replacement crash before/after append; historical as-of vectors for consent/safety/config/rollback; every Telegram certainty code with exact attempt count; adherence over/under/equal boundaries; meal constraint mutation, serving bounds, combination ties/no-match; all exact expected proposal bytes and transport counts. Gate C/D remain blocked pending user policy values and manual topic-59 proof.

## Remaining intent reconciliation
Goal/rate band; adherence tolerance/minimum; calorie step/bounds/cooldown/emergency rate; protein/fat settings; category multipliers/schedule authority; meal constraints/catalog owner; D+29–84 policy.
