# Diagnostic Isolation — Constructor Closure Plan

## Scope
Incorporates Stage 33 and changes only three construction contracts. User approved Ultragoal execution after consensus.

## RALPLAN-DR
Principles: durable identity differs from process identity; registered adaptive paths must be impossible to construct without paired canonical persistence; dormant control must not require an enabled coordinator. Drivers: restart safety, constructor-enforced invariants, testability. Chosen option is explicit typed factories and sealed bindings. Duck typing, ambient globals and optional production fallbacks are rejected.

## 1. Canonical transaction factory and enforcement
Add `CanonicalEventTransaction.for_customer_runtime(runtime: CustomerRuntime)` in `store.py`. `CustomerRuntime` exposes canonical resolved `customer_root`, `wizard_root`, and `nutrition_plans_root`; factory derives:
- events: `wizard_root/events.jsonl`
- sequence: `nutrition_plans_root/canonical-sequence.jsonl`
- lock: `wizard_root/.events.lock`
It validates containment/privacy/no symlinks and returns the only registered-customer transaction.

`WizardService.__init__(wizard_root, *, canonical_transaction=None, adaptive_registered=False)` enforces `adaptive_registered => canonical_transaction is not None` before creating `EventStore`. `NutritionCoachingCoordinator._configure_registry` builds the transaction from each enabled `CustomerRuntime` and passes both arguments. Customer admin activation/reconciliation and every adaptive wizard construction use the same factory. `EventStore` receives the transaction and registered finalization calls paired transaction append; if absent in a registered/adaptive flow it rejects before event bytes. Standalone nonregistered `WizardService`/`EventStore` retains event-only behavior.

Tests: registered customer cannot construct/finalize without transaction; exact paths/lock; coordinator/admin/reconcile all use factory; standalone unchanged; batch/single append paired; no nested flock.

## 2. Dormant delivery authority and transport factory
Add `DiagnosticDeliveryAuthority` independent of any coordinator. Constructor takes verified spec, boot epoch, profile session store, registry/artifact loaders and immutable adapter/test-bot identity. Methods:
- `verify_reservation(session_digest, generation, reservation_id, destination_digest, deadline)`
- `verify_provider_start(...)`
- `terminalize_cancel_or_timeout(...)`
Every method reloads current session/spec/owner/registry/bot/pins under the established admission/profile locks.

Add `TelegramCustomerTransport.for_diagnostic(adapter, verified_spec, authority)`. It requires the dormant adapter’s exact verified bot identity, exact customer destination from spec, sealed diagnostic method version, and native Telegram async sender. It does not require `NutritionCoachingCoordinator`. It returns a transport whose only diagnostic method is:
`async send_diagnostic_customer(body, *, reservation_id, session_digest, generation, deadline_monotonic, destination_digest)`.
The method delegates authority verification then calls the exact adapter bot with bounded connect/pool/write/read timeouts. Generic send compatibility is unavailable on this object.

Construction order: validate roots/spec/bot → create session store and `DiagnosticDeliveryAuthority` → create sealed diagnostic transport → create `DiagnosticHost` → create dormant `DiagnosticControlService` → register Topic-59 only. Activation later creates the child coordinator and injects the already frozen transport explicitly. No circular coordinator dependency.

## 3. Durable transport binding digest
Define canonical preimage:
`{schema_version:"diagnostic_transport_binding_v1", adapter_kind:"telegram-test-bot", test_bot_digest, customer_destination_digest, max_provider_timeout_seconds, method_version:"send_diagnostic_customer_v1", authority_spec_digest}`.
`diagnostic_transport_binding_digest = sha256(canonical_json(preimage))`.

The digest is computed before session prepare from verified durable inputs and stored in spec approval/config projection, every diagnostic session/transition/capability/reservation row, and preflight digest map. It never contains Python object identity. On restart the factory reconstructs authority/transport from the same verified spec and adapter identity and requires exact digest equality before Topic-59 prepare/revalidate. Changed bot/destination/timeout/method/spec invalidates old sessions.

`DiagnosticHost` additionally freezes the process-local transport reference and checks `transport is self._transport` under admission lock from reservation through provider completion. This is an extra runtime TOCTOU check only. Coordinator setters reject while diagnostic state is nonterminal.

Tests: digest vectors; same-value restart rebind succeeds only current boot revalidation; any preimage mutation rejects; object swap with same marker/digest rejects by identity; old process object is never serialized; dormant factory requires no coordinator; setter race provider zero.

