# Stage 41 — crash-completable, gateway-consistent synthetic 30-day history

## Status, boundary, and decision

Planning only. This supersedes stage 40 and closes Critic required changes 1–7. No execution is approved. The only mutable runtime boundary in a later approved execution is `/home/cube/.hermes/profiles/dualcoachtest/data/customers/virtual_customer`, through supported registered-store and derived-journal APIs only. The profile root, registry, feature epoch, approved revision 7, policies, overlays, activation/lifecycle/reservation/delivery journals, credentials, destinations, and every other customer are immutable. The scenario is fixed to the 30 completed KST days `2026-06-28..2026-07-27`, customer `virtual_customer`, profile `dualcoachtest`, version `synthetic_30_day_v2`, and seed `synthetic-30day-v2-dualcoachtest-2026-07-27`. No Telegram, provider, gateway/service startup, deployment, approval, activation, delivery, raw JSONL write, truncate, delete, restore, or `recover()` is permitted.

The implementation changes the dualcoachtest CLI package, its strict event contract, and the hermes gateway validator together. No target data is changed by this plan.

## Baseline seal and immutable evidence

Before any write, a strict read-only loader must acquire stable descriptors with `O_NOFOLLOW`, validate the registered `RegisteredCustomerBinding` without `_registered_customer()`, `load_runtime_customer_registry()`, `_recover_activation_journal()`, initialization, or any recovery routine, and require resolved paths exactly equal to the sealed constants. It must capture device/inode/size and SHA-256 for every protected artifact, then recheck them after locks are acquired and before each append. It rejects symlinks, replacement, missing files, malformed JSON, a torn canonical pair, a partial append protocol, or any digest mismatch before writing.

The preflight seal records: 44 validated canonical events and 44 sequence rows; exact byte digests for their complete files and each of the first 44 rows; 44/88 source-day/source-day-intent prefix rows and per-row digests; full feature-epoch bytes/digest; approved-artifact revision 7 and full bytes/digests of `adaptive-overlays.jsonl`, every `adaptive-registration-*.jsonl`, `policy*.json*`, lifecycle, reservation, delivery, activation, audit, registry, and profile-binding artifacts. It is a stop condition, never a repair input. The same complete artifact manifest is asserted byte-for-byte after successful import, failed import, replay, reconciliation failure, and retraction, except for the expressly append-only canonical, sequence, source-day, and intent journals.

## 1. Actual overlap coexistence and exact ownership

A date is not a collision merely because it has a terminal chain. Each planned day has one immutable `EffectiveDayOwner` selected under the canonical lock:

* `incumbent`: the exact currently pinned terminal chain owns the day; no synthetic root is created.
* `synthetic`: no nutrition/check-in terminal chain exists for the day; the deterministic synthetic root owns it.
* `omitted`: offset 11 only; no owner and no event.

The owner vector is part of the generation identity: canonical JSON of `(offset, day, owner_kind, incumbent_terminal_event_id, incumbent_terminal_digest)` for offsets 0–29, hashed with the scenario and binding digests. On replay/resume, any owner-vector change is `stale_precondition` with zero writes. An incumbent is accepted only when its terminal event ID and canonical newline-terminated SHA-256 exactly equal this pinned table; every other chain shape, extra terminal, fork, orphan, altered terminal, or new terminal is a stop, not a compatibility guess.

| offsets | days | owner | pinned terminal ID / canonical SHA-256 |
|---|---|---|---|
| 16–22 | 2026-07-14..20 | incumbent | `gate_d_weight_only_20260714_00` `7530c1d0e48378f07e912d8488211429ca7c3678f32d9cfefbe83654c242c50c`; `...15_01` `e81d79a99187700544fbd39d77ef3b2c1b3fdd8dcbdbe24e569269a3302b7502`; `...16_02` `7ced87cf650e76de3f74574aa03ae4d94dcddae279d7cb6cec869c75b0efd9cc`; `...17_03` `bb97b4778057d8ba2714dc30183b1e6395c7a4095e3007013680f2fafc6d2af4`; `...18_04` `a3ae9f9e111f6edb6382417ace37e3d96d40ccf9047622b623355fab7f32a9fc`; `...19_05` `044e8dad5219c798d6a5e73c118ad56feb415480e93bee8ab8d4e9d8c631433f`; `...20_06` `8404bf44cc56325e000c635dc2c8d9691490313ce88461abf843797ae33de3e9` |
| 24–26 | 2026-07-22..24 | incumbent | `gate_d_weight_only_20260722_07` `d4b2c6216096a38a2c1fa5c97e888fe43742d348b328c6d100f561f04fcf5606`; `...23_08` `99921ca2a56ab414e58815ebbb1b962394e7e8416b7a2dfe4fc5c0e929c7e57c`; `...24_09` `24c356df566ac4080633dde62bea254f90b4d0e1090ef453e559f94cf7ccd8b8` |
| 29 | 2026-07-27 | incumbent | `gate_d_weight_only_20260727_10` `144100ec84027971f35e9534bb950dfeafc41af5bf7aec587a2aff36d01937fc` |
| 11 | 2026-07-09 | omitted | planned no-row |
| 0–10, 12–15, 23, 27–28 | remaining 18 days | synthetic | deterministic root |

