# Safe synthetic 30-day adaptive-history plan (revised, consensus stage 1 successor)

## Status and scope

Planning only. This revision supersedes planner stage 1 (`sha256:3726b653…f91b3935`) and resolves every Architect (BLOCK) and Critic (ITERATE) finding in §3 before any execution. Execution is sealed in code to exactly one target: profile root `/home/cube/.hermes/profiles/dualcoachtest`, registry `/home/cube/.hermes/profiles/dualcoachtest/customers/registry.json` (`registry_mode: ordinary_v1`, registration alone is **not** treated as evidence of disposability), customer key `virtual_customer`, data root `/home/cube/.hermes/profiles/dualcoachtest/data/customers/virtual_customer`. The seal is a module-level constant with no CLI/API override (test-only monkeypatch seam, §4.1).

Immutable pre-mutation baseline, asserted pre/post by digest:

- Canonical events: `wizard/events.jsonl` = 44 rows; canonical sequence: `nutrition-plans/canonical-sequence.jsonl` = 44 rows; pair-valid via `CanonicalEventTransaction.for_customer_runtime(runtime)` (`store.py:105`). Existing synthetic observations overlap recent dates.
- Feature document `nutrition-plans/feature-epoch.json` preserved byte-exact (sha256 recorded): `epoch=3, activation=true, delivery=false, analytics_shadow=false, operator_candidates=false, schema_version="1.0", config_digest=09314d41…f14f92`. The **whole** document and all flags are bound, not just two booleans (Critic C4/C9).
- Proposal revision 7: authoritative reader is `load_approved_adaptive_artifacts(data_root)` (`adaptive_nutrition.py:1768`); terminal proposal revision in `nutrition-plans/events.jsonl` (16 rows) must remain 7. `adaptive-overlays.jsonl`, `adaptive-registration-*.jsonl`, `policy*.json*` digests unchanged.
- Derived journals `source-days.jsonl` (44) / `source-day-intents.jsonl` (88): first 44/88 row digests unchanged; only additive mapping rows for newly appended synthetic roots are permitted.
- Any preflight mismatch is a stop condition, never a repair opportunity. No `recover()`, truncation, deletion, rewrite, or resequencing anywhere.

Reports/receipts contain only dates, counts, booleans, decision/reason enums, scenario/version labels, offsets, and 64-hex digests; never customer text, raw event IDs, destinations, credentials, or tokens.

## RALPLAN-DR

### Principles

1. **Canonical append-only truth:** history is created only as validated `Event` objects through the paired `CanonicalEventTransaction`; never raw JSONL, never edits to the 44/44 prefix.
2. **Schema/projection parity:** an importer emits only fields present in both strict `Event` (`models.py:763`) and `checkin-event.schema.json`; every projected field is typed and tested importer→projection end-to-end.
3. **Determinism with provenance:** one versioned, seeded 30-day scenario yields stable identities, payloads, and dedupe keys; the single execution-time value (audit `recorded_at_kst`) is persisted with the generation and reused on replay (Architect A2, Critic C6).
4. **Fail closed and sealed:** the importer hard-rejects every runtime except the sealed disposable boundary, rejects future days, foreign collisions, torn ledgers, stale preconditions, and mixed generation state; it never activates, delivers, or calls a provider.
5. **Non-destructive recovery:** compensation is a typed, append-only retraction; reconciliation is read-only against canonical files; malformed/torn pairs stop execution (Architect A3).

### Top decision drivers

1. P1 is a source contract defect: `history_imports._baseline_event()` (`history_imports.py:83`) emits archived `history_imported` without `import_manifest`; `project_canonical_events()` (`adaptive_nutrition.py:2504`) skips non-`accepted`/`unsafe` terminals and reads `row["import_manifest"]` from the **terminal** row; strict `Event` and the JSON schema reject the field today.
2. `_canonical_adherence_values()` (`adaptive_nutrition.py:2422`) consumes an unpersistable `adherence_signal`; the typed `CanonicalAdherenceSignal` already exists (`models.py:964`, alias `AdherenceSignal` at 1032) and must be **reused**, not duplicated (Critic C2).
3. Reconciliation independently derives source days from `occurred_at_kst` (`customer_admin.py:3495`) and calls `canonical_transaction.recover()` (`customer_admin.py:3788`), which truncates torn tails (`store.py:318`) — both violate parity and immutability (Architect A3/A4).
4. `_append_many_locked()` (`store.py:257`) treats any existing dedupe key as a duplicate without payload equality; `read_snapshot_readonly()` (`store.py:208`) cannot nest under `EventStore._record_lock()` (`store.py:1085`) (Architect A6, Critic C1).

### Options considered

