Files

286 lines
54 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Domain and application refactoring contract
Status: integrated architecture map and handoff after M26. M20–M26 moved the
responsibilities below to their current owners. This document records verified
source ownership and behavioral proof; it does not authorize behavior,
file-format, dependency, or product changes. M27 review and acceptance remain
pending.
## Authority and boundaries
The server configuration is authoritative for server peers and their managed
descriptive metadata. `ConfigDocument` preserves its bytes and exposes every
peer section, even when a section has no usable public key or cannot be
represented by the supported client model. `ClientStates` is a restricted
projection used for client operations; it is not a second peer registry. The
vault is authoritative for encrypted client secrets, and the saved files are
authoritative for persisted state. Runtime reads are observations, not desired
configuration. Historical successful verification is not current verification.
The dependency direction remains:
```text
TUI -> application facade / application read model -> domain rules
| -> config document/editor
| -> vault/import/delivery
| -> backup/persistence
| -> runtime/service adapters
```
Domain rules do not read files, parse server configuration, perform cryptography,
call the runtime/service, or render terminal text. The application coordinates
these owners. The TUI submits typed requests and displays secret-free facts; it
does not authorize business operations or parse saved configuration. The
document editor never rebuilds the server file from a client snapshot.
## Reading guide: common workflows
These paths identify the application entry, the rule owner, the side-effect
boundary, and tests that exercise failures as well as success.
### Create, Commit, first delivery, explicit restart
- **Entry:** TUI `UiCommand::CreateClient`, `Commit`, `DeliverFirstIssue`, and
`PreviewRestart` followed by the explicit restart choice; the adapter is
`src/tui/events/executor.rs`. The facade methods are
`Application::prepare_new_client`, `commit`, `deliver_first_issue_file`,
`preview_restart`, and `restart`.
- **Rule owners:** `app/draft.rs` coordinates the draft; `app/commit.rs` owns
Commit; `app/first_delivery.rs` owns the one-use session capability and
retries; `app/first_file.rs` owns first-file output; `app/restart.rs` owns
backup, job completion, comparison and durable apply publication. The app
read model reports eligibility; the TUI does not grant it.
- **Effect boundary:** creation changes memory only. Commit backs up and installs
saved files/vault, without a runtime call. First delivery writes the selected
client file and may display its QR. Only the later explicit restart may call
the service/runtime adapters; addresses and current applied knowledge change
only after complete verification and durable bookkeeping.
- **Failure proof:** `app::new_client_preparation_is_transactional_and_uncommitted_keys_cannot_issue`,
`app::commit_failure_keeps_pending_issue_and_successful_retry_authorizes_it`,
`app::failed_file_publication_can_retry_in_a_different_directory`,
`app::restart_backup_failure_preserves_draft_and_submits_no_job`,
`app::restart_mismatch_and_post_restart_write_failure_never_release_addresses`.
### Delete, Commit, verified address release
- **Entry:** `UiCommand::DeleteClient`, then `Commit`, then `PreviewRestart`
and a confirmed restart choice through the executor; facade methods are
`Application::delete_client`, `commit`, and `restart`.
- **Rule owners:** `app/draft.rs` stages peer removal; `ConfigDocument` in
`wireguard/document.rs` and `wireguard/edit.rs` owns peer identity and
byte-preserving edits; `domain.rs` owns pending/committed reservations and
allocation; `app/commit.rs` persists the deletion; `app/restart.rs` publishes
release only after `runtime.rs` verifies the full saved peer set and durable
state install succeeds.
- **Effect boundary:** delete is draft-only. Commit backs up and changes saved
files/vault but leaves the running interface unchanged. The address remains
reserved through any restart failure or mismatch and is released only after
verified publication.
- **Failure proof:** `app::deletion_of_unprojectable_peer_persists_its_exact_host_route_reservation`,
`app::deletion_of_unprojectable_peer_with_broad_route_fails_without_draft_change`,
`commit_restore::backup_failure_blocks_commit_and_restore_without_losing_drafts_or_saved_bytes`,
`app::restart_mismatch_and_post_restart_write_failure_never_release_addresses`.
### Preview and confirm restore
- **Entry:** TUI `UiCommand::PreviewRestore`, then `ConfirmRestore` or cancel;
`src/tui/events/executor.rs` binds the preview to its worker generation.
Facade methods are `Application::preview_restore` and `confirm_restore`.
- **Rule owners:** `app/restore.rs` validates the archive, rebuilds internal
reservations and coordinates confirmed replacement; `backup.rs` owns archive
validation/encoding; `persistence.rs` owns safe installation and recovery.
The selected archive's settings, configuration and vault remain authoritative
for restored saved state.
- **Effect boundary:** preview and cancel are read-only. Confirmation requires a
fresh precautionary backup, then replaces saved files/vault as a whole. It
resets current apply knowledge to Unknown and does not call the running VPN or
request a vault password.
- **Failure proof:** `app::preview_is_read_only_and_confirm_restores_without_unlocking_vault`,
`app::restore_accepts_changed_non_path_settings_and_rejects_changed_paths`,
`app::restore_does_not_reserve_unprojectable_exact_hosts_owned_by_restored_document`,
`commit_restore::backup_failure_blocks_commit_and_restore_without_losing_drafts_or_saved_bytes`,
`events::prepared_delivery_and_restore_confirmations_are_bound_to_the_reviewed_generation`.
## Current concepts and migration map
References use current repository-relative module paths. The M19.1 map is
resolved below against the integrated M20–M26 implementation. Test names point
to existing behavioral coverage; this documentation update does not claim those
tests were rerun.
### Application session fields and workflows
| Current type / field / function | Business meaning | Authority | Current owner | Readers / writers | Existing proof | Resolution |
| --- | --- | --- | --- | --- | --- | --- |
| `Application::store` (`src/app.rs`) | Saved-file boundary, configured paths, validation and install capability | `SavedStore` paths and installed files | Application facade; scenario owners in `app/commit.rs`, `app/restore.rs`, `app/backups.rs`, `app/restart.rs` | Read by application scenarios; saved files change only through saved-operation owners | `commit_restore::backup_failure_blocks_commit_and_restore_without_losing_drafts_or_saved_bytes`; persistence `complete_install_publishes_components_with_private_modes` | Resolved M23/M25 |
| `Application::saved` (`src/app.rs`) | Exact installed generation held by this session | `SavedBytes` read from `SavedStore` | Application session; bound contexts in `app/restore.rs` and `app/restart.rs` | Open/read, Commit, restore, verified bookkeeping, restart preview | `app::commit_advances_saved_state_without_applying_and_backup_contains_old_bytes`; `app::successful_restore_clears_verified_current_history_but_backup_retains_it` | Resolved M23/M25 |
| `Application::draft` (`src/app.rs`, `src/app/draft.rs`) and its document | Coordinated in-memory document, client projection, vault, and draft-only reservations | Authoritative server document plus encrypted vault; `ClientStates` is a restricted projection | `app/draft.rs` stages and publishes coordinated replacements; `wireguard/edit.rs` owns byte-preserving edits | Draft operations prepare replacements; Commit serializes the staged values | `app::new_client_preparation_is_transactional_and_uncommitted_keys_cannot_issue`; `config_preservation::edits_keep_interface_and_unowned_bytes_exact_and_can_be_reparsed` | Resolved M22 |
| `Application::states` (`src/domain.rs`) | Narrow draft/saved management snapshots, current apply knowledge, and address reservations | Document projection for manageable clients plus persisted internal state | Domain `ClientStates` and `AddressReservations`; persistence only encodes/decodes durable fields | App draft/Commit/restart/reopen; read model exposes secret-free facts | `domain::discard_restores_draft_and_commit_keeps_applied_snapshot_separate`; `domain::successful_verified_apply_releases_deleted_addresses_and_mismatch_does_not`; `persistence::persisted_history_never_marks_the_runtime_verified` | Resolved M20–M22 |
| `Application::draft.vault` (`src/app/draft.rs`) | Encrypted vault draft used for secret encryption, import, deletion, and reissue | Installed vault bytes at open, then current app draft | `app/draft.rs` coordinates; cryptographic/value operations stay in `vault.rs` and `vault/import.rs` | App setup/import/create/delete/Commit/reissue | `app::selected_file_import_uses_the_existing_vault_draft_without_saving`; `app::deletion_only_removes_selected_current_record_and_retains_other_records`; `vault::encrypted_bundle_round_trips_without_unlocking_and_retrieves_with_password` | Resolved M22/M24 |
| Pending exact-host reservations (`ClientStates::address_reservations`, `src/domain.rs`) | Draft-only exact host addresses retained when a saved peer cannot project to `Client` | Saved document peer routes and pending deletion | Domain `AddressReservations`, coordinated by `app/draft.rs` | Delete adds; Commit persists/promotes; discard clears | `app::deletion_of_unprojectable_peer_persists_its_exact_host_route_reservation`; `app::deletion_of_unprojectable_peer_with_broad_route_fails_without_draft_change` | Resolved M21/M22 |
| `historical_apply` | Last successfully persisted verification timestamp/status, for archive/history display | `PersistedState.last_verified_at_utc` and archive manifest | Persistence codec owns representation; application read model presents it | Open/backup/restore/restart bookkeeping | `persistence::persisted_history_never_marks_the_runtime_verified`; `app::verified_history_persists_across_reopen_without_marking_runtime_applied`; `app::historical_verified_timestamp_does_not_suppress_reissue_warning_after_reopen` | Resolved M21/M23/M26 |
| `verified_saved_projection` | Saved generation verified by the current session, used for delivery activation warning | Successful full runtime comparison bound to saved bytes | Application session/restart scenario; read-only view exposes only safe status | Restart publication writes; delivery warning reads; edits/restore invalidate as appropriate | `app::first_issue_warning_tracks_verified_saved_generation_for_file_and_qr`; `app::historical_verified_timestamp_does_not_suppress_reissue_warning_after_reopen` | Resolved M21/M24–M26 |
| `active_restart_attempt`, `next_restart_attempt` | One-use authorization that binds verification publication to the confirmed attempt and saved generation | Application-created attempt ID plus saved-byte equality | Private `app/restart.rs` scenario context | Preview/confirm/restart lifecycle; verified persistence consumes it | `app::restart_confirmation_rejects_a_generation_changed_after_preview`; `app::restart_mismatch_and_post_restart_write_failure_never_release_addresses` | Resolved M20/M25 |
| `FirstDeliverySession` (`src/app/first_delivery.rs`) | Creation-session-only first-delivery eligibility, one-use authorization and retry state | Generated key held in memory, committed peer directives and encrypted record binding | `app/first_delivery.rs` owns session capability state; `app/first_file.rs` runs first-file output; vault owns ciphertext/crypto and delivery adapters own publication | Coordinated creation registers; Commit promotes; file delivery consumes or updates; delete/discard/invalidation removes | `app::first_issue_authorization_is_single_flight_and_success_consumes_it`; `app::deletion_and_discard_revoke_only_session_bound_initial_material`; `app::uncertain_first_file_publication_retains_warning_and_skips_qr` | Resolved M20/M22/M24 |
| `open_from_saved_store`, `open` (`src/app.rs`) | Construct a session from installed saved bytes; injected-state `open` exists for fixture/state-seeding callers. Reopen is Unknown regardless of history. | Installed files/document; persisted reservations/history | Application session constructors | Production startup uses `open_from_saved_store`; `open` is still called by library unit and integration fixtures | `startup::management_session_loads_supported_state_and_keeps_unsupported_peers_visible`; `app::verified_history_persists_across_reopen_without_marking_runtime_applied` | Resolved M22; retained fixture API, see caller audit |
| `prepare_new_client`, `add_client`, `rename_client`, `delete_client`, `discard_draft` (`src/app.rs`) | Stage client/document/vault changes without saved-file or runtime effects; `add_client` is a legacy fixture helper and does not issue a key/vault secret | Document plus restricted client projection and vault | `app/draft.rs`, application facade and domain/editor/vault owners; no separate `app/clients.rs` was needed | TUI uses prepared create/rename/delete/discard; test fixtures still call `add_client` | `app::new_client_preparation_is_transactional_and_uncommitted_keys_cannot_issue`; `client_delivery::committed_creation_retries_after_overwrite_decline_and_duplicate_aliases_are_independent`; `client_delivery::deletion_and_restore_remove_password_free_authority_and_reopen_requires_vault_password` | Resolved M22; retained fixture helper, see caller audit |
| `commit`, `preview_restore`, `confirm_restore` (`src/app.rs`) | Commit atomically installs staged saved bytes after backup; restore is a preview-bound full saved-state replacement after backup | App draft for Commit; validated archive payload for restore | `app/commit.rs`, `app/restore.rs`; low-level formats/install remain in `backup.rs`/`persistence.rs` | TUI executor calls facade; store installs only with `BackupComplete` | `app::commit_advances_saved_state_without_applying_and_backup_contains_old_bytes`; `app::preview_is_read_only_and_confirm_restores_without_unlocking_vault`; `commit_restore::backup_failure_blocks_commit_and_restore_without_losing_drafts_or_saved_bytes` | Resolved M23 |
| `preview_restart`, `restart`, verified-result publication (`src/app.rs`, `src/app/restart.rs`) | Confirmed restart scenario: preconditions, backup, service job completion, runtime comparison, durable status then publish applied knowledge | Saved generation + complete `SavedPeerProjection` + runtime observation | `app/restart.rs`; service submission in `service.rs`; comparison in `runtime.rs`; durable fields in persistence | TUI previews/confirms; fixture apply callback is `#[cfg(test)]`; success updates state only after durable write | `app::confirmed_restart_orders_backup_job_read_and_durable_publication`; `app::service_timeout_reports_submission_and_may_still_complete`; `app::failed_verified_history_write_does_not_advance_memory_or_reopen_as_verified` | Resolved M20/M21/M25 |
### Domain state and transitions
| Current type / field / operation | Business meaning | Authority | Current owner | Readers / writers | Existing proof | Resolution |
| --- | --- | --- | --- | --- | --- | --- |
| `Client` / `ClientAddresses` / `Snapshot` (`src/domain.rs`) | Public, manageable projection keyed by public key; aliases may repeat, IPv6 may be absent for legacy clients | Public key and supported host-address projection from document | Domain value types in `domain.rs`; no child module split was needed | App operations and read model; draft/saved snapshot transitions | `domain::aliases_may_repeat_but_control_characters_are_rejected`; `domain::existing_ipv4_only_clients_remain_ipv4_only`; `wireguard::wireguard_projection_supports_ipv4_only_and_rejects_ipv6_only_or_default_routes` | Resolved M20/M21 |
| `ClientStates::draft` / `saved` (`src/domain.rs`) | Manageable-client snapshots for staged and committed state, not all server peers | Document is source; snapshots are derived | Domain client states, mutated only through narrow transitions | Application edits draft and Commit advances saved; TUI consumes read-model facts | `domain::discard_restores_draft_and_commit_keeps_applied_snapshot_separate`; `app::commit_advances_saved_state_without_applying_and_backup_contains_old_bytes` | Resolved M21/M22 |
| `applied_snapshot` / `ApplyStatus` | Optional management snapshot last verified plus current knowledge; may be `None` while full technical verification is Verified for valid unmanageable peers | Current verified runtime comparison; historical timestamp is separate | Domain apply knowledge; technical evidence remains runtime-owned | Restart publication changes; reopen resets current knowledge; read model maps current knowledge | `app::restart_verifies_metadata_free_legacy_routes_without_partial_applied_snapshot`; `app::verified_history_persists_across_reopen_without_marking_runtime_applied` | Resolved M20/M21 |
| `reserved_addresses` and `restore_persisted_reservations` (`src/domain.rs`) | Persisted conservative addresses that cannot be allocated | Prior saved peer routes and internal state | `domain.rs` owns reservation value/rules; `persistence.rs` only encodes/decodes | Allocator reads; Commit/reopen/restart update; restart success may release | `domain::a_draft_only_deletion_does_not_create_a_committed_reservation`; `domain::successful_verified_apply_releases_deleted_addresses_and_mismatch_does_not`; `persistence::state_codec_is_canonical_and_rejects_bad_or_unsupported_state` | Resolved M21 |
| Narrow `stage_client`, `remove_draft_client`, `commit_draft`, `discard_draft` (`src/domain.rs`) | Mutate the manageable draft projection through scoped operations and preserve saved/applied separation | `ClientStates` snapshots, ultimately document-derived | Application draft coordinator calls crate-private stage/remove and public commit/discard transitions | No unrestricted `draft_mut` accessor remains; direct mutation in domain tests is limited to transitions | `domain::discard_restores_draft_and_commit_keeps_applied_snapshot_separate`; `app::discard_restores_draft_without_writing_installed_bytes`; `app::commit_failure_keeps_pending_issue_and_successful_retry_authorizes_it` | Resolved M21/M22; escape hatch removed |
| Verified apply, technically verified apply, runtime unknown/unavailable transitions (`src/domain.rs`) | Change present apply knowledge only after verified result publication; preserve current uncertainty and optional management projection | Complete runtime comparison plus successful persisted bookkeeping | Domain transitions are crate-private and called by the application restart scenario | App restart success/failure/reopen; statistics reads cannot call transitions | `domain::successful_verified_apply_releases_deleted_addresses_and_mismatch_does_not`; `app::restart_mismatch_and_post_restart_write_failure_never_release_addresses`; `tui::statistics_refresh_is_generation_bound_and_never_changes_applied_state` | Resolved M20/M21 |
| `allocate_ipv4`, `allocate_ipv6`, `AddressAvailability` (`src/domain.rs`) | Pure address allocation against server, occupied and reserved sets | Validated settings and document-derived address sets | Domain address allocator and reservations | App create reads | `domain::ipv4_allocation_skips_server_occupied_reserved_network_and_broadcast`; `domain::ipv6_allocates_first_free_address_from_a_very_large_range`; `tui_workflow::create_form_submits_automatic_dual_stack_client_without_changing_saved_state` | Resolved M21 |
### Peer and runtime projections
| Current projection | Business meaning / authority | Current owner and consumers | Existing proof | Resolution |
| --- | --- | --- | --- | --- |
| `PeerInspection`, `PeerIssue`, `DocumentIssue`, `ConfigDocument` (`src/wireguard/document.rs`) | Every authoritative server peer section, byte spans, parsed routes and anomalies. Identity can be absent or duplicated. Malformed/no-key peers remain inspectable by document position. | `wireguard/document.rs`; app editing/deletion/restart and application read model consume it | `wireguard::wireguard_peer_issues_report_missing_malformed_and_duplicate_identity_fields`; `tui::malformed_peer_can_be_inspected_by_document_location_without_mutation_identity`; `tui::duplicate_public_keys_remain_inspectable_by_peer_position_but_not_mutable` | Resolved M22/M25/M26 |
| `ClientDisplay`, `SessionDisplay`, `ApplicationView` (`src/app/read_model.rs`) | Secret-free current draft-document rows and authoritative per-action eligibility; mutable identity only for an unambiguous manageable peer | App-owned read model; `Application` facade exposes it | `tui::enter_opens_an_unsupported_peer_for_inspection_without_mutation_identity`; `tui::issues_screen_hides_valid_address_shape_limits_but_keeps_genuine_errors` | Resolved M26 |
| `SessionView`, `ClientStateRow`, `VisibleRecord` (`src/tui/state.rs`) | Maps `ApplicationView` into UI navigation state; keeps generation, selection, screens and rows, without reparsing saved bytes or constructing authority | TUI state consumes app facts and owns UI selection/navigation | `tui_workflow::saved_peer_projection_counts_legacy_peers_without_inventing_managed_state`; `tui::keyboard_navigation_reaches_draft_removed_rows_without_authorizing_them`; `tui::refresh_preserves_selection_by_public_key_not_alias_or_row_position` | Resolved M26 |
| Renderer-derived labels and issue text (`src/tui/render.rs`) | Styles and renders app facts; no operation eligibility or peer authority is inferred from labels | Renderer owns visual strings, styling, wrapping, geometry and layout | `tui::persistent_details_follow_the_highlight_and_keep_geometry_fixed`; `tui::client_rows_show_draft_state_and_escape_anomalous_unsupported_peers`; `tui::issues_screen_hides_valid_address_shape_limits_but_keeps_genuine_errors` | Resolved M26 |
| `SavedPeerProjection`, `RuntimeObservation`, `TechnicalMatch` (`src/runtime.rs`) | Complete technical route set independent of manageable `Client`; valid subnet-route peers can be restart-verified without UI mutability. Handshakes/counters are not apply evidence. | Runtime owns projection/comparison and read-only transport; app restart scenario coordinates; read model exposes only safe status/findings | `runtime::projection_keeps_valid_unsupported_routes_and_ipv4_only_peers`; `runtime::counters_and_handshake_changes_do_not_change_technical_match`; `app::restart_verifies_metadata_free_legacy_routes_without_partial_applied_snapshot` | Resolved M20/M25/M26 |
### Authorization, prepared contexts and worker generations
| Current context | Meaning and authority | Current owner | Readers / writers | Existing proof | Resolution |
| --- | --- | --- | --- | --- | --- |
| UI selection guard `UiState::selected_client_mutable` (`src/tui/state.rs`) | Presentation-level guard requires one matching row, matching document position if selected, and a mutable identity; cannot grant operation authority | TUI selection state only; application owns typed per-action eligibility and execution revalidation | Terminal key handlers and footer hints read it; app methods/editors independently reject invalid actions | `tui::selection_uses_peer_position_for_duplicate_keys_and_disables_mutation`; `tui::removed_saved_client_is_visible_and_never_mutable` | Resolved M26 |
| Per-action UI prerequisites (`src/tui/terminal.rs`, `src/app/read_model.rs`) | Selection is presentation-only; rename/delete/delivery/reissue availability comes from typed app eligibility, and each operation revalidates on execution | App read model owns availability facts; terminal maps them to actions and hints | `tui::state` binds selected row; app methods and config editor validate mutation | `tui::detail_hints_only_offer_saved_delivery_for_mutable_saved_selection`; `app::deletion_of_unprojectable_peer_persists_its_exact_host_route_reservation`; `app::deletion_of_unprojectable_peer_with_broad_route_fails_without_draft_change` | Resolved M26 |
| First-issue session context (`src/app/first_delivery.rs`, `src/app/first_file.rs`) | Session-local secret and one-use authorization bound to identity, peer directives and encrypted record; retry state reflects publication/cleanup uncertainty | `app/first_delivery.rs` owns session lifecycle; `app/first_file.rs` owns first-file scenario; adapters own crypto and publication | Create registers; Commit enables; delivery validates/consumes; edits/delete/discard invalidate | `app::committed_first_issue_publishes_selected_file_and_consumes_eligibility`; `app::published_file_is_reported_when_saved_store_bookkeeping_fails`; `app::failed_first_delivery_requires_consuming_capability_before_retry`; `app::temporary_cleanup_failure_requires_manual_cleanup_before_first_issue_retry` | Resolved M20/M24 |
| `ApplicationExecutor` prepared destination and restore preview (`src/tui/events/executor.rs`) | Worker-owned continuation state bound to target, delivery mode/QR choice, attempt, preview and request generation | Private named contexts in the executor child | Executor command handling; clears on unrelated actions, consumes on confirmation/cancel | `events::prepared_delivery_and_restore_confirmations_are_bound_to_the_reviewed_generation`; `client_delivery::overwrite_confirmation_is_bound_to_the_selected_destination`; TUI workflow restore cancellation/confirmation | Resolved M20; both contexts use named structs |
| `PendingOperation`, `SessionView.generation`, worker generation (`src/tui/events.rs`, `src/tui/state.rs`) | Monotonic identity for submitted work and returned view; stale view/statistics cannot overwrite current UI state | TUI worker/execution adapter only; not domain state | Mutating submit increments; worker tags result; UI accepts matching generations; periodic statistics rechecks generation across read | `events::delayed_statistics_are_bounded_and_do_not_advance_generation`; `events::statistics_started_before_a_mutation_are_discarded_as_stale`; `tui::stale_view_is_rejected_and_unsupported_selected_peer_is_not_mutable` | Resolved M20/M26 |
| `FormController`, `SecretInput`, `SecretText`, `SensitiveCommand` (`src/tui/forms.rs`, `src/tui/events.rs`) | Short-lived form state and zeroizing password command; secrets excluded from view/debug/clone | Forms own input; worker carries secret command; application consumes only during vault operation | Form submits sensitive command; worker invokes setup/reissue; form clears on cancel/consumption | `tui::compact_form_hints_follow_the_active_form_without_leaking_input`; `vault::every_retrieval_requires_the_correct_password`; `client_delivery::reissue_unlocks_installed_saved_record_each_time_and_warns_after_reopen` | Resolved M24/M26; lifetimes preserved |
### Public API escape-hatch callers and removal conditions
| API | Actual callers | Safe path and timing |
| --- | --- | --- |
| `ClientStates::draft_mut` | No current declaration or callers. Former unrestricted accessor was replaced by crate-private `stage_client`/`remove_draft_client` and narrow transitions in `src/domain.rs`; full `rg` audit finds no production, test, startup, binary, integration or container-driver reference. | Removed in M22 after caller audit; no replacement is needed by callers. [M22 evidence](../evidence/milestone-22.md) records the review. |
| `Application::vault_draft_mut` | No current declaration or callers. The public mutation bypass was removed and no test-only API replaced it. | Removed in M22; vault state is mutated through coordinated application operations. [M22 evidence](../evidence/milestone-22.md) records the review. |
| `Application::add_client` (`src/app.rs`) | Unit-test setup in `src/app.rs`; integration-fixture setup in `tests/commit_restore.rs` and `tests/runtime_restart.rs`. No binary, startup, production module, `tests/support`, or `src/container_driver.rs` caller. `ConfigDocument::add_client` remains the actual byte-preserving editor used by `prepare_new_client` and preservation tests. | Retained as a public compatibility/test-seeding operation because current integration fixtures still use it. It does not generate/encrypt a private key and must not be mistaken for the normal create workflow. Removal requires deliberate fixture migration. |
| `Application::open(store, states)` (`src/app.rs`) | Unit tests and fixture helpers in `tests/client_delivery.rs`, `tests/commit_restore.rs`, `tests/runtime_restart.rs`, and `tests/tui_workflow.rs`. No binary/startup or production caller; no container-driver caller. | Retained for tests that seed `ClientStates` and apply knowledge. Production startup uses `open_from_saved_store`; fixture construction is the only current consumer class. |
| `Application::open_from_saved_store` (`src/app.rs`) | Binary startup through `startup::open_management_session`; application/TUI/integration tests; `container_driver::open_fixture_application` in `src/container_driver.rs`, which is included only under `#[cfg(test)]` in `src/lib.rs`. | Required production and fixture entry point; retain. Container driver normal tests are synthetic; its ignored Alpine scenario is opt-in and is not a systemd acceptance test. |
| `Application::display_projection` (`src/app.rs`) | No production-module, binary, or actual startup-path call. `startup::tests::management_session_loads_supported_state_and_keeps_unsupported_peers_visible` calls it after exercising session startup. There are no `tests/` integration-test calls. The only other direct callers are in `src/container_driver.rs`'s `#[cfg(test)]` synthetic lifecycle test `phase_protocol_keeps_creation_commit_and_first_delivery_in_one_app_session`; the ignored Alpine scenario does not call it. Current TUI presentation uses `Application::read_model`. | Retain for now as a public compatibility projection: the startup unit and synthetic fixture tests still exercise its inclusion of unsupported peers. Its compatibility value and test coverage do not make it a production authorization/read path. Removal requires deliberate migration of those callers and an API compatibility decision; do not infer it is obsolete solely because M26 moved TUI reads to `read_model`. |
| Former public `Application::create_pre_restart_backup` (`src/app.rs`) | Direct callers are confined to colocated app unit tests: helper `test_mark_verified`, `manual_and_pre_restart_backups_are_listed_without_runtime_calls`, and `failed_verified_history_write_does_not_advance_memory_or_reopen_as_verified`. No binary, integration-test, or container-driver caller. | Restricted to a private `#[cfg(test)]` method because only fixtures use it. Production restart performs its mandatory backup within `app/restart.rs`; no production API exception remains. |
## Transition tables
These tables preserve observed behavior. “No mutation” below means no protected
saved-file or runtime mutation; drafts may remain in memory where specified.
### Draft lifecycle
| Trigger | Preconditions | Next state | Side effects | Failure or cancellation | Existing proof |
| --- | --- | --- | --- | --- | --- |
| Open session | Saved settings/config validate; optional state/vault parse | Draft begins from saved document and client snapshot; current apply knowledge is Unknown | Read installed bytes, restore conservative reservations, load history separately | Invalid saved state rejects open; history never promotes current apply knowledge | `startup::management_session_loads_supported_state_and_keeps_unsupported_peers_visible`; `app::verified_history_persists_across_reopen_without_marking_runtime_applied` |
| Create client | Vault exists; alias/address inputs valid; configured ranges available | Document/client/vault drafts all contain new peer/record; first-issue material registers only after staging succeeds | Generate key, allocate/check dual stack, encrypt record, write no files | Any preparation failure leaves all live draft components and issue rights unchanged | `app::new_client_preparation_is_transactional_and_uncommitted_keys_cannot_issue`; `app::manual_addresses_reject_occupied_and_out_of_range_without_partial_staging` |
| Rename or delete | Target identity resolves to an authoritative unambiguous peer; deletion of unprojectable peer can reserve only exact host routes | Corresponding document and manageable snapshot edit; delete removes its vault record if present | Mutate in-memory drafts; retain exact unsupported routes pending Commit; invalidate relevant issue eligibility | Malformed/ambiguous/unsafe broad-route edit fails without partial draft change | `app::deletion_of_unprojectable_peer_with_broad_route_fails_without_draft_change`; `app::deletion_only_removes_selected_current_record_and_retains_other_records` |
| Discard draft | Session can reread saved bytes | Draft returns to installed saved state | Reparse installed document/vault, restore persisted reservations; retain only still-valid awaiting-first-file issue material | Read/parse failure returns error; no installed writes; already-consumed delivery is not recreated | `app::discard_restores_draft_without_writing_installed_bytes`; `app::deletion_and_discard_revoke_only_session_bound_initial_material` |
| Commit | Draft validates; mandatory pre-Commit archive succeeds | Saved state advances to installed draft only after successful recoverable install | Persist document, encrypted vault and internal reservations; no runtime call; apply knowledge becomes unknown when saved differs | Backup failure stops before protected writes; partial install reports failure/recovery; draft remains retryable | `app::backup_failure_preserves_installed_files_and_draft`; `app::injected_archive_and_install_failures_preserve_bytes_and_session_saved_state`; `app::commit_advances_saved_state_without_applying_and_backup_contains_old_bytes` |
| Restart with draft | Valid saved-state restart preview; explicit choice | Cancel keeps draft; discard choice resets it before restart sequence | Separate restart flow; Commit never applies | Cancel makes no backup/job/runtime call. Once service request is submitted, cancellation/timeout cannot assert that the job did not finish | `app::restart_cancel_preserves_draft_and_never_calls_adapters`; `app::service_timeout_reports_submission_and_may_still_complete` |
### Address reservations
| Trigger | Preconditions | Next state | Side effects | Failure or cancellation | Existing proof |
| --- | --- | --- | --- | --- | --- |
| Delete saved manageable client in draft | Target's addresses known | No committed reservation yet; addresses remain occupied by saved snapshot | Draft deletion only | Discard restores client and no pending reservation is published | `domain::a_draft_only_deletion_does_not_create_a_committed_reservation` |
| Delete saved unprojectable peer in draft | Every route to be retained is an exact IPv4 `/32` or IPv6 `/128` host route | Exact routes enter draft-only reservation set | Prevent allocation while deletion is staged | Broad/subnet route or missing address evidence rejects deletion without draft mutation | `app::deletion_of_unprojectable_peer_persists_its_exact_host_route_reservation`; `app::deletion_of_unprojectable_peer_with_broad_route_fails_without_draft_change` |
| Commit deletion | Mandatory backup and saved install succeed | Deleted saved addresses enter persisted reservation set; committed client snapshot advances | Allocator excludes current draft/saved and reserved addresses | Backup/install failure does not publish release or new saved state | `domain::discard_restores_draft_and_commit_keeps_applied_snapshot_separate`; `app::state_file_round_trip_keeps_pending_addresses` |
| Restore archive removing current peers | Preview is valid and confirmation accepted; pre-restore backup succeeds | Rebuilt internal state carries archived/current persisted reservations, addresses from current manageable clients, and exact host routes from unsupported current peers; reservations are excluded for addresses still assigned by restored peers | Full saved replacement only; runtime is untouched | Cancel leaves saved state unchanged; backup/install failure leaves recoverable prior state and drafts | `commit_restore::restore_reserves_addresses_of_current_saved_clients_that_the_archive_removes`; `app::restore_does_not_reserve_unprojectable_exact_hosts_owned_by_restored_document`; `app::partial_restore_failure_preserves_session_and_pre_restore_backup_then_reopens_to_old_set` |
| Verified restart publication | Confirmed attempt still matches installed generation; completed service job; complete interface/peer match; internal-state install succeeds | Reservations for removed clients clear | Persist verified timestamp/state, then publish current applied knowledge | Mismatch/read/service/timeout/persistence failure retains reservations and conservative apply knowledge | `domain::successful_verified_apply_releases_deleted_addresses_and_mismatch_does_not`; `app::restart_mismatch_and_post_restart_write_failure_never_release_addresses` |
### Current and historical verification
| Trigger | Preconditions | Next state | Side effects | Failure or cancellation | Existing proof |
| --- | --- | --- | --- | --- | --- |
| Open/reopen | Saved configuration and internal state parse | Current state Unknown; optional management applied snapshot absent; historical timestamp remains separately available | Restore reservations from persisted metadata only | Invalid internal state rejects session; historical success is not live proof | `persistence::persisted_history_never_marks_the_runtime_verified`; `app::verified_history_persists_across_reopen_without_marking_runtime_applied` |
| Periodic statistics read | Worker has an interface name; bounded read returns observation | UI statistics update only | No domain apply transition, no saved write, no reservation release | Unavailable/unknown data remains so; stale-generation result is discarded | `tui::events::statistics_started_before_a_mutation_are_discarded_as_stale`; `tui::state::statistics_refresh_is_generation_bound_and_never_changes_applied_state` |
| Saved state changes | Commit/restore changes installed generation | Current apply knowledge becomes Unknown; old historical timestamp may remain or be cleared according to existing successful restore bookkeeping | No runtime operation | Failed write leaves in-memory published state conservative | `app::commit_advances_saved_state_without_applying_and_backup_contains_old_bytes`; `app::successful_restore_clears_verified_current_history_but_backup_retains_it` |
| Restart comparison matches complete saved technical peer set | Interface identity and complete peer dump match; malformed/duplicate/unverifiable records absent | Still not published until durable verification bookkeeping succeeds | Prepare persisted reservations/timestamp update | Persistence failure leaves in-memory apply knowledge/reservations unchanged | `app::confirmed_restart_orders_backup_job_read_and_durable_publication`; `app::failed_verified_history_write_does_not_advance_memory_or_reopen_as_verified` |
| Full technical match with peer outside management projection | Peer record is valid/verifiable even if not a `Client`, e.g. subnet routes | Knowledge may be Verified with no complete `applied_snapshot` | Persist verified result; do not invent partial management snapshot | Any incomplete runtime dump still cannot pass | `app::restart_verifies_metadata_free_legacy_routes_without_partial_applied_snapshot`; `runtime::projection_keeps_valid_unsupported_routes_and_ipv4_only_peers` |
### First delivery and password reissue
| Trigger / stage | Preconditions | Next state | Side effects | Failure or cancellation | Existing proof |
| --- | --- | --- | --- | --- | --- |
| Create before Commit | Successful coordinated create in this session | Pending draft issue | Hold generated private key in session and bind peer directives/ciphertext | Any failed create publishes no new issue authorization | `app::new_client_preparation_is_transactional_and_uncommitted_keys_cannot_issue` |
| Commit succeeds | Created peer and encrypted record appear in installed state and match session context | Awaiting first file | Promote one-use capability for current session | Commit failure keeps retryable draft capability but does not permit output | `app::commit_failure_keeps_pending_issue_and_successful_retry_authorizes_it` |
| Prepare destination | Eligible committed issue or saved reissue selected; output path validated | Prepared destination is bound to identity, mode, QR request, attempt and generation | No key disclosure or file write yet | Unrelated command/cancel invalidates the prepared context; stale generation requires reselection | `events::prepared_delivery_and_restore_confirmations_are_bound_to_the_reviewed_generation`; `client_delivery::overwrite_confirmation_is_bound_to_the_selected_destination` |
| Confirm first file | Same session, context still matches saved record, overwrite confirmation if required | File is published; first issue is consumed only after saved bookkeeping | Secure file publication; optional QR derives from the same payload and happens after mandatory file | Before-publication failure can retry; uncertain publication requires overwrite confirmation; cleanup failure requires manual cleanup; QR failure does not undo file | `app::failed_file_publication_can_retry_in_a_different_directory`; `app::uncertain_first_file_publication_retains_warning_and_skips_qr`; `app::temporary_cleanup_failure_requires_manual_cleanup_before_first_issue_retry`; `app::file_and_qr_share_payload_and_qr_failure_still_consumes_first_issue` |
| Saved bookkeeping after file | File publication succeeded | Consumed if bookkeeping succeeds; otherwise explicit published-but-uncertain result, no automatic retry capability | Report file exists; preserve activation warning | Do not misreport as failed/unpublished or auto-delete file | `app::published_file_is_reported_when_saved_store_bookkeeping_fails` |
| Reissue after session/reopen | Selected client is in saved state; password supplied for this action | No first-issue session eligibility is involved | Unlock matching saved vault record, render/write selected client file; optional QR | Wrong password/record mismatch/file failure is reported without secret leakage; each client requires fresh password | `client_delivery::reissue_unlocks_installed_saved_record_each_time_and_warns_after_reopen`; `vault::every_retrieval_requires_the_correct_password`; `client_delivery::imported_ipv4_only_parameters_are_preserved_and_authenticated_conflict_blocks_reissue` |
### Restore preview and confirmation
| Trigger | Preconditions | Next state | Side effects | Failure or cancellation | Existing proof |
| --- | --- | --- | --- | --- | --- |
| Select archive for preview | Safe archive name and valid archive format | Opaque preview holds validated bytes plus metadata; UI receives redacted summary/change list and generation | Read-only archive validation; no password and no runtime call | Invalid/traversal archive rejected; cancel drops preview | `app::preview_is_read_only_and_confirm_restores_without_unlocking_vault`; `commit_restore::archive_parser_rejects_traversal_links_duplicates_and_oversized_members` |
| Confirm matching preview | Preview token still belongs to the operation generation; installation paths remain compatible | Selected archive replaces saved settings, server configuration and vault as a whole after mandatory pre-restore backup; running VPN remains unchanged | Rebuilt internal state carries archived and current persisted reservations, addresses of current manageable clients and unsupported peers' exact host routes that the restore removes; addresses still assigned by the restored configuration are excluded. Draft-only exact routes are included. The rebuilt state has no historical verification timestamp; current apply knowledge becomes Unknown. No restart occurs. The pre-restore backup retains the prior saved bytes and history. | Stale/cancelled preview cannot write; backup/install failure preserves prior saved state or requires journal recovery | `events::prepared_delivery_and_restore_confirmations_are_bound_to_the_reviewed_generation`; `app::restore_accepts_changed_non_path_settings_and_rejects_changed_paths`; `commit_restore::restore_reserves_addresses_of_current_saved_clients_that_the_archive_removes`; app `restore_does_not_reserve_unprojectable_exact_hosts_owned_by_restored_document`; `app::successful_restore_clears_verified_current_history_but_backup_retains_it`; `commit_restore::backup_failure_blocks_commit_and_restore_without_losing_drafts_or_saved_bytes` |
| Cancel preview/confirmation | No restore install has begun | Existing saved state and draft remain | Clear app-executor preview context | No backup/install/runtime side effect | `tui_workflow::worker_backup_restore_preview_cancel_and_confirm_are_separate_saved_operations`; app preview read-only test above |
### Restart attempt identity and uncertainty
| Trigger | Preconditions | Next state | Side effects | Failure or cancellation | Existing proof |
| --- | --- | --- | --- | --- | --- |
| Preview restart | Installed bytes equal session saved generation; interface/service settings and complete saved peer projection are safe | Read-only preview captures saved bytes/settings/projection and draft presence | No backup/job/runtime operation | Unsafe or ambiguous peer state blocks before backup; stale saved bytes reject confirmation | `app::unsafe_saved_peer_diagnostics_block_preview_before_backup_with_safe_location`; `app::restart_confirmation_rejects_a_generation_changed_after_preview` |
| Cancel choice | Preview still valid | Cancelled report; draft remains | No backup, service request, runtime read, or persistence | No operation was submitted | `app::restart_cancel_preserves_draft_and_never_calls_adapters` |
| Confirm restart | Preview still current; explicit choice; backup succeeds | Attempt identity bound to saved generation, then service job submitted | Optional draft discard at specified safe point; complete backup; one shared deadline | Backup failure submits no job; before submission cancellation is definite | `app::restart_backup_failure_preserves_draft_and_submits_no_job`; `app::confirmed_restart_orders_backup_job_read_and_durable_publication` |
| Service job submitted | Request accepted | Await job completion under same deadline | Job may still complete even if caller times out/disconnects | Timeout report retains submitted/may-still-complete distinction; never claim cancellation or rollback | `app::service_timeout_reports_submission_and_may_still_complete`; `app::submitted_terminal_job_failures_cannot_still_complete` |
| Job complete, observe and compare | Job successful; remaining shared deadline; expected configured interface and complete runtime peer set | Matching result becomes candidate for durable verification | Bounded observation and pure comparison; no direct state publish yet | Read error/timeout/missing interface/incomplete dump/mismatch stops publication and retains reservations | `app::shared_deadline_expired_after_successful_job_stops_before_runtime_read`; `app::shared_deadline_expired_after_runtime_read_reports_timeout_before_comparison`; `runtime::partial_duplicate_and_unavailable_records_never_pass` |
| Durable verification succeeds | Attempt token and both current/attempt saved bytes still equal; full technical match; internal state install succeeds | Publish current verified knowledge and clear retained addresses | Persist timestamp/reservations first; update in-memory session after successful install | Failed persistence leaves conservative state; no second restart or automatic rollback | `app::failed_verified_history_write_does_not_advance_memory_or_reopen_as_verified`; `app::restart_mismatch_and_post_restart_write_failure_never_release_addresses` |
## M20–M26 migration disposition
This register resolves the destinations proposed by M19 against the integrated
source. The milestone labels are historical; the owner column names the current
code. Public facade calls remain where the caller audit below documents an
active integration-fixture dependency. Existing low-level adapters and
persisted formats remain in place.
| Milestone | Production surface and destination | Compatibility / preserved boundary | Existing baseline and exact focused commands |
| --- | --- | --- | --- |
| M20 — vocabulary and named contexts | Domain/apply vocabulary is in `src/domain.rs`; observations/comparison in `src/runtime.rs`; presentation mapping in `src/tui/state.rs`; `PreparedDestinationContext` and `RestorePreviewContext` are private named structs in `src/tui/events/executor.rs`. | Semantics remain unchanged; no domain child split was justified. | Historical focused checks recorded in M20 evidence; current generation-bound proof is `events::prepared_delivery_and_restore_confirmations_are_bound_to_the_reviewed_generation`. |
| M21 — reservations and apply knowledge | `src/domain.rs` owns `AddressReservations`, allocation and narrow apply transitions; `src/persistence.rs` encodes durable state; `src/runtime.rs` compares technical peer state; `src/app/restart.rs` coordinates verified publication. | No `domain/reservations.rs` or `domain/apply.rs` split was warranted; unrestricted draft mutation was removed. Persisted schema and semantics remain in persistence. | `domain::exact_host_routes_stay_pending_until_commit_and_discard_drops_them`; `persistence::persisted_history_never_marks_the_runtime_verified`; `app::restart_mismatch_and_post_restart_write_failure_never_release_addresses`. |
| M22 — coordinated draft and create/edit/import | `src/app/draft.rs` coordinates document/client/vault/reservation drafts; `src/app.rs` remains the facade; `src/vault/source.rs` reads sources and `src/vault/import.rs` reconciles/encrypts. | No separate `app/clients.rs` or `app/import.rs` was justified. `Application::open`/`add_client` remain for injected-state and fixture setup, documented in the caller audit. | `app::new_client_preparation_is_transactional_and_uncommitted_keys_cannot_issue`; config preservation, delivery, import and workflow integration suites. |
| M23 — saved Commit, backups and restore | `src/app/commit.rs`, `src/app/restore.rs`, and `src/app/backups.rs` own scenarios; `src/persistence.rs` owns journaling/path safety and `src/backup.rs` owns archive encoding/parsing. | Preview is byte-bound and saved-only. M23 added restoration of exact routes from unsupported current peers; this closed the earlier noted reservation gap. | `app::commit_advances_saved_state_without_applying_and_backup_contains_old_bytes`; `app::preview_is_read_only_and_confirm_restores_without_unlocking_vault`; `restore_does_not_reserve_unprojectable_exact_hosts_owned_by_restored_document`; `commit_restore::backup_failure_blocks_commit_and_restore_without_losing_drafts_or_saved_bytes`. |
| M24 — delivery scenarios | `src/app/first_delivery.rs` owns first-issue capability/retry state; `src/app/first_file.rs` runs first-file delivery; `src/app/reissue.rs` runs password reissue. `src/delivery.rs`, `src/delivery/files.rs`, `src/delivery/qr.rs`, and `src/vault.rs` are focused adapters/value owners. | No `app/delivery.rs` was created because session capability, file delivery and saved-record reissue have separate cohesive ownership. | `app::failed_file_publication_can_retry_in_a_different_directory`; `app::temporary_cleanup_failure_requires_manual_cleanup_before_first_issue_retry`; `client_delivery::reissue_unlocks_installed_saved_record_each_time_and_warns_after_reopen`. |
| M25 — restart scenario | `src/app/restart.rs` owns preview, attempt token, backup/job/read/compare/durable-publication order; `src/runtime.rs` owns complete peer projection/comparison; `src/service.rs` owns systemd adapter; persistence owns durable state. | Fixture apply is compiled only under `#[cfg(test)]`. No host service calls occur in ordinary fixture tests. | `app::confirmed_restart_orders_backup_job_read_and_durable_publication`; `app::service_timeout_reports_submission_and_may_still_complete`; `app::restart_mismatch_and_post_restart_write_failure_never_release_addresses`. |
| M26 — read model and TUI command adapter | `src/app/read_model.rs` owns secret-free peer/state/issue/reservation/apply facts and per-action eligibility. `src/tui/state.rs` owns UI navigation/generation; `src/tui/events.rs` and `src/tui/events/executor.rs` own worker lifecycle and command adaptation; forms own transient inputs; renderers own visual output. | TUI does not parse saved bytes or duplicate business authorization. Split of broader terminal/render modules was deferred; their shared lifecycle/layout helpers did not justify a larger move. | `tui::`; `tui_workflow`; `client_delivery`; M26 evidence records focused/full host results and limits. |
### Test inventory and verification notes
- There is no `tests/client_lifecycle.rs` target. Lifecycle coverage is
distributed across `app::`, `client_delivery`, `commit_restore`,
`vault_import`, and `tui_workflow`; the M22–M26 work used existing
targets, and a new target is needed only if acceptance finds a real behavioral
gap.
- Application projection and eligibility are covered by colocated `app::`
tests plus `tui::` and `tui_workflow::` tests; no separate integration target
was needed.
- Restore reservation coverage includes manageable saved clients, exact routes
from unprojectable current peers, persisted reservations, and exact routes
retained by the restored document. `app/restore.rs` excludes routes still
owned by restored peers. The previous M19/M23 gap note is superseded by
`restore_does_not_reserve_unprojectable_exact_hosts_owned_by_restored_document`.
- `ClientStates::draft_mut` and `Application::vault_draft_mut` no longer exist.
`Application::add_client` and injected-state `Application::open` remain
public because integration fixtures use them; the caller audit above records
those exceptions. They are not the production create/startup path.
- Existing test-only container driver is `src/container_driver.rs` included only
by `src/lib.rs` under `#[cfg(test)]`. Its normal fixture tests are synthetic;
ignored `container_driver::alpine_scenario_driver` is opt-in and is not an
ordinary host test or a systemd acceptance test. Do not run it as part of
this documentation task.
- M19.2 and the subsequent migrations use only approved dependencies already
present. No dependency, manifest, lockfile, configuration, or product behavior
change is implied.