## Pre-mortem and verification
- Missing transaction writes unpaired event: constructor/finalization rejection.
- Dormant transport accidentally needs coordinator: constructor test with no coordinator.
- Restart trusts object identity: digest-vector/reconstruction tests.
- Swap after validation: frozen-reference admission test.

Run full profile pytest+compileall; focused gateway tests+py_compile including diagnostic sibling; full gateway truthfully; wheel/policy and read-lock/race tests from Stage 33; cleaner/Architect/QA/Critic. No live Telegram, credentials, manual P2–P6 or deployment.

## Acceptance
All registered adaptive wizard events are paired by factory construction; dormant host builds without coordinator; durable binding is canonical/restartable and process identity is only an additional check; all Stage-33 contracts remain unchanged.

## Intent reconciliation
No new user choice: this only makes the previously approved isolated diagnostic design constructible and restart-safe.

## ADR
Use typed factories and a durable binding digest; reject optional production fallbacks and process-object persistence.

# Revision 35 Binding Addendum
This addendum resolves every Stage-34 finding.

## Sealed registered versus standalone construction
Add frozen `RegisteredCustomerBinding` to `customer_coaching.py`, created only by committed/diagnostic registry loaders with a module-private sentinel. Fields: customer key digest, resolved data-root digest, registry digest/version, activation digest, mode, binding digest, and nonserializable private sentinel. `CustomerRuntime` from a registry always carries it.

`WizardService` and `EventStore` have no permissive public constructor defaults. Their internal constructors require a module-private construction token. Public factories are disjoint:
- `WizardService.for_registered(runtime: CustomerRuntime)` requires and validates `RegisteredCustomerBinding`, builds `CanonicalEventTransaction.for_customer_runtime(runtime)`, and constructs registered EventStore.
- `WizardService.for_standalone(home)` explicitly creates event-only standalone mode and rejects a `CustomerRuntime`/registered binding.
- `EventStore.for_registered(transaction, binding)` and `EventStore.for_standalone(home)` mirror this split.

Direct `WizardService(home)`, `EventStore(home)`, omitted transaction/binding, forged binding, or standalone factory passed a registered runtime reject before opening/appending bytes. Every production registered callsite in gateway coordinator, wizard handlers, customer admin, reconciliation, activation and tests uses `for_registered`. Explicit standalone tools/tests use `for_standalone`. Loader absence/corruption rejects before service construction. Tests assert direct bare construction and omitted binding cannot append, while standalone remains compatible through its explicit factory.

## Exact diagnostic delivery bridge
Add typed `DiagnosticDeliveryRequest(session_digest, generation, reservation_id, destination_digest, transport_binding_digest)`; it contains no body. Add frozen `VerifiedDiagnosticReservation` containing canonical reserved body bytes, body digest, exact destination binding, reservation row digest, receipt pin, session/generation/binding digests, and a module-private verification token.

`DiagnosticDeliveryAuthority` exact methods, all requiring the host’s private admission-lock token:
- `verify_reservation(request, *, lock_token) -> VerifiedDiagnosticReservation`: reload session/spec/owner/registry/reservation, require active/current generation/binding, verify reservation row and immutable body/body digest, and return canonical reserved bytes. Missing/stale/conflict raises `DiagnosticAuthorityError` before provider.
- `verify_provider_start(verified, *, deadline_monotonic, lock_token) -> VerifiedDiagnosticReservation`: revalidate every live pin, exact reservation/body/destination/binding and positive deadline; return the same verified object only if unchanged.
- `terminalize_cancel_or_timeout(verified, *, receipt, reason, lock_token) -> Mapping`: append exactly one linked unknown when receipt absent or audit-pending when receipt present; idempotently return existing exact terminal, conflict fails closed.

Add `DiagnosticHost.deliver(request)`. While holding `_admission_lock`, it calls verify_reservation, creates/validates durable admission as required by Stage 33, calls verify_provider_start, then calls the frozen transport directly with `VerifiedDiagnosticReservation`—never the ordinary `send_adaptive_customer` discovery branch. Transport signature:
`async send_diagnostic_customer(verified: VerifiedDiagnosticReservation, *, deadline_monotonic: float) -> Mapping`.
It sends only `verified.body_bytes` to `verified.destination_binding`; caller-supplied body/destination is impossible. Host persists receipt/audit or uses terminalize method on timeout/cancellation before releasing lock. Activated child coordinator delegates diagnostic send to `DiagnosticHost.deliver`; generic customer transport path is prohibited in diagnostic mode.

Integration tests cover exact successful argument propagation; stale reservation/generation/body/destination/authority/binding provider zero; cancellation/timeout terminal once; close race; ordinary generic sender never called.