| Option | Pros | Cons |
|---|---|---|
| **A. Typed tagged manifest + sealed customer-bound importer + shared root resolver + generation lifecycle + read-only reconciliation + typed retraction. Chosen.** | Fixes P1 at source; parity between projection and reconciliation; deterministic resume/replay; additive compensation; no canonical mutation during reconciliation. | Touches model, shared schema, importer, store, projection, reconciliation, admin, and tests. |
| B. Hand-author/patch JSONL rows. | Fast. | Violates append-only/supported-API constraints; unprovable idempotence. Rejected. |
| C. Drive current-date wizard/Telegram-like flows. | Reuses UI. | Finalization timestamps, transport context, live-delivery risk; cannot create prior-day records. Rejected. |
| D. Projection-only occurrence-date fallback. | Small diff. | Schema drift, provenance ambiguity, projection/reconciliation mismatch on corrections. Rejected (Architect tradeoffs). |
| E. Canonical `recover()` inside reconciliation. | Repairs dev ledgers. | Truncates the target ledger; violates the no-rewrite execution contract. Rejected (Architect A3). |
| F. Generic registered-customer import CLI. | Reusable. | Breaks virtual-customer-only containment. Rejected (Architect A1). |
| G. Pair-wise append with no generation state. | Minimal storage change. | Partial history + timestamp-conflicted retry after crash. Rejected (Architect A5). |

### Chosen decision / ADR

**Decision:** implement Option A. Introduce (i) a strictly typed, mutually exclusive tagged `ImportManifest` union (`historical_baseline` | `synthetic_30_day`) plus optional `Event.adherence_signal: CanonicalAdherenceSignal` and `Event.synthetic_history_retraction`; (ii) a sealed `EventStore.import_synthetic_history()` / `retract_synthetic_history()` pair executed under one exclusive canonical transaction lock using `_read_snapshot_locked(token)` / `_append_many_locked(..., token)`; (iii) a deterministic generation lifecycle (ABSENT → PARTIAL → COMPLETE, CONFLICT fails) whose audit timestamp is persisted in the first appended member and reused for byte-equal replay; (iv) one shared `resolve_canonical_roots()` resolver consumed by both `project_canonical_events()` and `_reconciliation_event_plan()`; (v) reconciliation made read-only against canonical files (no `recover()`); (vi) a typed append-only retraction as the sole compensation.

**Why:** repairs the real producer/consumer break instead of guessing, keeps gateway-facing source-day evidence equal to the local snapshot, preserves the immutable prefix/revision/epoch, and makes every failure branch fail closed without deletion.

**Consequences:** legacy manifest-less rows remain valid, untouched, and non-projectable; new baseline imports gain typed baseline manifests; the synthetic importer is the sole supported writer for this scenario (not a generic append API); retraction removes a generation from projection/reconciliation only, never from the ledger.

## Finding resolution ledger

| Finding | Resolution |
|---|---|
| Architect A1 (HIGH, generic CLI scope) / Critic C4 | §4.1 sealed boundary constants + hard rejection tests (wrong profile/customer, standalone store, forged binding, symlink/foreign root, ordinary sibling). |
| Architect A2 (HIGH, timestamp vs replay equality) / Critic C6 | §4.3 generation lifecycle: first `recorded_at_kst` persisted in generation members, reused on resume/replay; mixed timestamps → CONFLICT, zero writes; injected-clock byte-stability test. |
| Architect A3 (HIGH, `recover()` truncation in reconciliation) / Critic C8 | §4.5 reconciliation uses validated snapshot only; torn/malformed pair raises before any derived write; byte-identical canonical files asserted after failed reconciliation. |
| Architect A4 (HIGH, reconciliation source-day semantics) | §4.5 shared `resolve_canonical_roots()` used by `_reconciliation_event_plan()`; root manifest day retained through terminal corrections; parity test projection vs mapping. |
| Architect A5 (HIGH, partial-generation recovery) / Critic C8 | §4.3 lifecycle classification under lock; PARTIAL quarantined from projection/reconciliation; resume appends exactly missing roots; crash tests at every pair boundary. |
| Architect A6 (HIGH, nested-lock TOCTOU) / Critic C1 | §4.3 exact lock protocol: `read_snapshot_readonly()` only for outer preflight; single `_record_lock()` critical section revalidates everything; no nesting; stale-preflight/interleaving-writer test. |
| Architect A7 (MED, compatible overlap) / Critic C6 | §4.3 field-by-field terminal-root semantic equality predicate; foreign collisions fail closed; skipped days reported, never counted as appended. |
| Architect A8 (MED, tagged shapes) / Critic C2, C7 | §4.2 tagged `oneOf` manifest union, bounded retraction reason enum, target-membership proof, deterministic retraction identity, model+schema parity tests. |
| Critic C2 (adherence model) | §4.2 binds `Event.adherence_signal` to existing `CanonicalAdherenceSignal`; no parallel model; adherence allowed on history roots only, forbidden on terminal corrections. |
| Critic C3 (exact APIs/digests) | §4.2/§4.6 exact signatures, receipt schemas, error enum, digest preimages. |
| Critic C5 (scenario recipe) | §4.4 fixed window `2026-06-28..2026-07-27`, fixed seed, SHA-256 counter-stream formula, full offset table, bounds, allowlists, expected counts/correlations. |
| Critic C9 (concrete execution/acceptance) | §5 exact commands/args (no placeholders); §6 quantified acceptance incl. per-prefix row digests, delta ranges, decision enum, zero-delta replays. |
| Critic C10 (fault/no-live breadth) | §4.7 negative/crash/tripwire matrix; §8 explicit no-live boundaries. |
| Importer→projection P1 | §4.2 producer manifests + §4.5 projection root-manifest/archived/typed-adherence repair + §4.7 importer-to-projection test. |