Thus the sealed baseline has exactly 11 incumbent days, one omission, 18 appended roots, and one appended correction: import delta **19 events and 19 sequence rows**. Offset 23 (`2026-07-21`) is synthetic-owned and has exactly one root plus exactly one terminal correction; it is never corrected when incumbent-owned, and neither a correction nor a retraction is fabricated in an all-incumbent run. The correction's `supersedes` is the deterministic offset-23 synthetic root and its root digest is included in the 18-root membership set.

The classifier remains variable so the copied-fixture tests can exercise every owner vector. In the all-skipped case (29 incumbent owners plus offset 11 omitted), `import_synthetic_history` validates the full vector then returns `all_skipped_noop`, appends no root, correction, sequence, pair-intent, mapping, intent, or retraction, and replay is byte-identical zero-write. Retraction of an all-skipped generation is the same zero-write `nothing_to_retract` receipt. A partial synthetic generation is never projected or reconciled.

## 2. Typed contracts and fully pinned scenario oracle

Add frozen, `extra="forbid"`, discriminator-based `HistoricalBaselineManifest` and `SyntheticHistoryManifest`, reuse the existing `CanonicalAdherenceSignal` on `Event.adherence_signal`, and add `EventType.SYNTHETIC_HISTORY_RETRACTED` with frozen `SyntheticHistoryRetraction`. Mirror the union and all discriminator rules in both strict profile models and the shared schema: manifests only on history-imported roots; synthetic provenance is `derived`; baseline provenance is `historical_markdown`; adherence is only on history roots; corrections are full replacement check-ins and contain neither manifest nor adherence; retractions contain only accepted typed retraction payload, no check-in, manifest, adherence, or `supersedes`. Legacy manifest-less records remain valid but non-projectable.

The v2 root formula is `U(field,o)=int.from_bytes(SHA256(seed_digest + "|" + f"{o:02d}" + "|" + field)[:4],"big")/2**32`; numeric `N(lo,hi,p)=round(lo+U*(hi-lo),p)`; choice `C(values)=values[min(len(values)-1,floor(U*len(values)))]`. Exact root fields are: target kcal/carbs/protein/fat = `2530/282/150/80` on offsets `%7 in {1,3,5}`, otherwise `2300/245/150/80`; actual kcal `round(target_kcal*N(.92,1.08,3))`, carbs/protein/fat each `round(target*N(.95,1.05,3))`; weight `N(77.6,78.4,1)` except 14–17 `N(78.4,78.8,1)` and 18–22 `N(77.9,78.2,1)`; sleep `N(7.0,8.0,1)` except 14–17 `N(5.5,6.0,1)`; training `C([strength_upper,strength_lower,zone2,intervals])` on training days else `rest`; digestion `C([normal,bloated,reflux])`, pain `C([none,knee_mild,shoulder_mild])`. At 14–17 actual kcal is instead `round(target*N(1.12,1.18,3))`; all four are complete, `adherent=false`, `status="inadequate"`, and reason is the literal `"calories_outside_tolerance"`. Offset 7 omits weight and sleep. Offset 19 omits calories and the entire adherence signal. All other root signals are computed by the existing `derive_canonical_adherence_signal`, including every target/actual/tolerance/complete/adherent/status/reason field; test golden vectors pin their serialized values. Offset 23's terminal correction changes root weight to literal `78.2`, contains the full replacement check-in, and no manifest/adherence. `occurred_at_kst` is `day + "T00:00:00+09:00"`.

Digest preimages are canonical JSON, sorted keys, compact separators, UTF-8, newline terminated, SHA-256 lowercase. `seed_digest=H("synthetic-seed:v2|"+seed)`; `scenario_digest=H("synthetic-scenario:v2|"+version+"|2026-06-28|2026-07-27|"+seed_digest)`; `generation_digest=H("synthetic-generation:v2|"+scenario_digest+"|virtual_customer|"+binding_digest+"|"+owner_vector_digest)`. IDs/dedupe keys derive from that digest, offset, role, and canonical payload digest. The checked-in golden fixture must contain all 18 serialized roots, offset-23 correction, all per-field values, IDs/dedupe keys, root digests, owner vector digest, and the literal pure-proposal decision **`maintain`** (`Decision.MAINTAIN`, `calorie_changing=false`); no runtime-generated expectation is accepted.

## 3. Append-only, crash-completable canonical pair protocol