## Acyclic digest schema
Amend `DiagnosticIsolationSpecV1` with fields `spec_core_digest` and `diagnostic_transport_binding_digest`. Canonical hash order:
1. `spec_core_preimage` = all durable spec inputs except `spec_core_digest`, `diagnostic_transport_binding_digest`, `spec_digest`, `authority_digest`, and approval metadata. `spec_core_digest=SHA256(canonical_json(spec_core_preimage))`.
2. transport preimage = `{schema_version:"diagnostic_transport_binding_v1", adapter_kind:"telegram-test-bot", test_bot_digest, customer_destination_digest, max_provider_timeout_seconds, method_version:"send_diagnostic_customer_v1", spec_core_digest}`. Hash to `diagnostic_transport_binding_digest`.
3. `spec_digest=SHA256(canonical_json({spec_core_digest, diagnostic_transport_binding_digest}))`.
4. `authority_digest=SHA256(canonical_json({spec_digest, approved_by, approved_at_kst, supersedes_digest}))`.

The owner-approved spec/config projection stores all four digests and exact schema versions. Every session/transition/capability/reservation stores spec, authority and transport-binding digests. On restart the transport factory recomputes steps 1–4 from verified bytes and current adapter identity, requiring exact equality before revalidation. Python object identity is never serialized; `is` remains an additional process-local check.

Golden vector tests parse fixed raw spec bytes and independently recompute all four digests, reject field/exclusion/order mutation, and prove no cycle or object representation enters canonical JSON.

## Acceptance additions
- Registered and standalone persistence are reachable only through disjoint sealed factories; no optional fallback exists.
- Diagnostic provider receives only authority-returned reservation-pinned bytes/destination through `DiagnosticHost.deliver`.
- Digest derivation is ordered, acyclic, byte-reproducible and restart-safe.


# Revision 36 Binding Addendum
This addendum resolves the sole Stage-35 Architect finding.

## Host-owned reserve-and-verify sequence
Replace `DiagnosticDeliveryRequest` with `DiagnosticDeliveryCandidate`, created only by the activated child coordinator from the exact latest approved/activated proposal and persisted rendered body. Fields: session/spec/transport-binding digests, generation, proposal/revision/body/destination/config/registry/consent/activation/source/registration/policy/catalog/constraints/epoch pins, dedupe key, and canonical body bytes+digest. It is not a reservation and grants no provider authority.

Replace `verify_reservation` with:
`DiagnosticDeliveryAuthority.reserve_and_verify(candidate, *, lock_token) -> VerifiedDiagnosticReservation`.

While `DiagnosticHost.deliver(candidate)` holds `_admission_lock`, `reserve_and_verify`:
1. reloads active session/generation/spec/owner/registry and every authority/artifact/proposal pin;
2. recomputes persisted rendered body digest and destination binding, rejecting any candidate mismatch before rows;
3. under the existing adaptive store lock appends the immutable `delivery_attempt_started` reservation with all pins/body digest and obtains reservation ID;
4. rereads and validates the exact committed reservation;
5. returns `VerifiedDiagnosticReservation` containing only reservation-pinned canonical bytes/destination and private verification token.

No reservation can be created outside this method in diagnostic mode. The ordinary coordinator reservation branch is bypassed; activated diagnostic coordinator calls only `DiagnosticHost.deliver(candidate)`. If an exact terminal/consumed reservation already exists, method returns the typed duplicate/no-send result and never creates provider authority. Conflicting same dedupe key fails closed.

`DiagnosticHost.deliver` order under one admission lock is exact: `reserve_and_verify` → `verify_provider_start` → frozen transport `send_diagnostic_customer(verified, deadline)` → persist receipt/delivered/audit terminal; timeout/cancellation uses `terminalize_cancel_or_timeout`; finally release lock. Close/expiry/stop uses the same lock. Therefore close-before-reserve yields all delivery/provider deltas zero; reserve-first is a pre-detach attempt and detaching cannot append until its terminal row is durable.

Tests deterministically cover: candidate validation failure/no row; close wins before reserve/no row/provider; reserve wins then close waits; crash immediately after reservation/restart no resend; duplicate candidate no row/provider delta; body/destination/pin mismatch before reservation; exact successful one provider chain.


# Revision 37 Binding Addendum
This addendum resolves every Stage-36 Critic finding.

## Closed reservation decision algebra
`reserve_and_verify(...)` returns the sealed union:
- `VerifiedDiagnosticReservation` — fresh provider authority;
- `DiagnosticDuplicateNoSend` — stable fields `status="duplicate"`, existing terminal/status digest, Korean/operator outcome, provider_authority=false;
- `DiagnosticUnknownNoSend` — stable fields `status="delivery_unknown"`, linked terminal row digest, provider_authority=false.