## Exact implementation plan

### 4.1 Sealed target boundary (Architect A1, Critic C4)

Modify `/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/checkin_cli/customer_admin.py` and `store.py`:

```python
_SEALED_PROFILE_ROOT = Path("/home/cube/.hermes/profiles/dualcoachtest")
_SEALED_REGISTRY = _SEALED_PROFILE_ROOT / "customers" / "registry.json"
_SEALED_CUSTOMER_KEY = "virtual_customer"
_SEALED_DATA_ROOT = _SEALED_PROFILE_ROOT / "data" / "customers" / "virtual_customer"
_SEALED_SCENARIO_VERSION = "synthetic_30_day_v1"
```

- `import_synthetic_history` / `retract_synthetic_history` resolve the runtime exclusively via `_registered_customer(registry, profile_root, customer_key, data_root=None)` (`customer_admin.py:229`) and then require: resolved profile root == `_SEALED_PROFILE_ROOT` (no symlink, `resolve()` equality), requested registry == `_SEALED_REGISTRY`, key == `_SEALED_CUSTOMER_KEY`, `runtime.data_root.resolve()` == `_SEALED_DATA_ROOT`, `runtime.binding` is `RegisteredCustomerBinding`, and the store's `CanonicalEventTransaction` is `for_customer_runtime(runtime)` (`store.py:105`) whose `events_path`/`sequence_path` resolve to the sealed wizard/nutrition-plans files. Any deviation raises `CustomerAdminError("sealed target boundary violated")` before any read or write.
- `EventStore.import_synthetic_history()` additionally refuses stores whose `_canonical_transaction is None` (standalone/unregistered store rejection).
- The seal is a module constant; no CLI flag or API parameter can widen it. Tests monkeypatch the constant to a copied tmp fixture (E2E only); the production command path has no override.

### 4.2 Strict model/schema parity — exact typed contracts (Critic C2/C3, Architect A8)

Modify `/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/checkin_cli/models.py` and `/home/cube/.hermes/profiles/physique-coach/contracts/checkin-event.schema.json` (shared contract — every legacy shape must remain valid in both profiles):

```python
class HistoricalBaselineManifest(BaseModel):  # frozen, extra="forbid"
    manifest_version: Literal["1.0"] = "1.0"
    kind: Literal["historical_baseline"]
    observation_kst_day: date                      # ISO YYYY-MM-DD, strict parse, no prefix truncation
    source_digest: str = Field(pattern=r"^[0-9a-f]{64}$")

class SyntheticHistoryManifest(BaseModel):       # frozen, extra="forbid"
    manifest_version: Literal["1.0"] = "1.0"
    kind: Literal["synthetic_30_day"]
    observation_kst_day: date
    scenario_version: Literal["synthetic_30_day_v1"]
    seed_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
    scenario_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
    generation_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
    day_offset: int = Field(ge=0, le=29)

ImportManifest = HistoricalBaselineManifest | SyntheticHistoryManifest  # discriminated by `kind`

class RetractionReason(StrEnum):
    OPERATOR_REQUEST = "operator_request"
    VALIDATION_FAILURE = "validation_failure"
    PARTIAL_GENERATION = "partial_generation"
    TEST_CLEANUP = "test_cleanup"

class SyntheticHistoryRetraction(BaseModel):     # frozen, extra="forbid"
    retraction_version: Literal["1.0"] = "1.0"
    generation_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
    reason: RetractionReason
    root_digests: tuple[str, ...] = Field(min_length=1)   # each ^[0-9a-f]{64}$; target-membership proof
```

`Event` additions: `import_manifest: ImportManifest | None = None`, `adherence_signal: CanonicalAdherenceSignal | None = None` (reusing `models.py:964` exactly — no parallel model), `synthetic_history_retraction: SyntheticHistoryRetraction | None = None`; `EventType.SYNTHETIC_HISTORY_RETRACTED = "synthetic_history_retracted"`.

`Event.validate_discriminator()` (`models.py:825`) conditional matrix (schema mirrors each rule; both reject extra/forbidden combinations):