Replace the assumption of pair atomicity with a single-candidate, append-only completion protocol inside the registered `CanonicalEventTransaction`; ordinary snapshot readers remain strict and never repair.

1. Under the exclusive canonical record lock, validate the common pair prefix and persist a canonical `pair-intent` record (candidate event bytes, candidate sequence bytes, their SHA-256s, common-prefix row digests, generation digest, ordinal) using one write-all buffer followed by flush and fsync. Its own partial suffix is accepted only if it is a unique byte prefix of the deterministic intent; append exactly its missing bytes, flush, and fsync. Any non-prefix, ambiguity, or mismatched prefix stops.
2. Write the one complete event JSONL record with write-all; after every partial-write boundary, flush boundary, and fsync boundary, a process death is recoverable only by rereading the byte suffix. Empty, exact prefix, or exact complete candidate is the only permitted state. Append precisely missing bytes, then flush/fsync. The event may be at most one candidate ahead of sequence.
3. Apply the identical suffix-completion algorithm to its one sequence record, then append/fsync an immutable `pair-complete` record. Only after its completion can the next candidate begin. Recovery never truncates, rewrites, resequences, or guesses; it only appends the remaining suffix of the uniquely durable intent/candidate.
4. The import starts by `complete_pending_synthetic_pair_locked` before strict classification. It may complete only the last v2 intent for this sealed generation and only its unique candidate. A normal runtime loader, proposal, projection, gateway, and readonly reconciler reject any pending intent, unequal row count, partial line, or absent/mismatched complete marker as `torn_pair_stop`; they do not invoke completion.

Fault injection covers process death after each write-all partial return, after write before flush, after flush before fsync, and after fsync for intent, event, sequence, and completion marker; it also covers event-complete/sequence-empty, event-complete/sequence-partial, sequence-complete/marker-missing, and the same boundaries while the offset-23 correction is the candidate. Every restart must converge append-only to exactly the golden 19/19 rows and identical bytes; malformed or ambiguous fragments remain zero-write failures.

## 4. Locks, lifecycle, and no-recovery loader

Use the sole lock order **canonical transaction lock → adaptive derived-journal lock(s)**. No code may acquire canonical while holding adaptive; lock-order assertions and two-process tests enforce this. Under canonical lock, reread the sealed binding and canonical snapshot, complete only the permitted pending pair, rerun all digest/inode/precondition and owner-vector checks, classify the generation, and build/validate candidates. Before any derived append, retain canonical lock, acquire adaptive locks in deterministic pathname order, reread immutable feature/revision artifacts and both journals through strict existing-file readers, then recheck every inode/digest. Release adaptive before canonical. A concurrent feature/revision/registry replacement or lock-order inversion fails closed without writes.

Generation state is keyed by generation digest plus owner vector: `ABSENT`, `PARTIAL`, `COMPLETE`, `RETRACTED`, or `CONFLICT`. Persist a single recorded timestamp in the first intent and reuse it exactly on completion/replay. `PARTIAL` contains only exact expected synthetic members and is quarantined. `COMPLETE` contains exactly its variable synthetic root set (18 here) and, iff offset 23 is synthetic-owned, exactly its one expected correction. Different payload, timestamp, owner vector, duplicate/foreign child, changed incumbent, or unexpected member is `CONFLICT` and writes nothing. Duplicate dedupe keys require byte-equal validated canonical serialization; unequal content is a conflict before any intent.

## 5. One canonical/derived coverage invariant and typed tombstones

Replace the gateway's raw `source_day.root_event_id == canonical_sequence_event_id` set check with one shared `resolve_canonical_coverage` implementation used by projection, sealed reconciliation, and `AdaptiveOperatorService._validate_journal_consistency`. The invariant is:

* Every canonical sequence event ID has exactly one normalized immutable **coverage record** keyed by `canonical_event_id`; legacy rows normalize to their existing key unchanged.
* A root coverage record holds root ID, observation day, terminal ID, and `active`; a correction coverage record has its own canonical event ID but the same root ID/day and `active`; a retraction coverage record has its own canonical event ID and `retraction` state.
* The coverage ID set equals the validated canonical-sequence ID set exactly. The materialized active-evidence set is coverage records whose root is not marked by a valid tombstone, whose generation is complete, and whose terminal is the resolver's terminal. It is this set—not raw historical rows—that feeds source-day readers and the adaptive projection.

Version the append-only source-day and intent schemas. Reconciliation appends coverage plus prepare/commit intents for every previously uncovered new canonical event, including the offset-23 correction and retraction; it never mutates the 44/88 prefix. A `SyntheticCoverageTombstone` is frozen/typed/schema-validated: version, generation digest, retraction event ID, reason enum, sorted unique exact synthetic root-digest set, and sorted unique synthetic root event-ID set. On a valid canonical retraction, append one tombstone record and its prepare/commit intents. Reader folding marks all coverage records with those root IDs inactive (including their correction coverage); the old records remain byte-exact audit evidence. Tombstone membership must equal the COMPLETE generation's variable root set, excludes incumbent/baseline/correction IDs and duplicates, and is idempotent only for byte-equal same reason. A different reason conflicts.