`DiagnosticHost.deliver` pattern-matches immediately. Only `VerifiedDiagnosticReservation` proceeds to `verify_provider_start` and transport. Duplicate/unknown returns the stable result with no provider and no later branch.

## Existing dedupe-state matrix
Under admission+adaptive store locks:
- no row: validate, append one started reservation, reread, return Verified;
- exact unconsumed `delivery_attempt_started`: append/reuse exactly one linked `delivery_unknown` reason `diagnostic_started_without_provider_receipt`, return DiagnosticUnknownNoSend; never recreate authority;
- exact consumed row without receipt/terminal: append/reuse linked unknown, return UnknownNoSend;
- exact receipt/delivered/audit-pending/sent-audited/unknown terminal chain: return DiagnosticDuplicateNoSend using canonical existing status; no row/provider delta except reconciliation is available only through its separate capability/action;
- any same-dedupe body/destination/session/generation/spec/binding/pin conflict: raise `DiagnosticReservationConflict`, no new row/provider.

Startup recovery runs before diagnostic revalidation and applies the same matrix to all exact started/consumed rows, terminalizing uncertain rows unknown with provider zero. Therefore restart can never convert persisted reservation evidence into provider authority.

Tests cover every matrix row with exact row/provider deltas, restart ordering, and stable caller-visible result text/status.

## Mechanical host-only reservation ownership
`RegisteredCustomerBinding.mode` distinguishes ordinary versus `diagnostic_isolated_v1`. At the first line of public ordinary `AdaptiveNutritionCoordinator.deliver_latest_once` and every ordinary reservation helper, a diagnostic-bound runtime raises `AdaptiveWorkflowError("diagnostic delivery requires DiagnosticHost")` before store lock/append/provider. No compatibility fallback or raw capability bypass applies.

The diagnostic host uses a separate private `_deliver_diagnostic_candidate(candidate, host_lock_token)` path that validates the module-private host token and calls only `reserve_and_verify`. It is not exported. Direct invocation of ordinary delivery/reservation against diagnostic runtime is tested for zero session-external lifecycle/delivery rows and provider zero; forged host token rejects. Code search/AST tests assert `delivery_attempt_started` for diagnostic mode is emitted only from `DiagnosticDeliveryAuthority.reserve_and_verify`.

## Acceptance additions
Fresh authority is created once only; persisted started/consumed evidence is terminal no-send; duplicate/terminal evidence is stable no-send; ordinary public delivery cannot reserve for diagnostic runtimes.


# Revision 38 Binding Addendum
This addendum resolves the sole Stage-37 Critic finding.

## Canonical terminal no-send variants
The sealed union is:
- `VerifiedDiagnosticReservation` (`provider_authority=true`);
- `DiagnosticUnknownNoSend(status="delivery_unknown", text="전송 결과를 확인할 수 없습니다. 다시 보내지 마세요. 조정이 필요합니다.", reconciliation_available=false)`;
- `DiagnosticAuditPendingNoSend(status="audit_pending", text="고객 전송 영수증은 확인됐습니다. 재전송하지 말고 감사 기록을 복구해 주세요.", reconciliation_available=true)`;
- `DiagnosticDuplicateNoSend(status="duplicate", text="이미 처리된 전송입니다.", reconciliation_available=false)`.
All no-send variants have provider_authority=false.

Validated chain precedence:
1. A valid chain containing `sent_audited` returns DuplicateNoSend, even if earlier rows include receipt/delivered/audit_pending.
2. Otherwise a valid chain with provider receipt, delivered, or audit_pending returns AuditPendingNoSend; reconciliation capability is the only allowed next mutation and provider stays zero.
3. Otherwise an existing `delivery_unknown` returns UnknownNoSend.
4. Otherwise exact started/consumed without receipt is terminalized once to unknown and returns UnknownNoSend.
5. Receipt evidence mixed with unknown without the canonical audit-pending normalization, multiple conflicting receipts/terminals, or invalid ordering fails closed as `DiagnosticReservationConflict`, no row/provider.

`DiagnosticHost.deliver` returns these variants unchanged before provider-start branching. Reconciliation transforms a valid audit-pending chain to `sent_audited`; the next repeated send returns DuplicateNoSend. It never transforms unknown into send permission.

Matrix tests assert exact result class, status, Korean text, row delta, provider delta and reconciliation availability for started, consumed, unknown, receipt-only, delivered-without-audit, audit-pending, audit-pending→sent-audited, and direct sent-audited chains across same-process replay and restart recovery.