| Rule | Requirement |
|---|---|
| `import_manifest` present | only on `event_type=history_imported` root (`supersedes is None`); synthetic variant requires `provenance.source_type == "derived"`; baseline variant requires `provenance.source_type == "historical_markdown"`; `occurred_at_kst` date == `observation_kst_day`. |
| `history_imported` without manifest | still valid (legacy), but projection ignores it. |
| `adherence_signal` present | only on `history_imported` roots; **forbidden** on terminal corrections, retractions, and all other types. Correction terminals are full replacement `check_in` payloads with no manifest/adherence. |
| `synthetic_history_retracted` | requires `synthetic_history_retraction`; forbids `check_in`, `adherence_signal`, `import_manifest`, `supersedes`; status `accepted`. |
| Manifest on non-root or mismatched `kind`/variant fields | reject (tagged union is mutually exclusive; baseline forbids synthetic fields and vice versa). |

JSON schema: `oneOf` on `import_manifest` with `additionalProperties: false` per variant; `adherence_signal` mirrors the `CanonicalAdherenceSignal` fields (target/actual kcal/carbs/protein/fat, `tolerance_percent`, `complete`, `adherent`, `status ∈ {adequate,inadequate,missing,contradictory}`, `reason ≤160`); retraction definition with bounded `reason` enum; event-type enum extended; all legacy shapes remain valid. No permissive `object` escape hatch.

Digest preimages (all SHA-256 hex): `seed_digest = H("synthetic-seed:v1|" + seed)`; `scenario_digest = H("synthetic-scenario:v1|" + scenario_version + "|" + window_start + "|" + window_end + "|" + seed_digest)`; `generation_digest = H("synthetic-generation:v1|" + scenario_digest + "|" + customer_key + "|" + profile_binding_digest)` where `profile_binding_digest = H(str(resolved customer_root))` (matching `store.py` binding digest); per-day `event_id = "syn_" + H(generation_digest + "|" + offset + "|root|" + payload_digest)[:24]`; `dedupe_key = "synthetic-history:v1:" + generation_digest[:32] + ":" + offset + ":" + role`; correction identity = `H(root_event_id + "|" + corrected_payload_digest)`; retraction identity/dedupe derived from `generation_digest` only (deterministic, idempotent).

### 4.3 Store hardening, exact lock protocol, generation lifecycle (Architect A2/A5/A6/A7, Critic C1/C6)

Modify `/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/checkin_cli/store.py`:

1. **Equality-on-duplicate:** in `_append_many_locked()` (`store.py:257`), when `existing_by_dedupe` hits, compare the candidate's full canonical serialization against the existing validated event; equal → idempotent skip; unequal → `ValueError("conflicting canonical event replay")` raised inside the candidate loop, i.e. before any pair append (zero writes on conflict).
2. **Exact lock protocol (no nested-lock TOCTOU):**
   - Outer read-only preflight (execution phase 0 and the verification helper): `CanonicalEventTransaction.read_snapshot_readonly()` (`store.py:208`) or `read_locked()` (`store.py:213`) only. Evidence = counts, per-row prefix digest, torn-tail validity.
   - Import/retract: `with self._record_lock():` (`store.py:1085`) → token = `self._active_transaction_token` → **revalidate everything** under the token via `self._canonical_transaction._read_snapshot_locked(token)` (`store.py:417`): pair validity, 44/44 baseline counts, sealed boundary, feature/revision preconditions, generation classification, collision classification → build candidates → `validate_event()` each → `_append_many_locked(candidates, token)` → sanitized receipt. `read_snapshot_readonly()` is never called inside `_record_lock()` (its `_shared_locked()` rejects while a transaction token is active). Candidate construction never trusts pre-lock evidence without revalidation. Test: stale preflight + interleaving writer must fail closed.
3. **Generation lifecycle (crash-safe commit/resume/quarantine):** expected set = deterministic from `(scenario_version, seed_digest, ending_on)`: 29 roots (offset 11 omitted) + 1 correction terminal = 30 events. Under the same lock, scan existing events for `import_manifest.generation_digest == spec.generation_digest`:
   - `ABSENT` (no members): capture one `recorded_at_kst` (KST now) once; build all candidates with it.
   - `COMPLETE` (every non-skipped expected member present and byte-equal to candidates rebuilt with the **persisted** `recorded_at_kst`, and previously skipped days still classify compatible): return zero-write receipt.
   - `PARTIAL` (some members present, all byte-equal, none conflicting): resume — append exactly the missing members using the persisted `recorded_at_kst`. Partial generations are **quarantined** from `project_canonical_events()` and reconciliation until complete (resolver treats incomplete generations as absent for analysis; see §4.5).
   - `CONFLICT` (any present member differs, mixed `recorded_at_kst`, unexpected members, or a previously skipped day now incompatible): raise `generation_conflict`; zero writes; stop.