Projection consumes the same resolver: it carries manifest day/provenance from the root through correction terminal values; uses typed adherence only; excludes legacy manifestless, incomplete, and retracted synthetic roots. The sealed reconciler never calls recovery or a mutating operational reconciler. After retraction it appends the retraction coverage/tombstone only; a second reconciliation is zero-write, and no retracted synthetic root is active evidence.

## 6. APIs, receipts, and compensation

Expose sealed `import_synthetic_history`, `reconcile_synthetic_history_readonly`, and `retract_synthetic_history` only for the fixed profile/customer; no generic event or destination input. Receipts contain only schema/scenario/generation/owner-vector digests, counts, offset dispositions, state/reason enums, dates, and file/prefix digests. They exclude event IDs, text, destinations, credentials, and tokens.

Retraction first verifies COMPLETE variable membership and exact root digests, then appends one typed canonical retraction through the pair protocol and derives its coverage/tombstone. It never removes canonical or derived rows. Unknown, partial, all-skipped, altered, or already differently retracted membership fails/noops as defined above. This is the only compensation.

## 7. Required test and observability matrix

**Unit:** both model/schema packages parse identical legacy/root/correction/retraction vectors; reject every forbidden discriminator combination; golden all-field v2 vectors and digest preimages; exact owner table; all-skipped receipt; offset-23 ownership permutations; `inadequate` deviation status; duplicate equality; strict loader rejection; pair suffix state machine at every boundary; tombstone folding and membership rejection.

**Integration (copied registered fixtures):** actual 11-chain table yields exactly 18 roots + offset-23 correction (19/19); each altered incumbent digest/extra chain fails zero-write; every partial pair boundary resumes append-only to golden bytes; malformed fragments fail unchanged; concurrent stale writer; canonical→adaptive lock inversion; concurrent feature/revision/registry inode replacement; partial generation quarantine; import/replay; correction/retraction coverage set equality through the real gateway validator; retraction deactivates all affected root/correction evidence; repeated reconciliation zero delta.

**Local-only E2E:** monkeypatch seal to copied fixture; import → readonly reconcile → strict loader → projection → typed safety → approved-artifact load → pure proposal; assert exact golden output decision `maintain`; replay; retract; reconcile; verify no active synthetic evidence and exact coverage invariant. Tripwire construction/calls for Telegram, provider, gateway startup, coordinator production proposal, approval, activation, delivery, reservation, and deployment. No live endpoint is contacted.

**Immutable-artifact assertions:** successful import, all-skipped import, every fault injection failure, conflict, replay, readonly reconciliation failure, successful retraction, and post-retraction replay assert byte/digest equality for registry, binding/profile, feature epoch, approved revision, overlays, every registration/policy/lifecycle/reservation/delivery/activation/audit artifact, credentials/destinations, and all immutable prefix rows. Allowed journals additionally assert prefix identity, exact append count, schema validity, and no truncation. Observability tests enforce the receipt/report allowlist and absence of raw IDs/text/tokens/destinations.

## Deliberate pre-mortem

1. **An incumbent chain changes between review and import.** Sign: owner-vector digest mismatch. Mitigation: exact terminal digest table and under-lock recheck; stop zero-write.
2. **A power loss leaves a physical suffix.** Sign: pending intent or unequal rows. Mitigation: candidate-by-candidate suffix completion under lock; strict consumers reject pending state.
3. **Gateway accepts coverage but still reads retracted evidence.** Sign: active-evidence differs from resolver. Mitigation: shared resolver, typed tombstone folding, and real-validator retraction E2E.
4. **A loader silently repairs registry/journals.** Sign: protected digest changes in a failure test. Mitigation: direct no-recovery binding reader and exhaustive artifact assertions.
5. **Feature/revision changes race canonical append.** Sign: inode/digest mismatch under adaptive lock. Mitigation: canonical→adaptive order and rechecks.
6. **Golden scenario drifts.** Sign: vector or decision mismatch. Mitigation: committed full serialized golden fixture and literal `maintain` oracle.
7. **A live path is reached.** Sign: any tripwire invocation. Mitigation: local fixtures, pure functions, and construction-level tripwires.

## Execution gate

Implementation may begin only after this plan is separately approved. Before any target invocation, all unit/integration/E2E/observability tests above must pass locally, read-only preflight must match the seal, and a final review must verify the commands use only `dualcoachtest/virtual_customer`. Any mismatch stops; it is not repaired in place.