4. **Overlap compatibility predicate (exact):** a pre-existing terminal row for the same observation day is *compatible* iff all hold: `event_type == "history_imported"`; `status == "archived"`; it is a childless root (`supersedes is None`, no correction children); `provenance.source_type` equal; every `check_in` field present in **both** payloads is byte-equal after canonical serialization; existing manifest (if any) has equal `observation_kst_day`. Compatible → skip the candidate day, receipt disposition `skipped_compatible`; skipped days never change `generation_digest` and are never counted as appended. Any field mismatch, multiple terminals for the day, an existing correction chain, or unknown provenance → `foreign_collision`, zero writes, stop.

New store APIs (registered stores only; sanitized frozen receipts — counts, day range, digests, per-offset dispositions; never raw IDs/text):

```python
def import_synthetic_history(self, spec: SyntheticHistorySpec) -> SyntheticHistoryReceipt
def retract_synthetic_history(self, generation_digest: str, reason: RetractionReason) -> RetractionReceipt
```

Retraction: validates the generation exists and is complete, computes `root_digests` from ledger members (membership proof), appends one typed retraction event. Same generation + same reason → byte-identical dedupe → idempotent zero-write; different reason → dedupe conflict raises; unknown/partial generation → `unknown_generation` fail closed. Baseline immunity: builder rejects any digest not belonging to the synthetic generation.

### 4.4 Producers and the fixed scenario recipe (Critic C5)

Modify `/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/checkin_cli/history_imports.py`:

- `_baseline_event()` (`:83`) attaches `HistoricalBaselineManifest(observation_kst_day=entry.recorded_date, source_digest=entry.source_sha256)`; archived status and deterministic identity unchanged. `baseline_events()` (`:43`) returns only validated `Event`s.
- New: `SyntheticHistorySpec` (frozen: `customer_key`, `profile_binding_digest`, `ending_on: date`, `scenario_version`, `seed: str` 16–64 ASCII `[a-z0-9:_-]`), `SyntheticHistoryReceipt`, `RetractionReceipt`, `build_synthetic_history_events(spec, existing_events, recorded_at_kst) -> tuple[Event, ...]` (returns `Event`s only), and a typed retraction-event builder.

**Fixed recipe:** window = 30 completed KST days **2026-06-28 (offset 0) … 2026-07-27 (offset 29)**, `ending_on = 2026-07-27 ≤ current KST day − 1`; any future/current day or wrong window size rejects. Seed (execution value): `synthetic-30day-v1-dualcoachtest-2026-07-27`. Deterministic stream: `U(field, offset) = int.from_bytes(sha256(f"{seed_digest}|{offset:02d}|{field}")[:4], "big") / 2**32`; `value = round(lo + U·(hi−lo), precision)`.

| Offsets | Pattern | Values (bounds; rounding) |
|---|---|---|
| all | weight | `78.0 ± 0.4 kg`, 0.1 kg; range [74.0, 82.0] |
| all | sleep | `7.0–8.0 h`, 0.1 h |
| training days `offset % 7 ∈ {1,3,5}` (1,3,5,8,10,12,15,17,19,22,24,26,29) | carbs/calories up | carbs `282 g` (245+15%), calories `2530 kcal` (2300+10%), actual within ±8% of target, 1 kcal/1 g |
| rest days | base | carbs `245 g`, calories `2300 kcal`, protein `150 g ±5%`, fat `80 g` |
| 14–17 | disruption | sleep `5.5–6.0 h`; digestion token ∈ {`bloated`,`reflux`}; actual calories `+12–18%` over target → adherence `adherent=false, status=adequate`; weight `+0.4–0.8 kg` transient |
| 18–22 | recovery | sleep `≥7.0`; adherence true; weight drifts `−0.1–−0.3 kg` |
| 11 | planned no-row omission | 0 events |
| 7 | partial | omit `body_weight_kg`, `sleep_hours` |
| 19 | partial | omit `calories_kcal` + all adherence fields → adherence `status="missing"` |
| 23 | correction | root weight `78.9 kg`; terminal `correction` event (`supersedes=root`, full replacement payload, weight `78.2 kg`, no manifest/adherence) |

Enum allowlists: training ∈ {`strength_upper`,`strength_lower`,`zone2`,`intervals`,`rest`}; digestion ∈ {`normal`,`bloated`,`reflux`}; pain ∈ {`none`,`knee_mild`,`shoulder_mild`}. `occurred_at_kst = f"{day}T00:00:00+09:00"`. Counts: 30 window days; 29 roots; 1 correction terminal; 30 candidate events; distinct observation days 29 (minus `skipped_compatible` at execution); 28 complete adherence signals (29 − offset 19), adherent false exactly offsets 14–17, true on 24; weight samples 28. Correlations asserted in tests: training-day carb mean ≥ rest-day mean × 1.10; disruption sleep mean ≤ 6.0 vs ≥ 7.0 elsewhere; weight peaks inside 14–17 and declines through 18–22.

### 4.5 Shared root resolver, P1 projection fix, read-only reconciliation (Architect A3/A4, Critic C7)

Modify `/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/checkin_cli/adaptive_nutrition.py` and `customer_admin.py`:

```python
@dataclass(frozen=True)
class CanonicalRootResolution:
    root_event_id: str
    terminal_event_id: str
    event_type: str
    mapped_flow: str | None
    observation_kst_day: date
    import_manifest: ImportManifest | None
    adherence_signal: CanonicalAdherenceSignal | None
    synthetic_generation_digest: str | None
    generation_complete: bool
    retracted: bool

def resolve_canonical_roots(events: tuple[Event, ...]) -> dict[str, CanonicalRootResolution]
```

- Input is **validated `Event` objects only**; both consumers stop passing raw mappings across the contract boundary (reconciliation converts snapshot records via `validate_event()` first). Correction graph rules unchanged: cycle/fork/orphan raise; terminal = childless tip. For each terminal: root id, terminal id, `mapped_flow` from `_EVENT_FLOW_BY_TYPE` (`:2343`), day from the **root** `import_manifest.observation_kst_day` for `history_imported` (strict full-date ISO parse — no `[:10]` truncation), terminal `check_in` values, root `adherence_signal`, synthetic generation digest, `generation_complete` (all expected members present), `retracted` (any validated retraction whose `root_digests` covers the root and whose `generation_digest` matches; retraction referencing unknown digests → raise).
- `project_canonical_events()` (`:2504`): consume resolutions; accept an archived terminal iff `event_type == "history_imported"` with a valid root manifest; a terminal correction keeps the root day/provenance with terminal values; use the typed `CanonicalAdherenceSignal` (never raw dicts); exclude roots where `retracted` or `not generation_complete`; legacy manifest-less rows remain ignored; preserve `canonical_projection=True` and safety-union semantics.
- `_reconciliation_event_plan()` (`customer_admin.py:3495`): consume the same resolutions; source-day mapping day = root manifest day for history imports (identical to `occurred` day for roots; correct through terminal corrections), keyed by `root_event_id` as today. Behavior is preserved for the existing 44 mappings and corrected for history corrections — one semantics, one resolver, tested for parity.
- **Read-only reconciliation:** remove the `canonical_transaction.recover()` call (`customer_admin.py:3788`) from this path; obtain evidence via the validated snapshot (`_read_snapshot_locked`/`read_locked`). Any torn/mismatched pair raises before any derived journal write; failed reconciliation must leave `wizard/events.jsonl` and `canonical-sequence.jsonl` byte-identical (asserted by digest in tests). `reconcile_adaptive_nutrition_journals(profile_root, customer_key, *, canonical_events, registry)` (`:3627`) remains the only supported entrypoint, invoked with the sealed profile root/key. Derived writes stay append-only to `source-days.jsonl`/`source-day-intents.jsonl`; retracted or incomplete generations produce no new mappings; existing mappings are never rewritten or deleted.

### 4.6 Admin APIs, CLI, sanitized receipts, bounded errors (Critic C3)

`customer_admin.py`:

```python
def import_synthetic_history(profile_root: Path, customer_key: str, *, registry: Path,
                             ending_on: date, scenario_version: str, seed: str) -> Mapping[str, object]
def retract_synthetic_history(profile_root: Path, customer_key: str, *, registry: Path,
                              generation_digest: str, reason: str) -> Mapping[str, object]
```

CLI: `import-synthetic-history <customer_key> --ending-on <YYYY-MM-DD> --scenario-version synthetic_30_day_v1 --seed <seed>` and `retract-synthetic-history <customer_key> --generation-digest <64-hex> --reason <enum>` (registry/profile-root as existing global options). Neither accepts event JSON, event types, source refs, or provider/destination parameters. Receipts (`SyntheticHistoryReceipt`/`RetractionReceipt`): `schema_version, scenario_version, seed_digest, scenario_digest, generation_digest, window_start, window_end, recorded_at_kst, appended_count, skipped_compatible_count, event_count_before/after, sequence_count_before/after, canonical_prefix_digest_before/after, per_offset_disposition ∈ {appended, skipped_compatible, omitted_planned, corrected}, retraction fields (reason, retracted_root_count)`. Bounded error enum `SyntheticHistoryError`: `sealed_boundary_violation, stale_precondition, window_invalid, future_day_rejected, generation_conflict, foreign_collision, torn_pair_stop, schema_drift, unknown_generation, retraction_target_mismatch, unregistered_store, unsafe_provenance`.

### 4.7 Tests before any target mutation

Modify/add under `/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/tests/` and `/home/cube/projects/richard/hermes-agent/tests/gateway/test_adaptive_nutrition.py`:

- **Unit:** fixed seed/window byte-stable `model_dump`s; all bounds/rounding/allowlists; correlation invariants; omission/correction offsets; tagged-union and discriminator matrix (model **and** schema, incl. forbidden combos); adherence reuse (no parallel model); digest preimages; dedupe equality rule; retraction identity/idempotency/reason-conflict; date parsing rejects `2026-07-27junk`.
- **Integration (tmp registered fixtures):** single-lock import writes equal event/sequence deltas; stale preflight + interleaving writer fails; two-process serialized replay → second zero-write; injected-clock replay byte-stability; crash injection after the *k*-th pair for k ∈ 0..30 → resume completes exactly; crash between event-row and sequence-row write (torn pair) → next run raises `torn_pair_stop`, no truncation, files preserved; importer→`project_canonical_events()` P1 test (baseline + synthetic roots contribute samples/adherence; terminal correction keeps root day; retraction excludes only its generation; incomplete generation quarantined); projection↔reconciliation mapping parity; failed reconciliation leaves canonical bytes identical.
- **E2E (local-only, monkeypatched seal to a copied fixture):** import → reconcile → pure `validate_typed_safety` (`adaptive_nutrition.py:1886`) / `project_canonical_events` / `load_approved_adaptive_artifacts` (`:1768`) / `propose` (`:3330`) → redacted report → replay (zero delta) → retract → re-reconcile (no new mappings) → original fixture rows/digests survive. Tripwires: monkeypatch Telegram transport, provider clients, gateway entrypoints, and `AdaptiveNutritionCoordinator.create_production_proposal` to raise on construction; assert zero invocations; assert feature bytes, revision, lifecycle/reservation/delivery rows unchanged on every failure branch.
- **Observability:** receipts/report contain only the allowlisted fields (§6 report schema); assert absence of raw event IDs, destinations, tokens, customer text; gateway regression module keeps rejecting corrupted pairs/unapproved artifacts/feature-blocked states without delivery.

## Execution phases (after all tests pass locally)

0. **Read-only preflight** via `read_snapshot_readonly()` + `load_approved_adaptive_artifacts`: assert 44/44, pair validity, revision 7, feature document sha256, sealed boundary resolution. Capture digests only; any mismatch stops.
1. **Focused tests:**
   ```bash
   cd /home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli
   python -m pytest tests/test_event_contract.py tests/test_history_imports.py tests/test_adaptive_nutrition.py tests/test_customer_admin.py -q
   cd /home/cube/projects/richard/hermes-agent && .venv/bin/python -m pytest tests/gateway/test_adaptive_nutrition.py -q
   ```
2. **Import (twice):**
   ```bash
   cd /home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli
   python -m checkin_cli.customer_admin --registry /home/cube/.hermes/profiles/dualcoachtest/customers/registry.json --profile-root /home/cube/.hermes/profiles/dualcoachtest import-synthetic-history virtual_customer --ending-on 2026-07-27 --scenario-version synthetic_30_day_v1 --seed synthetic-30day-v1-dualcoachtest-2026-07-27
   ```
   Second run must return `appended_count=0`, zero deltas.
3. **Read-only verify:** `event_delta == sequence_delta == receipt.appended_count` (delta range [0,30] depending on compatible overlaps; equality is the invariant, per-offset dispositions reported); every new row validates against the schema; no future day; first-44-row prefix digests unchanged; revision 7; feature sha256 unchanged.
4. **Reconcile once** via `reconcile_adaptive_nutrition_journals()` with the sealed root/key; expected derived delta = one source-day mapping + one intent per newly appended root; first 44/88 derived rows unchanged; second reconciliation = zero delta.
5. **Pure local analysis only:** `validate_typed_safety`, `project_canonical_events`, `load_approved_adaptive_artifacts`, `propose`. Record projection counts and decision enum.
6. **Fail-closed dry-runs on in-memory/read-only copies:** malformed manifest, orphan correction, torn pair, unknown-generation retraction, mixed-timestamp generation, wrong profile/customer/store, current feature state (activation true/delivery false) via `audit_gate_d_preflight()` (`customer_admin.py:5158`) as read-only audit — its feature blocker is expected evidence, never a flag change.
7. **Report:** write `/home/cube/projects/richard/traning coach/.gjc/reports/synthetic-history-execution-report.md` (schema in §6).

## Acceptance criteria

- P1 fixed at source: schema-valid baseline and synthetic `history_imported` roots carry tagged manifests and project into samples/adherence; a terminal correction retains the root day/manifest with terminal values; projection and reconciliation source-day mappings agree exactly.
- Model/schema parity for tagged manifests, reused `CanonicalAdherenceSignal`, and typed retraction; legacy shapes still valid; no raw/untyped bypass.
- Scenario covers exactly 2026-06-28…2026-07-27 with the §4.4 table; no future day; byte-stable across injected clocks.
- Import preserves all 44/44 prefix rows (per-row prefix digests), revision 7 (via `load_approved_adaptive_artifacts`), and the feature document sha256; `event_delta == sequence_delta == receipt.appended_count`; replay and second reconciliation are zero-delta; differing replay fails closed with zero writes.
- Projection evidence: observation-day count = 29 − skipped; 28 complete adherence signals (4 non-adherent at offsets 14–17); 28 weight samples; `propose()` returns a decision enum whose calorie delta is zero (`calorie_changing == false`) with the exact enum token pinned by the focused test.
- All §4.7 unit/integration/e2e/observability tests pass, including crash, replay, collision, seal-rejection, and tripwire branches.
- Cleanup = at most one typed retraction; no deletion/restoration; ledger, revision, and epoch unchanged afterward.

Report schema allowlist: plan/scenario/seed/generation digests, window dates, per-offset disposition enums, pre/post counts and prefix digests, feature sha256 + epoch + flags, revision int, projection counts, decision token, reconciliation deltas, replay/retraction outcomes, fail-closed reason codes, test pass counts. Everything else forbidden.

## Additive rollback / compensation matrix

| Failure phase | Action |
|---|---|
| Preflight mismatch | Stop; no writes; report digests. |
| Candidate/schema validation failure | Stop before append; zero writes; fix code, rerun tests. |
| Crash between pair appends | Resume under lock (PARTIAL → exact missing roots, persisted timestamp); CONFLICT → stop, zero writes. |
| Torn event/sequence pair | Unrecoverable within this plan: stop, preserve files/evidence, escalate; **no** `recover()`, truncation, retraction, or manual edit. |
| Post-append validation/reconciliation failure | Append typed retraction (`validation_failure`) after recording evidence; reconciliation of retracted generation adds no mappings; canonical files never modified. |
| Receipt loss | Rebuild receipt read-only from ledger scan (generation members, counts, digests). |
| Double/mistaken retraction | Same reason → idempotent zero-write; different reason/unknown generation → fail closed. |
| Operator abort | `retract-synthetic-history … --reason operator_request`; derived journals stay append-only; report documents the immutable audit trail. |

## Deliberate pre-mortem

1. **Schema/model drift** — `validate_event()` rejects new fields at append. Early sign: parity test failure. Mitigation: §4.7 parity tests gate all target commands.
2. **Silent duplicate/skew on rerun** — unequal deltas or same-dedupe unequal payload. Mitigation: equality rule, fixed generation digest, zero-write replay assertion.
3. **Projection drops history or misplaces a corrected day** — sample counts lag appended count. Mitigation: shared resolver + importer-to-projection and parity tests (archived, correction chain, retraction scope, quarantine).
4. **Analysis wanders into lifecycle/delivery code** — revision/epoch/reservation/provider counters move. Mitigation: pure functions only, coordinator/transport tripwires, pre/post digest comparisons.
5. **Operator attempts raw-file rollback after post-append failure** — stop, typed retraction only, evidence preserved.
6. **Stale preflight exploited by interleaving writer** — revalidation under the exclusive token; stale-evidence test proves fail-closed.
7. **Retry-after-crash timestamp conflict** — persisted `recorded_at_kst` reuse; mixed timestamps = CONFLICT, zero writes.
8. **Seal bypass via symlink/foreign root/sibling customer** — resolved-path equality plus binding digest; rejection tests.
9. **Reconciliation truncates a torn tail** — `recover()` removed from this path; byte-identity assertion after failed reconciliation.
10. **Redaction leak in receipts/report** — allowlist schema test; absence assertions for IDs/destinations/tokens/text.

## Explicit no-live boundaries

No Telegram transport construction or send; no provider API call or client construction; no gateway/service start (the hermes-agent module is run only as local pytest); no `AdaptiveNutritionCoordinator.create_production_proposal`, approval, activation, delivery, or schedule-reservation call; no deployment; no feature-flag, registry, profile, credential, or destination modification; no `canonical_transaction.recover()`, file truncation, deletion, restoration, resequencing, or raw JSONL writes anywhere; no mutation outside the sealed `virtual_customer` canonical pair and its append-only derived adaptive journals.

## Non-goals

- No modification of any record outside the sealed boundary, including profiles, registry, credentials, destinations, providers.
- No migration/repair of the existing 44/44 prefix, derived journals, or revision 7.
- No generic import API; no retrofitting manifests onto legacy rows; no inference of legacy observation days.
- No live adaptive proposal lifecycle; no use of this synthetic generation for any live profile.

## Follow-ups

Retain the sanitized report and receipts as execution evidence. Any future live-profile use requires a separately approved plan and must not reuse this generation, seed, or the sealed API invocation.
