HEXWORK live-integration plan — Phases 9–11 #
This plan closes the gap between subsystem implementation and the production game. It does not freeze Phase 12, content, or isolated mechanics work.
Status semantics #
Every phase is tracked independently as:
- code — subsystem/API implementation and focused tests exist;
- integrated — normal production initialization and executable actions use it;
- gate — an acceptance test drives the production path end to end.
The detailed Phase 9–11 boxes in 06-implementation-checklist.md describe real component work, but their production runtime gates remain open.
Runtime authority #
State.actors: Vec<Actor>is the sole simulation-entity authority for 1.0 integration.- Actor-keyed
BTreeMaps attach identity and subsystem state to those actor IDs. - The dormant
hecswrapper is not mirrored during this integration pass. A later migration must be explicit, atomic, and drift-tested. State::step()and its ordered phases remain the sole simulation clock.- Content definitions are immutable templates; per-run mutable authority lives in
State. - Worldgen returns deterministic apply-ready plans. The production session applies a plan to
State; tests use the same path. - Presentation caches, screen stacks, audio buffers, and accessibility layout never enter the canonical simulation hash.
Persistence decision #
HEXWORK uses a hybrid model:
- a versioned current authoritative checkpoint reconstructs the run and retains already-applied history without reapplying it;
- ordinal typed lifecycle events record player actions and Story resurrection with explicit pre/post simulation ticks; rollback does not erase prior events;
- bounded replay starts from canonical recorded identity, uses the same action/lifecycle paths, checks dead/checkpoint/restored hashes across resurrection, and verifies the terminal hash;
- the active Story town checkpoint is persisted as a nonrecursive StateImage plus event-prefix marker, with latest-marker provenance proved from retained history before untrusted restored use;
- Classic lineage data belongs in a separate atomically replaced file and must not be deleted with a run save; production lineage transfer and filesystem ordering remain Stage 3c2 work.
No save/replay acceptance claim is valid until canonical hashing covers all authoritative deterministic state.
Coordinate convention #
- Milthollow is floor 0 around world z = 8.
- Deep depth
duses the existing negative-z convention around-10 * d. - A production Deep entrance connects town floor 0 to depth 1.
- Generated floor plans must provide traversable Up, objective, and Down routes; three or more vertical connector affordances remain required per floor.
Representative vertical slice #
The first end-to-end contract is rats_in_granary:
- chargen;
- spawn in populated Milthollow;
- walk to the real notice board;
- inspect and accept
rats_in_granary; - enter generated Deep floor 1;
- encounter cable-rats, items, and environmental systems;
- satisfy the objective;
- return to Milthollow;
- turn in for exactly 25 copper and standing effects;
- use the Gilded Whetstone inn and advance one world day;
- save, quit, reload, and verify the identical canonical state.
The permanent headless runner must call the same initialization, action-dispatch, plan-application, reward, service, and persistence functions as the interactive executable. Test-only state construction cannot satisfy this gate.
Work graph #
Stage 1 — foundations (parallel) #
- Core runtime types: actor identity, wallet, active-contract lifecycle, run location, mandatory authority validation, and a versioned full canonical persistence/replay hash. The full hash runs only at explicit save/replay/checkpoint boundaries; production ticks use no discarded full-world scan.
- Deterministic world plans: Milthollow geometry/anchors/spawns and physical voxel plans for all 12 Deep floors. A plan is not "playable" merely because it contains voxels: paired transitions, carved connector columns, production-equivalent collision/support/headroom routes, canonical named anchors, accepted content IDs, and transactional application are required.
- Phase 12 UI/audio/keymap/certification and schema-only content expansion continue independently.
Stage 2 — production session integration #
Parent-owned merge point:
- load and validate the canonical content pack once;
- initialize
GameSession/equivalent from seed, mode, content, and world plans; - apply plans into
Statewithout a second entity representation; - expose one typed action dispatcher shared by terminal input, headless tests, save journals, and replay;
- replace static Contracts tooltip behavior with board inspection/acceptance;
- implement Deep entry/exit, objective progress, turn-in, service use, and sleep.
Stage 3 — persistence and deterministic evidence #
- complete canonical hashing over every authoritative field in stable order;
- version authoritative checkpoints and typed action journals;
- implement save/quit/load across process boundaries;
- persist lineage vault atomically and load it for descendants;
- write
.hexdemoon Classic death with expected final hash; - restore Story town checkpoints rather than healing at the death position;
- replay real committed actions and report first divergence clearly.
Stage 3c1 repair plan — implementation independently accepted #
Goal #
Make the production session's current-state save, retained lifecycle history, Story town checkpoint, and deterministic replay agree across save/resume and repeated resurrection. The earlier Stage 3c1 candidate was rejected despite passing happy-path tests. The repaired implementation now has independent ACCEPT for latest-checkpoint proof, transactional witness refresh, bounded lifecycle replay/evidence and genuine death/resurrection scenarios. This is a session-level acceptance, not a production/process completion claim. Stage 3c2 remains responsible for terminal/CLI/filesystem integration, lineage transfer, and the strict unused-API lint debt.
Implementation Summary #
Replace only the top-level persistence DTOs/envelopes and GameSession persistence/lifecycle orchestration. Reuse the accepted bounded StateImage and action adapters and existing world/action/controller code. Run and replay formats become v3; the core canonical hash remains v3. Initial identity is immutable, current Character progression is not initial identity, and simulation ticks are not global event sequence numbers.
Production shapes:
RunIdentity { seed, mode, ancestry, calling, display_name, content_hash }.HashedState { tick, hash, image: StateImage }(existing explicit StateWire on disk).StoryCheckpoint { state: HashedState, after_event_count: u32 }. This never contains save bytes, history, or another checkpoint.SessionEvent { pre_tick, post_tick, kind }, ordered by vector index.SessionEventKind::Action(CommittedAction).SessionEventKind::StoryResurrect { checkpoint_after_event_count, checkpoint_hash, dead_state_hash, restored_state_hash }. All four fields are mandatory.- Run: identity, current HashedState, optional StoryCheckpoint, full retained events.
- Replay: identity, events, expected final tick/hash. No state snapshot and no duplicated persisted rule-chain rows. Replay derives a bounded subtitle stream in event order, including cause deltas before a rollback erases them.
Story requires one saved active checkpoint; Classic/Iron require none and prohibit resurrection events. Prefix zero identifies canonical initialization. A nonzero checkpoint prefix ends immediately after a successful town-entry transition event. Resurrection never truncates full event history. It restores only checkpoint State, applies documented chapel changes, retains identity/checkpoint, and appends one lifecycle event.
Implementation Plan #
-
Upstream reciprocal-melee prerequisite (discovered during session implementation; design approved with final warning-order coverage clarification applied). Current production actions cannot kill the player: supported-only movement/transitions and one-sided attacks make genuine Story death replays impossible. Within existing zero-tick
ResolveAttack, the player strikes first for the existing 3 damage; only a surviving hostile returns one strike for the same 3 damage. Killing blows receive no retaliation. This is a reciprocal-melee slice, not autonomous monster AI or a claim of complete combat. No new action variant, RNG, elapsed tick, or worldgen/content mutation is needed.- Session owns role/location/range legality. One pure
melee_geometryhelper serves contextual selection, pre-attack preview, and dispatch: same active plan, same z, nonzero Chebyshev distance <=1. For diagonals either orthogonal side column's solid body/head occupancy blocks the strike; do not confuse weapon-line geometry with movement support/water/actor-occupancy rules. Rejections do not mutate. - Core owns a zero-tick
resolve_melee_exchangetransaction over two distinct living positive-HP actors, explicit 3/3 damage parameters, and optional defender-kill objective credit. Preflight runtime/actors, objective eligibility, outcome-dependent corpse capacity leaving next_id valid, and fallible collection reservations supported by current containers before first semantic mutation. Saturate HP arithmetic; compute at most one death; commit causes, exact death consequences, and optional objective progress with no later Result branch. Do not clone/hash the full StateImage or scan world voxels for rollback. A malformed explicit objective-credit request rejects the whole exchange; session omits credit when no eligible rat contract exists rather than swallowing errors after damage. - Extract existing death bookkeeping into a private exact-ID finalizer shared by ordinary phase_death and melee. Keep phase_death's ordinary collector/order/cleanup semantics, but melee finalizes only the one actor killed in this exchange; never invoke the broad phase entry from an attack. Preserve corpse template
corpse, FLESH, identified=true, Sound, tags corpse+perishable; fail that actor's Active/Completed contracts, retain terminal statuses, remove its chapel debt, and preserve the current Search/ID/bargain consequences consistently. Preflight new corpse allocation strictly enough that immediate runtime validation and hashing remain valid (reject an allocation that would leave next_id==MAX). Return an infallible committedMeleeExchangeresult after successful preflight. - Register outcome rule
meleein canonical_rule_tag, laws::explain, epitaphs::subtitle and RULE_SUBTITLES; use existingdeathfor finalization. Preserve hash domain order/primitive encoding/version; new cause contents legitimately change resulting hashes. Pin cause-image roundtrip and registry coverage. Extend AttackResolved with retaliation_damage:Option and player_killed:bool while retaining outgoing damage/target killed/objective fields. - Permit the smallest presentation-only main.rs edit: render a pure contextual melee hint for the currently legal target before the next input poll, using the shared legality helper (e.g.
Attack Cable-rat: deal 3; if it survives, it returns 3 immediately). Ensure a universal melee warning is explicitly flushed on entering Play before the first Play input poll (not deferred to the ordinary post-poll render path), through a small testable ordering seam, so queued move/Confirm input cannot cause an undisclosed first retaliation. Report actual retaliation/player death in the result too. No main persistence/scheduler/filesystem changes belong to this prerequisite. - Existing first-loop three-rat contract remains completable: fresh HP10, three surviving-rat return strikes of 3 leaves HP1; each rat killing blow has no return strike. Tougher already-generated floor2 hostiles provide lethal exchanges: navigate town->floor1->floor2, approach generated HP30 Grate-mother, attack four times; the fourth return strike kills the player while the hostile survives. End-to-end tests use only canonical initialization, moves/transitions and attacks, never position/HP/death injection. Review this narrowed prerequisite before code, then implement core exchange plus minimal session/presentation wiring and v3 test conversion before resuming remaining lifecycle gates. Explicit ownership includes core state/combat/cause registry files, session.rs, and only the stated main.rs presentation lines; unrelated protocol/content/main work stays untouched.
- Session owns role/location/range legality. One pure
-
Freeze v3 protocol and tests (save owner). Replace public DTOs and top-level run/replay pipelines in
hexwork-save/src/protocol.rs, with exports inlib.rs. Preserve explicit bounded State/action adapters and core hash tags. Keep the existing 8-byte magic values as stable format-family identifiers despite their historical numeric suffixes; decode the bounded raw prefix(magic, format_version, canonical_hash_version)first, gate the format version, and only then decode the v3 body. Real v2 run/replay fixtures must return OlderVersion before any v3 body decoding. Unknown magic and newer versions remain explicit errors. Legacy header/string-demo APIs remain component-only until Stage 3c2 removes their production use. -
Shared limits and structural event checks (same save owner). One decode context and one encode budget cover identity, events, current image, and optional Story image together. Do not reset budgets per image. Retain compressed/raw/global backstops and pre-allocation bounded strings/vectors; apply combined chunk/row/string limits symmetrically. Event cap stays 65,536. Start structural cursor at zero. Each event requires pre_tick >= cursor. Action spans must match current production semantics: Wait=1; Inn=sleep_ticks(); current remaining actions=0. Reject unsupported service actions rather than inventing a timing contract. Resurrection is Story-only, post_tick <= pre_tick, with canonical mandatory payload. Set cursor to post_tick, allowing a reduction only for resurrection. Final/current tick >= final cursor. Historical pre-death ticks need not be <= current tick. All arithmetic is checked. Validate checkpoint prefix is within history and, unless zero, identifies a transition; the session proves its town-entry meaning.
-
Canonical session identity and restore checks (session owner after protocol). Enforce supplied initial Character equals
character::new(ancestry, calling); retain the existing PlayerSetup interface for current main compatibility. Persist original identity separately, never infer it from grown state during resume. Require exactly one Player with matching immutable template/name/ancestry/calling in both images. Current player attributes must be between canonical initial values and 20, skills between canonical initial values and 100, uses >= pips, and pips must be zero at mastery or belowuses_for_next_point(skill)otherwise. Do not require an exact uses-to-skill equation: training increases skill and clears pips without changing uses. Validate required player tables. Connector IDs must be nonzero, unique, and match the generated set; ID/kind/endpoints/length are immutable provenance. Permit changed finite nonnegative load, quality <=100, and blocked state. Implement these session-specific checks locally for this pass; do not broaden core ownership. -
Checkpoint-reference proof (session owner). Fresh/live checkpoints are proven by construction. Restored checkpoints first pass structural, content, identity, living-player, canonical Milthollow Town, seed, and state-hash validation. Mark their prefix provenance unverified in private, nonserialized session metadata. Before the first resurrection OR replay export from a resumed Story run, bounded-replay the event prefix through the same canonical initializer/dispatcher/lifecycle code and compare its derived active checkpoint prefix/tick/hash to the saved checkpoint. Prefix zero is proved by canonical initialization. Fail without mutation/publication of an artifact on mismatch or budget exhaustion; cache successful proof observationally. The private, nonserialized proof cache is keyed by the exact witness
(checkpoint.after_event_count, checkpoint.state.tick, checkpoint.state.hash, canonical_digest(events[..after_event_count])), never a boolean. Reuse requires exact key equality and a freshly validated checkpoint image/hash. Checkpoint replacement or mutation/truncation of the proved prefix clears the cache; successful live construction installs the new key, and rollback restores only a matching old key or clears it. Appending events strictly after the proved prefix does not invalidate it. The prefix digest uses deterministic explicit event wire encoding and SHA-256, never Debug text or unordered iteration. This avoids full replay on ordinary snapshot load but never uses an unproved checkpoint for resurrection or export. During all replay, derive the active checkpoint from actual town-entry outcomes, never from input-provided checkpoint images. Every resurrection must reference that exact latest checkpoint and compare dead hash before restoring and restored hash afterward. -
Session mutation and atomic refresh (session owner). Split dispatch into common preflight, private nonrecording action execution, and commit. Reject dead/nonpositive-HP Player and insufficient event capacity before mutation. Reserve event capacity before action/lifecycle execution. For Story town-entry actions only, retain the validated pre-action StateImage, old event length, and old checkpoint; execute and append one action event; build the checkpoint candidate through a private helper accepting an injected builder for unit tests. Publish the candidate only after validation/hash succeeds. On failure restore the pre-image, exact old history length, and old checkpoint without
expect; return a contextual error. Classic/Iron never invoke this checkpoint builder. Resurrection builds/validates/hashes a candidate state before commit, sets chapel access, HP10, living status, clears player fall odometer, synchronizes light, assigns core chapel debt, and appends one event containing both hashes. Replay uses these same methods with an empty generated history and compares each generated event to its expected record; it never preloads and doubles the input history. -
Replay work accounting (session owner). Expose trusted
ReplayLimitswith finite defaults: 10,000 full-phase iterations, 50,000 unattended iterations, 100,000 cumulative elapsed ticks,max_evidence_rows=16,384, and the protocol event cap. These are initial engineering safety ceilings, not a performance certification. Before constructing a world or stepping, checked preflight sums idle gaps, fixed semantic action costs, each rollback epoch, and the final gap exactly once. Wait charges one full step; Inn chargessleep_ticks()unattended iterations, regardless of claimed post_tick. Runtime independently decrements before each normal step and reserves the whole fixed unattended charge before service dispatch; verify actual post_tick. Prefix proof and subsequent work within the same replay/proof request share one remaining budget, with no reset at resurrection. Configurable upward limits are available only to explicit trusted callers, not file metadata. Collect release timings for a representative real-world gap and Inn action; if default ceilings are too expensive for interactive replay, lower them before CLI integration. Absolute u64::MAX/sparse horizons fail the cheap preflight. -
Derived evidence (session owner). Capture cause deltas after each action and idle step into bounded presentation evidence tagged by event/segment ordinal and local cause ordinal. Capture deltas before restoration, reset the cause cursor to the restored state's baseline after rollback, and retain the already-emitted evidence. Before cloning or pushing each evidence row, check/reserve against the request's shared
max_evidence_rows(default 16,384); prefix proof and replay share the budget without resetting it on resurrection. Bound each rendered row's owned text to 4 KiB total UTF-8 bytes and reject over-limit rows before cloning, so a row cap also bounds retained text. Never globally sort by tick, and never trust file-supplied subtitles. Return first event-index/pre/post/hash mismatch when known; terminal-only divergence must be described as terminal-only, not exact first-state divergence. -
Integrate and verify (parent). Protocol implementation precedes session implementation. No concurrent editing of protocol/session files. Scope excludes main.rs beyond step 0's pre-attack presentation lines, loop_.rs, filesystem paths, packaging, and core rule redesign beyond the explicitly reviewed reciprocal-melee prerequisite in step 0. Run all listed gates, obtain independent implementation review, fix/rebut findings, and re-review critical/high changes before Stage 3c2.
Acceptance Criteria #
- AC.1: v3 old/new/header gates, bounded dual-image budgets, canonical lifecycle tags and payloads pass before untrusted object allocation.
- AC.2: canonical initial identity is retained independently; valid practice/trainer growth resumes; malformed initial/current Character data rejects.
- AC.3: exact current state and prior town checkpoint survive Story save in Deep and resume; Classic resume can export a matching replay.
- AC.4: real town-entry refresh uses the correct event prefix and is atomic on injected failure; Classic/Iron do not call Story refresh.
- AC.5: repeated resurrection retains full history; resumed resurrection replays; divergence in an erased pre-death branch fails the dead-state hash check; cached proofs cannot bless a changed image or history prefix.
- AC.6: dead-player input and capacity exhaustion leave state/history/checkpoint unchanged; only the lifecycle method resurrects.
- AC.7: full-step, unattended, and elapsed work bounds reject cheaply and cannot reset across rollback/proof operations; service replay obeys actual timing.
- AC.8: legal connector load/quality/blocked changes resume, while duplicate IDs and changed provenance reject.
- AC.9: derived subtitles preserve pre-death and post-rollback event order without duplicate restored causes; evidence storage is bounded by the shared row/text policy.
- AC.10: previously accepted adapter, metadata, float, allocation, deterministic-continuation and action regressions remain green or are explicitly replaced by equivalent v3 cases.
- AC.11: independent review accepts this session boundary; production CLI/process gates remain unchecked until Stage 3c2.
- AC.12: reciprocal melee has canonical, saturating and atomic damage/death consequences; fresh three-rat contract remains winnable; a real action sequence against generated hostiles can kill the player and reproduce the exact death state from seed/events without fixture injection.
Test Strategy #
Named AC-to-test map (production initializer/plans/dispatcher required for session gates):
-
AC.1:
v2_run_and_replay_reject_as_older_before_body_decode,v3_dual_images_share_decode_and_encode_budgets,v3_event_tags_payloads_and_caps_are_canonical. -
AC.2:
noncanonical_initial_character_is_rejected,grown_character_with_training_and_use_pips_restores,invalid_character_ranges_and_pips_reject,resume_preserves_original_run_identity. -
AC.3:
story_deep_save_resume_retains_town_checkpoint,classic_resume_export_replay_roundtrip. -
AC.4:
deep_to_town_refreshes_checkpoint_after_transition_event,injected_story_refresh_failure_restores_state_events_and_old_checkpoint,classic_and_iron_town_transition_ignore_checkpoint_builder_failure_without_panic. -
AC.5:
checkpoint_proof_cache_cannot_bless_changed_image_or_prefix,repeated_resurrection_keeps_ordered_history_and_active_checkpoint_reference,resurrection_continue_export_replay_matches,resume_resurrection_export_replay_matches,tampered_erased_predeath_branch_fails_dead_hash,restored_checkpoint_prefix_proof_rejects_unrelated_image,multiple_town_checkpoints_and_resurrections_preserve_latest_reference. -
AC.6:
dead_player_dispatch_rejects_before_state_or_event_mutation,resurrection_tag_cannot_decode_as_player_action,event_capacity_rejects_action_and_resurrection_atomically. -
AC.7:
huge_sparse_replay_rejects_before_any_step,rollback_epochs_charge_elapsed_once_each,inn_service_charges_unattended_work_before_dispatch,limits_boundary_values_accept_then_reject,prefix_proof_and_replay_share_request_budget. Record a release replay/Inn timing experiment with its machine and limits; do not mislabel it a cross-platform performance gate. -
AC.8:
mutated_connector_load_quality_blocked_restore,duplicate_or_changed_connector_provenance_rejects. -
AC.9:
story_replay_derives_predeath_and_postrestore_subtitles_in_event_order,replay_evidence_limit_rejects_without_unbounded_growth(shared cap equality succeeds; cap+1 fails before clone/push; oversized row text rejects). -
AC.10: retain/rework
bounded_prefixes_and_aggregate_helpers_reject_without_large_allocations,encode_rejects_oversized_nested_containers_before_wire_construction,corrupt_truncated_trailing_and_bomb_err,crafted_reputation_wire_rejects_duplicate_and_reordered_arrays,f32_bits_roundtrip,descending_chunks_are_not_sorted,descending_laws_are_not_sorted,dense_27_chunk_runtime_state_survives_and_continues,all_actions_roundtrip_and_same_tick_order,run_metadata_content_and_versions_are_gated, and mixed session action/idle/service replay tests. Remove obsolete action-only global tick assumptions deliberately; do not count the actorless legacy Phase11 re-step as production proof. -
AC.11: cargo fmt --all -- --check; cargo test -p hexwork-save --all-targets; cargo test -p hexwork-cli --all-targets; cargo test --workspace --all-targets; strict clippy for affected crates/workspace; diff whitespace check; independent review. Strict CLI currently reports APIs unused by main: report that known Stage3c2 issue rather than suppressing it or calling the gate green. The behavioral acceptance can be conditional on this explicitly deferred wiring-only lint, but no ship/workspace-green claim is allowed. Changed-file scope inspection is an audit, not a behavioral test.
-
AC.12:
melee_survivor_retaliates_once_and_killing_blow_does_not,melee_dead_invalid_blocked_or_exhausted_rejects_atomically(HP/alive, causes, items/ground items, debts/bargains, contracts, next_id, state hash/tick and session events unchanged),melee_death_cleans_debt_contracts_and_mints_one_corpse(Active/Completed fail, terminal retained, exact corpse fields/location, one ID, immediately valid hash without step),melee_cause_image_roundtrip,melee_diagonal_through_solid_corner_is_not_contextual_and_rejects_atomically,adjacent_melee_warns_before_confirm_and_outcome_reports_retaliation,play_entry_melee_warning_is_emitted_before_first_play_input_poll(explicit pre-loop flush ordered before poll/dispatch with queued Confirm),production_three_rat_contract_survives_reciprocal_melee(HP10->1, three retaliation causes, three no-retaliation killing blows, Completed 3/3 and turn-in),production_hostile_exchange_reaches_replayable_player_death. In the last test, navigate from canonical initializer through actual transitions and committed attacks to floor2 Grate-mother; compare exact dead replay state/hash before resurrection; no direct position/HP/death mutation. Core numeric-edge fixtures are permitted for transaction unit tests only.
All Cargo tests use TMPDIR=/var/tmp. Negative tests returning GameSession use explicit result matching to avoid huge Debug dumps obscuring the failing assertion. Failure-injection closure is private/local, not a global test switch.
Review Strategy #
Design review must approve this artifact before implementation. Fix or explicitly rebut every critical/high finding; re-review substantive contract changes. After protocol gates, review v3 framing, aggregate budgets and event structure before session wiring. After session gates, a fresh reviewer must trace full Story resume/resurrection/replay and erased-branch validation, mutation atomicity, and resource budgets. Passing happy-path tests alone is insufficient.
Documentation Strategy #
This section records a proposed repair, not shipped behavior. On acceptance, update the persistence-decision summary to current-state + ordered lifecycle events + persisted nonrecursive Story checkpoint and bounded replay. Update Stage3c1 status only after evidence; leave filesystem/process, Classic-lineage and live terminal gates pending. Docs03/04 product promises remain authoritative; no weakening of replay or Story requirements to match incomplete code.
Risks, Blockers, and Required Decisions #
- Run/replay v3 intentionally reject older pre-release artifacts clearly; no migration is invented. Core hash remains v3.
- Two full images share memory/byte budgets, so a Story save can hit limits sooner than Classic. The genuine production session was measured at 54 chunks per image, so 27+27 protocol fixtures were insufficient: current+checkpoint needs 108 chunks. Resource policy is amended to per-image64, shared128 chunks, shared16,000,000 nested rows, and matching256MiB raw/bincode backstops (compressed64MiB unchanged). This covers the existing64+64 image allowance with one shared finite budget; it is not permission for unbounded decode. Test54+54 and the actual Deep save/resume path; never silently drop the checkpoint, reduce the test to live-only resurrection, or reset limits.
- A checkpoint prefix is not proven merely by an internally consistent hash. Lazy bounded prefix proof is mandatory before resumed Story resurrection/export; legitimate very long runs may need trusted larger limits. Return a recoverable error, never discard state/checkpoint/history.
- Hashes are deterministic consistency checks, not signatures. Dead/restored/checkpoint hashes detect replay divergence across erased branches; they do not prevent an editor from recomputing all evidence.
- Finite 65,536-event storage and replay ceilings need eventual archive/rollover product policy. At capacity reject before mutation; do not truncate unnoticed.
- Canonical chargen is the only supported initial setup in this pass. Later lineage-derived initial knowledge will need explicit initial-state identity in the lifecycle contract before Stage3c2 descendant integration; do not silently load mutable external lineage data during replay.
- Main still directly mutates State until Stage3c2. This plan cannot claim production sole-authority enforcement or remove unused APIs by suppression.
Parent-verified Stage 3c2 findings (not yet an implementation contract) #
The parent independently checked the local code after the Stage 3c2 research map. Main still uses Classic-default session construction despite computing other modes, legacy empty-tail autosaves/no-input replay, inline Story revival, direct State scheduler mutation, and discarded death/vault records. Source-checkout content roots and CWD artifact paths need explicit installed-runtime policies. Main panic reporting does not call its crash-artifact writer.
Corrections that must be resolved before the next handoff:
- Core
commit_deathscurrently clears the shared IDF book while finalizing any actor, including a killed monster. Reading terminalstate.idffor lineage transfer can therefore be too late. Define typed deterministic retention/transfer; do not reconstruct authority by parsing the prose cause log. - Current RunIdentity does not carry initial inherited knowledge. Snapshot any gameplay-relevant inheritance into the recorded initialization contract before applying it; replay/resume must not consult a mutable external vault. Freeze the complete shape before another format bump.
- A
.hexdemois produced by the replay encoder, not by renaming current-run snapshot bytes. Single-file atomic replacement does not make demo/vault/run retirement one transaction: define retry identity, failure ordering and interruption recovery before deleting a run save. - Keep the bible's auctioned-gear/buyback requirement. Auction records are distinct from automatically restoring inherited live equipment; do not delete the requirement to fit the current knowledge-only vault.
- Vault and main replay readers use
fs::readbefore length checks; add bounded file reads, not only bounded parsing. Directory syncing in the atomic writer is currently best-effort, and cross-platform durability is not certified. - Ordinary elapsed ticks should remain replayable gaps, not fake player actions, but tick advancement and liveness/death checks must move under GameSession control. Several helper methods are private or test-only; the research API list was not itself a ready-to-call main interface.
These are verified integration gaps and design requirements, not claims that process-level gates have passed.
Stage 3c2 implementation plan — design approved; implementation in progress #
Goal #
Connect accepted GameSession behavior to the real executable without losing learned knowledge or weakening replay, permadeath, or Story semantics. Prove save/resume, replay, Story recovery, and Classic lineage across independent processes. This stage does not claim complete auction gameplay, remaining town services, all-floor bosses, accessibility signoff, or 1.0 readiness.
Implementation Summary #
Use recorded immutable initialization rather than mutable external vault reads during replay. Retain knowledge in terminal State long enough for a typed death transfer. Introduce a single artifact owner/controller shared by terminal and a bounded headless command runner. Store a durable terminal snapshot and idempotent death intent before retiring any Classic save. Allocate independent run IDs under an OS file lock, so repeating the same seed is not confused with retrying the same run's death.
Data and Lifecycle Decisions #
- Core knowledge authority: add
RunKnowledge { mapped_regions, recipes, translated_hymns }as deterministic sets of owned IDs to State and StateImage. ExistingState.idfremains the typed per-template use-count book; it is not a recipe-ID namespace. Remove death finalization's clearing of global IDF and its prose-only claim that a vault was written. Both known and tried entries remain in the terminal/current run; only known entries (count >=2) transfer across Classic lineage. NPC/monster death must not erase the delver's knowledge. New-run initialization, not arbitrary actor death, starts a fresh working book. Story restores checkpoint knowledge as part of its normal rollback. Preserve all unrelated melee/death/corpse/contract/bargain semantics. - Versioning: adding core knowledge changes the canonical hash/image authority contract: append an explicit knowledge domain and bump canonical hash version to 4, repinning goldens with documented independent verification. Run and replay formats become v4, retaining bounded header-first old/new rejection. Vault becomes v2. No silent migration is invented; v3 run/replay and v1 vault receive clear supported-version errors. These versions are pre-release, not a backward-compatibility promise.
- Typed inheritance:
InitialLineagecarries canonical sorted maps, recipes, translations, identified item-template IDs, ghost memories and auction-lot records available when the run begins. Embed it, plus immutablerun_id, in the run/replay initialization record. A fresh controller loads the selected vault once, snapshots it, and passes it into the deterministic initializer. Identified templates initialize IDF to 2; maps/recipes/translations initialize RunKnowledge. Effective item-knowledge queries consult the working IDF as well as instance identification; prove an inherited known template changes the real inventory/look knowledge result. No live items, wallet, reputation, debts or progression are copied from the prior delver. Resume and replay never read the vault. Validate content references where the content pack defines a corresponding namespace; do not relabel arbitrary recipe slugs as identified templates. - Current learning: initial Town and successful floor/Town transitions record canonical mapped-region IDs in RunKnowledge through the same dispatcher used by replay. Existing identification use-count mechanics remain authoritative; wire the existing read-only Inventory/Look controls to the effective knowledge query to prove inherited identification. This stage adds no new CommittedAction and inspection does not invent use counts or grant knowledge. Recipe acquisition and hymn translation verbs not already executable remain explicit later gameplay work; their typed knowledge still roundtrips/inherits correctly. A fixture seeded with a valid lineage record proves inheritance but must not be advertised as proving every acquisition mechanic.
- Ghosts and auctions: v2 vault uses typed ghost records (origin run ID, one valid item-template ID, one nonblank warning) and auction lots (stable lot ID derived from origin run/item IDs, original item attributes needed for later priced buyback, quoted base/buyback copper). These are records, never live actor/entity IDs or auto-equipped items in a descendant. Derive a ghost from actual terminal inventory/cause evidence; choose deterministically, not hardcoded lamp_oil. If no remembered item exists, do not fabricate one; report the limitation and retain the death record. Auction capture does not implement the later buyback verb. Keep that bible requirement open and preserve provenance/conditions so future purchase can reconstruct a new live instance legally. Include initial ghost/lot snapshot in replay identity so later deterministic encounters cannot consult mutable external data.
- Vault idempotency: store applied Classic death run IDs and a checked next-run sequence in v2 lineage metadata. Allocate a new monotonically increasing fixed-format run ID under the artifact owner's exclusive lock and persist allocation before publishing a new playable session; gaps after interrupted initialization are harmless, ID reuse is not. Run ID is external provenance recorded in identity, never a core RNG source. Vault knowledge merges are set unions; ghosts/lots key by stable origin IDs. Reapplying the same death with the same terminal hash is a no-op; a different hash for an already-applied run ID is a conflict error.
Frozen Schema and Budget Contract #
This subsection resolves the earlier descriptive shorthand. Implement these DTOs, limits and validation fixtures before switching emitted version constants. The v4/v2 constants change only once the new bodies and old-format rejection paths are ready; never emit a placeholder schema.
| Type | Exact semantic fields / canonical representation |
|---|---|
LineageId |
lineage- plus32 lowercase hexadecimal digits from16 OS-random bytes allocated once by ArtifactStore at fresh-vault creation; recorded provenance, never core RNG input. Add direct getrandom0.2 dependency for the I/O owner if needed. |
RunId |
run- plus exactly20 zero-padded decimal digits encoding nonzero u64; length24; parse/roundtrip must be identical. |
LotId |
{RunId}-item-{origin_item_id:020}; length50; origin item ID is nonzero u64, provenance only, never reused as a new live item ID. |
Id |
Owned ASCII String,1..96 bytes, first [a-z0-9], remaining [a-z0-9_.:-] plus hyphen. No whitespace, control bytes, slashes or path interpretation. |
RunKnowledge |
Core-owned mapped_regions:BTreeSet<String>, recipes:BTreeSet<String>, translated_hymns:BTreeSet<String> containing validated IDs. Working field caps4096/4096/1024; combined8192 rows; total ID bytes1MiB. |
InitialLineage |
Sorted unique mapped_regions, recipes, translated_hymns, identified_item_templates ID vectors; ghosts:Vec<GhostMemory> sorted by origin RunId; auction_lots:Vec<AuctionLot> sorted by LotId. No counters or applied receipts. |
GhostMemory |
origin_run_id:RunId, item_template_id:Id, warning:String (1..512 UTF-8 bytes, nonblank, no C0/DEL control characters). Key is origin RunId. |
AuctionLot |
lot_id:LotId, origin_run_id:RunId, origin_item_id:u64, template_id:Id, material:u8, identified:bool, cursed:bool, blessed:bool, condition:Condition using existing explicit core tag mapping, silvered:bool, illegal:bool, tags:Vec<String>, fuel:u8, wet_ticks:u64, base_price_cu:u32, buyback_price_cu:u32. Preserve tag order and exact instance fields; tags1..64 ASCII bytes each and at most32 per lot. Prices are nonzero where source content requires and buyback must equal canonical services::auction_buyback(base_price_cu). |
RunIdentity v4 |
Existing seed:u64/mode/ancestry/calling/display_name/content_hash plus lineage_id:LineageId, run_id:RunId, initial_lineage:InitialLineage. Existing explicit enum tags retained; display name still bounded4KiB/nonblank; content hash must equal the loaded immutable pack. |
DeathEffect |
Sorted terminal mapped-region/recipe/translation sets, sorted known identified-template IDs (IDF>=2 only), optional GhostMemory, sorted AuctionLot vector. Contains no wallet, reputation, debts, live actor IDs or automatic gear grants. |
AppliedDeathReceipt |
terminal_core_hash:u64, effect_digest:[u8;32]. Parent vault key is RunId; lineage_id comes from the vault. |
VaultV2 |
version:u32=2, lineage_id:LineageId, next_run_sequence:u64, current canonical InitialLineage collections, applied_deaths:BTreeMap<RunId,AppliedDeathReceipt>. JSON wire uses explicit receipt rows keyed by run_id, not a serde map that silently overwrites duplicate keys. |
PendingDeathV1 |
version:u32=1, lineage_id, run_id, terminal_tick:u64, terminal_core_hash:u64, terminal_snapshot_digest:[u8;32], content_hash:String, effect_digest:[u8;32]. |
CommittedDeathV1 |
All PendingDeathV1 fields, replay_digest:[u8;32], epitaph_digest:[u8;32], vault_effect_digest:[u8;32] which must equal effect_digest and the matching AppliedDeathReceipt. Immutable once published. |
Digest fields are fixed32-byte binary values, represented as exactly64 lowercase hexadecimal characters in JSON. SHA-256 inputs use separately named versioned domains, explicit length framing and deterministic field order; no Debug text or unordered iteration. DeathEffect digest includes every field including ordered item tags/prices. A death key is (lineage_id, run_id, terminal_core_hash), not merely seed or display name.
Initial/vault inheritable limits are field counts1024 maps,1024 recipes,256 translations,2048 identified templates,128 ghosts,256 lots; combined outer rows8192; total lot tags8192; total owned initial-lineage string bytes2MiB. Every initial string/row/tag also charges the existing full-envelope string/row/nested budget before allocation; no reset at images or initialization. Working RunKnowledge has headroom beyond inherited map caps so the canonical initial Town can always be learned. Vault applied receipt count is at most16,384. Managed run allocation is bounded to65,536 sequence numbers; next sequence starts1,65,537 is exhausted. Vault bytes cap8MiB includes metadata/receipts; marker bytes cap16KiB. No silent eviction, truncation or loss on cap failures. Merging equal keyed ghost/lot/receipt values is idempotent; a same key with differing fields is a conflict before file publication. Run/replay source duplicate or unsorted keyed rows reject; vault normalization of knowledge sets is explicit, while conflicting keyed records always reject.
The maximum inheritable projection must be tested through both run and replay with a64+64 chunk, otherwise minimally populated, valid Story pair. Test all category maxima, actual combined string/tag/row accounting, and encoded/raw backstops. This does NOT promise that every independent maximum of arbitrary additional current-state strings/events can fit simultaneously: the shared envelope budget is authoritative and an over-budget combination must fail symmetrically without dropping initialization or checkpoint data. Likewise a full vault merge exceeding its8MiB/global limits fails with the terminal snapshot/intent preserved for recovery.
Frozen Ownership, Allocation and Recovery Contract #
- State is the only mutable run-knowledge authority. Replace the old mutable
hexwork-save::Knowledgewith adapters over core RunKnowledge or explicitly legacy-only compatibility needed until main migration; no production shadow copy. Core exposes validatedlearn_mapped_region,learn_recipe,learn_translation,initialize_known_templateand effective identification lookupinstance.identified || idf[template]>=2. Inventory/look uses that real lookup. Initialization generates/preflights the same world with the unchanged seed/RNG sequence, initializes State/Town, applies gameplay inheritance once, learns the initial Town ID, then captures the initial Story checkpoint. Region IDs are exactlytown:{canonical_town_id}anddeep:{depth}(decimal depth1..12, no leading zero); validate referenced towns/floors against the selected pack/catalog. Initial Milthollow learning recordstown:milthollow. Recipe IDs remain opaque bounded IDs because no recipe-definition collection exists yet; translated hymns and identified templates must resolve in the pack. Successful transitions learn destination IDs inside the same session transaction before event/checkpoint post-state; failed transitions learn nothing. Resume restores current knowledge and never reapplies initial lineage. - Startup order is fixed: check platform command support before writes; resolve/create data root; open one lock handle and acquire nonblocking exclusive lock; bounded-load/validate the single authoritative vault; recover managed terminal/pending deaths updating that same in-memory vault; checked-reserve sequence N; atomically persist next_run_sequence=N+1; copy InitialLineage from this exact post-recovery vault; create canonical run; atomically publish
runs/<RunId>/run.save; only then expose a playable session. New-vault lineage ID comes from OS randomness once under lock. No second stale vault object may overwrite counter/receipts. BeforePublish allocation failure leaves N reusable; once counter publication succeeds N is permanently consumed even if world/run creation fails. PublishedSyncFailed freezes startup for digest verification/resync/reload; never assume the counter update did not happen. Death merge preserves the current next sequence. - Recovery accepts only files under the selected root's strict managed RunId directory. Vault/decoded identity/marker/receipt lineage ID and all run IDs must match that directory and each other. Terminal byte digest, tick/core hash and content identity are checked. Exact content comes only from the normal selected content-root resolution and must match the recorded content hash. Instance fields come only from the validated terminal player's actual inventory; immutable matching content supplies template validation and base prices. Different current content cannot reprice a pending death. If exact content is unavailable before completion, preserve files and return a blocked-recovery error. External
--replay FILEis read-only and never a recovery source. - Derive DeathEffect deterministically from terminal State knowledge/known IDF, actual terminal player inventory and causal evidence; no prose parsing. Eligibility is exact: every item in the terminal Player's authoritative Pack with template other than the synthetic
corpsetemplate is included; it must resolve and be priced from the exact matching content pack or the whole effect fails before vault mutation. Exclude only synthetic corpse items and items not in that Pack; never silently skip an unresolved non-corpse item. Ghost remembers the lowest Instance.id in this set. Itsghost_warning_v1helper selects the latest cause with ruledeath, otherwise the last cause; rendersrule: detail, replaces control characters with spaces, collapses whitespace, and usesThe fallen delver left no readable warning.if the normalized result is empty/no cause exists. If text exceeds512 UTF-8 bytes, retain the largest character-boundary prefix fitting509 bytes and append the three-byte ellipsis character. If the eligible set is empty, omit the ghost with an explicit recorded diagnostic; do not fabricate an item. Auction every eligible carried item exactly once into a distinct provenance-only lot. Record all original lot fields; insufficient cap/content/price validation fails before the vault changes. AppliedDeathReceipt is published in the SAME atomic vault replacement as its full merged DeathEffect.
| Observed recovery state | Required transition |
|---|---|
| Living snapshot, no marker/receipt | Normal managed resume allowed after identity/content validation. |
| Dead snapshot, no pending or committed | Validate terminal and exact content, derive effect/digest, durably create PendingDeathV1. Do not expose as living. |
| Pending + terminal, no applied receipt | Check identity/file/core/effect digests; create or verify replay and epitaph; atomically merge effect AND receipt in vault. |
| Pending + matching applied terminal/effect receipt | Skip merge, verify/recreate matching artifacts from terminal as needed, publish matching committed receipt. |
| Same applied run ID but different terminal or effect digest | Conflict; preserve evidence and refuse mutation/retirement. |
| Committed + matching artifacts + matching vault receipt | Retry run.save retirement and pending cleanup; committed record remains permanent. Verify any remaining terminal/pending against receipt before deletion. |
| Missing terminal before a valid committed receipt | Unrecoverable conflict; do not reconstruct it from replay or guess lineage effects. |
| Missing terminal after fully matching committed/artifact/vault receipt | Cleanup may continue without rederiving effects; no content repricing or new merge. |
| Any missing/mismatched required artifact or vault receipt after terminal is gone | Conflict; keep remaining receipts and refuse cleanup that would destroy evidence. |
| Temp files from interrupted writes | Never treat as committed input. Only owner-named verified orphan temps may be cleaned; do not glob-delete unrelated files. |
Every immutable pending/committed or artifact retry compares exact canonical bytes/digests under the lock; matching destination can be synced/reused, mismatches conflict. Artifact bytes are from encode_replay and deterministic epitaph/cause rendering, not encode_current_run renamed. Durable deletion is allowed only after all matching evidence is verified. Recovery iterates at most65,536 strict run-directory entries and at most64 incomplete transactions per request. Lstat-only inventory/count/counter checks precede payload decoding. Because an unmarked snapshot may be living or terminal, add trusted max_snapshot_inspections=64 rather than decoding an unlimited unknown set to discover its status, and max_artifact_verify_bytes=256MiB for aggregate archive/artifact verification reads. Candidate planning retains small summaries, not a vector of full decoded worlds; re-read and digest-check one candidate at commit as needed. No file/directory sync occurs in read-only inventory/planning. Excess is an actionable maintenance error before replay work or mutation, not unbounded scanning or automatic data deletion. All replay/proof work within recovery shares an explicit trusted finite request budget; exceeding it retains terminal/pending files and requires an explicit higher-limit retry.
Frozen Durability and Platform Policy #
Replace the production use of the current best-effort writer with an artifact durability API: replace returns either Durable or an error classified BeforePublish / PublishedSyncFailed; removal distinguishes BeforeRemove / RemovedSyncFailed / Durable. Same-directory exclusive temp creation, file write/flush/sync, atomic rename over a regular destination, then parent-directory sync are required for Durable. Reject symlink/nonregular managed targets and directory destinations; exclusive temp creation must not follow an existing temp symlink. Do not claim race resistance against uncooperative external writers beyond the stated advisory lock model.
After rename-success/sync-failure or unlink-success/sync-failure, report durability uncertain, freeze play, and retry by reading/verifying the resulting bytes/absence under lock and syncing again. Do not attempt a compensating overwrite with stale state or assume publication/removal did not occur. The controller updates its authoritative in-memory vault from the verified published outcome before another mutation.
Linux/macOS mutating commands are the initial supported implementation, with target-specific replace/create/remove/directory-sync tests. Windows new, --load, headless play, recovery, and file export refuse with unsupported-durable-writes BEFORE creating/opening a writable artifact root or taking terminal input. Windows read-only --help, --arena, --replay FILE and stdout-only replay rendering remain available. The Windows release/distribution gate remains open; it is not removed. Network-filesystem and sudden-power-loss guarantees remain uncertified even when OS sync calls succeed. fs2 is advisory and its lock lifecycle is tested with real child processes; existence of the lock file never implies lock ownership.
Frozen Headless Grammar and Process Evidence #
Script v1 begins with exactly HEXWORK-COMMANDS 1; subsequent nonempty lines are one of input <key>, advance <u32>, save, observe <kind>, quit. ASCII tokens only, spaces as separators, no quoting/comments/trailing arguments, max256 bytes/line,8MiB/file,65,536 commands. Allowed keys map to existing normalized Input: north/south/east/west/northeast/northwest/southeast/southwest, confirm, contracts, escape, wait, pause-step, inventory, look, map, chapel, camera, pick1..pick6. No actor IDs, raw committed-action payloads, paths, grant/set/kill/teleport/eval commands. advance allows0..10,000; cumulative request budgets default10,000 full steps,50,000 unattended iterations,100,000 elapsed ticks, and65,536 actual actions. Inn/Wait costs are charged semantically before dispatch, not taken from script claims. Auto-lifecycle work and proof/replay also use bounded budgets; failed command stops later commands and returns a stable error. Explicit saves capped64 and state-hash observations capped64 per request to avoid free unbounded I/O/hash work.
Observe kinds are state, inventory, knowledge, artifacts. Output is flushed line-delimited JSON with schema_version1, record_kind, command_index, run_id, tick, status/code. Ordinary command acknowledgements use a cheap labeled diagnostic hash, not a full-world scan per input; observe state, save and terminal records include canonical hash plus its tick. Inventory/knowledge reports use bounded counts/digests and at most64 presented entries, with explicit truncation metadata; authoritative saved data is never truncated. Output record max16KiB and total stdout8MiB/request; evidence is bounded by accepted ReplayLimits. Emit artifact milestone records only after the relevant operation reports Durable. Stable process exit classes:0 success,2 usage/parse,3 content/format/integrity/mode,4 I/O/durability,5 runtime/limit,6 lock contention or platform refusal. Diagnostics go to stderr, never a false successful replay fallback.
A reusable crates/hexwork-cli/tests/support harness spawns CARGO_BIN_EXE_hexwork, supplies private HOME/CWD/data/content roots under /var/tmp, drains bounded output, and uses deadline-based waits for acknowledged milestones. To interrupt, kill only after an observed durable milestone; no arbitrary sleeps used as proof. A private I/O observer/failure seam may exercise exact post-rename/post-unlink errors in unit/integration tests; it never mutates simulation or ships arbitrary execution/hidden death commands. Terminal main and headless runner share the same application controller, action adapter, tick/lifecycle policy and artifact owner. quit and clean script EOF take the same controlled save-and-exit path for a living/Story-resolved run; no unsaved successful EOF. A pending Classic death must recover/finalize or return an error, never become a living quit-save. Already-clean current state need not be rewritten just to exit.
Implementation Plan #
- Core retention/schema node (core owner). Implement RunKnowledge and explicit StateImage/hash coverage; retain IDF across all actor deaths. Update deterministic golden/version tests, death regressions and image validation. No filesystem/serde in core. Empty knowledge is the canonical default. Expose bounded, deterministic knowledge helpers rather than multiple shadow authorities.
- Persistence v4/vault v2 node (save owner, after core API). Replace top-level initialization/vault DTOs with the complete typed shapes above; reuse existing State/action/event bounded adapters and latest-checkpoint semantics. One shared encode/decode budget covers initial lineage, events, current StateImage and Story checkpoint. Keep accepted per-image64/shared128 chunks, nested16,000,000, raw256MiB, compressed64MiB and event65,536 ceilings. Keep string4KiB and aggregate16MiB constraints; initial lineage must share, not reset, them. Vault retains an8MiB serialized cap and bounded per-collection limits; enforce file read caps before allocation/parsing. Use real v3/v1 fixtures for header-first explanatory rejection. Structural duplicates/order errors in run/replay reject; vault merge normalization remains explicit and deterministic. Publish exact DTO/helper signatures before session mutation.
- Session knowledge/tick node (session owner, after v4 API). Initialize recorded inheritance deterministically and validate immutable identity/current knowledge at restore. Keep original StateImage/latest-checkpoint/lifecycle proofs and all existing acceptance tests. Make State private to GameSession and provide observational accessors, liveness/death observation, and checked single-tick advancement under session authority. External callers cannot mutate State. Ordinary elapsed ticks remain gaps in lifecycle history, not fake player actions; each tick validates preconditions, performs one core step, reports actual advancement/death, and lets the controller stop on the first player death. Update Wait/Inn accounting consistently; preserve zero-tick action semantics. Any new genuine identification action needs an explicit event tag/timing and replay test, never unrestricted state mutation.
- Artifact-store node (separate save/CLI I/O module, after v4 shape). All play/resume/death writers hold one nonblocking exclusive OS lock for the selected data root for the process lifetime. Use
fs20.4.3 FileExt fully qualified calls for Rust1.75-compatible locking; source docs confirm advisory Unix flock/Windows LockFile semantics. Open one lock handle, do not clone/relock it, and prove lock release after child exit/crash. Lock-file existence is not ownership; no PID-based stale-lock deletion. Replay is read-only and needs neither vault nor store lock. Advisory locks do not defend against hostile writers, and network filesystems are not certified. - Paths and bounded readers (artifact/CLI owner).
--data-dir PATHselects one lineage artifact root; otherwise use the platform user-data directory (XDG_DATA_HOME/hexwork, Linux HOME fallback, Windows LOCALAPPDATA, macOS Application Support), never implicit CWD writes. Validate environment-root rules and make errors actionable.--content-root PATHthenHEXWORK_CONTENT_ROOTthen packaged executable-adjacent assets are preferred; a verified compile-time checkout root may be a clearly logged development fallback only. Missing content is an error; never substitute stored content hash. Managed run files live underruns/<fixed-format-run-id>/;--load RUN_IDreads only that managed run.--replay FILEmay read an explicit external path with the replay byte cap; it never treats decode failure as printable successful replay. Bound reads of vault, run, replay, command script, recovery markers and crash payloads using capped streaming/read-to-limit-plus-one helpers, not full fs::read before checking. - Classic death transaction (controller + artifact store). On detecting dead Classic/Iron Player, freeze dispatch and persist the exact terminal run snapshot atomically first. Then atomically write a small versioned
death.pendingintent referencing that run ID, terminal tick/hash and expected content identity. The terminal snapshot is the authoritative source of typed knowledge/gear/cause transfer; do not parse prose. If crash occurs between snapshot and intent, startup detects the dead managed run and reconstructs the intent. Prepare/write production.hexdemousing the replay encoder, plus deterministic epitaph/cause artifact. Merge knowledge/ghost/auction delta into the currently locked vault idempotently and persist it. Only after successful artifacts and matching vault receipt, write adeath.committedreceipt containing terminal and artifact digests, then retirerun.saveand remove the pending marker. Keep the committed receipt permanently for managed-run liveness. Recovery runs before any managed resume/new playable session: reject ambiguous/corrupt conflicts, finish pending stages, never load a pending/committed dead run as alive. Existing artifacts with matching digests may be reused; mismatches are errors, not overwritten blindly. On any failure retain the terminal snapshot/intent and return an actionable recoverable error. Do not delete the last recoverable save merely because a demo or vault write failed. Define fsync/rename/remove durability results precisely; single-file atomicity is not a multi-file transaction. Apply the frozen Windows mutation-refusal policy below the schema section; do not implement an unreviewed replacement fallback or present Linux tests as Windows durability certification. - Story death handling (controller). After a dead tick/action call the accepted session resurrection method, retain history/debt/checkpoint, reset purely interface/controller state, and atomically save the resulting current run. No Classic vault merge/run retirement on Story death. Stop further queued play inputs until this lifecycle boundary succeeds. Scheduler autosave tracks elapsed advancement since last successful save and fires once on crossing600 ticks, including a bulk Inn action; repeated render frames at the same tick do not rewrite files. After rollback/load reset interface cadence deliberately; cadence is not sim authority. Failed saves remain retryable without advancing the sim to make them succeed.
- Frontend wiring and process runner (CLI owner). Introduce a small application/controller module owning GameSession and ArtifactStore and use it from terminal main and a bounded
--headless-script FILEmode. Scripts contain only real normalized input/actions, bounded elapsed advancement, explicit save/quit, and observations—no direct HP/position/knowledge injection, arbitrary file paths or test-only death action. Max65,536 commands and8MiB script bytes; per-command elapsed work is charged to trusted finite limits. Reuse PlayController for contextual interactions and the same session action/tick/lifecycle dispatch. Add--load RUN_ID,--data-dir,--content-root, and--recover(recovery without new allocation). New-run-only--name, numeric--ancestry 1..4, and--calling 1..6select existing chargen choices; headless defaults are Mudborn/Lampbearer andthe delver, while interactive unspecified choices keep chargen. An explicit trusted work-limit override may raise replay/recovery ceilings with checked arithmetic; scripts/files cannot raise them. Loaded/replayed mode and identity are authoritative when no explicit constraint is supplied, otherwise mismatch errors. Reject conflicting --story/--daily rather than silently selecting one. Remove production uses of legacy header/demo/inline resurrection. Keep--arena --seedexplicit. Reports expose bounded deterministic hashes/events/knowledge/artifact status for tests; no hidden state mutation seam. - Crash state and presentation (CLI owner). Install the panic hook with a bounded last-committed snapshot updated at normal controller boundaries: run ID/seed/tick, last50 log lines, a current cheap diagnostic hash plus last canonical hash and its tick. Clearly label hash kind/freshness; do not add full-world hashing every tick. Use nonblocking lock access in the hook to avoid reentrant deadlock and write only to the selected artifact root with bounded output. Prove the hook through a separate process test harness; do not ship a player-death cheat or generic arbitrary execution flag. Consume session replay evidence for exported/viewed text/ANSI and live cause deltas for logs; remove dead API aliases where genuinely unused instead of suppressing strict lints.
- Integration/review/commit (parent). Core schema precedes save schema; session and artifact work may run in parallel only with disjoint files and frozen DTOs; main/controller integration follows both. Restore workspace compilation between nodes or explicitly label temporary staging; no completion commit while API migration is broken. Run strict workspace gates, independent reviews and all named process tests. Commit accepted layers promptly; do not accumulate another unreviewed monolith.
Acceptance Criteria #
- AC.1: monster/NPC and Player death retain exact typed run knowledge/IDF; known-only lineage delta can be derived from terminal authority without parsing logs; unrelated death semantics remain valid.
- AC.2: descendant initialization uses a recorded bounded initial lineage snapshot; actual identification visibility and maps are inherited, equipment/currency/debts are not; replay succeeds after external vault removal/change.
- AC.3: v4 run/replay/corehash and v2 vault encoding/decoding/identity/budgets are explicit and symmetric; old formats reject clearly before new body decode; recorded knowledge survives current and Story images.
- AC.4: all runtime mutation/ticks/death handling flow through GameSession/controller; each scheduled-tick death is handled without requiring another input; mode constraints are consistent.
- AC.5: managed data/content roots are deterministic and explicit; no accidental CWD artifact writes; every file input is bounded before parsing/allocation.
- AC.6: Classic death is recoverable/idempotent across each multi-file boundary, preserves the last recoverable terminal snapshot on failure, and never reopens a pending/retired run as living.
- AC.7: Story process death restores the saved town checkpoint and debt, continues with retained lifecycle events, and does not execute Classic retirement.
- AC.8: real binary headless/terminal paths share controller/initializer/dispatcher; save-process-exit-resume produces exact canonical state, and production replay applies events rather than actorless restepping.
- AC.9: typed ghost/auction provenance is stored without automatically granting live gear; buyback gameplay remains explicitly open until implemented.
- AC.10: autosave crossing/cadence, crash artifact freshness, lock contention/release, and unsupported-platform errors are observable and tested; strict workspace clippy has no unused API suppression.
Test Strategy #
- AC.1:
npc_and_monster_deaths_preserve_delver_knowledge,player_death_retains_typed_terminal_knowledge,terminal_lineage_delta_excludes_tried_identifications, retained melee/death/ID/corpse tests, explicit new core hash/version and knowledge-domain mutation tests. - AC.2:
descendant_known_template_changes_real_inventory_knowledge,initial_lineage_maps_recipes_translations_roundtrip,descendant_replay_ignores_removed_or_changed_vault,descendant_does_not_inherit_live_inventory_wallet_or_debts. Acquisition fixtures must be identified as fixtures, not complete gameplay verbs. - AC.3:
real_v3_run_replay_and_v1_vault_reject_before_new_body,initial_lineage_and_dual_images_share_all_budgets,knowledge_and_identification_survive_story_rollback, all retained malicious-prefix/bomb/order/metadata/54+54/hash-continuation and lifecycle tests. - AC.4:
scheduled_death_is_handled_without_another_input,queued_input_stops_at_death_boundary,story_and_daily_modes_reach_session,explicit_mode_mismatch_rejects, compile-time/private State ownership audit plus actual controller-only advancement tests. - AC.5:
private_data_root_receives_all_artifacts,launch_outside_checkout_uses_explicit_content_root,missing_content_never_uses_recorded_hash_as_fallback,oversized_file_reads_stop_at_cap_plus_one, invalid run ID/path escape tests. Test bounds with small-cap helpers and real capped production readers; do not allocate giant files into memory. - AC.6:
classic_demo_vault_receipt_precede_save_retirement,death_failure_keeps_terminal_save_and_intent,recovery_after_each_death_commit_boundary_is_idempotent,already_applied_death_with_different_hash_conflicts,pending_or_retired_run_cannot_resume_as_alive. Use deterministic file/directory collision failures and private I/O-stage seams plus real child restart; chmod-only failures are insufficient under privileged execution. Atomic writers may have unit failure injection, but final process proof uses actual files and no direct simulation mutation. - AC.7:
binary_story_deep_save_resume_death_recovers_town_and_debt,binary_story_repeated_resurrection_replaysusing real route/input scripts and reciprocal melee. - AC.8:
binary_save_exit_load_preserves_hash_and_history,binary_replay_applies_events_and_checks_terminal_hash,binary_replay_rejects_corrupt_old_or_mismatched_content,headless_and_terminal_controller_intents_agree; use actual executable child processes with private HOME/CWD/data roots under /var/tmp. Killing a child is synchronized to acknowledged I/O milestones, never arbitrary sleeps presented as deterministic interruption proof. - AC.9:
death_ghost_uses_real_item_and_warning,auction_lots_preserve_origin_and_condition_without_live_ids,death_retry_does_not_duplicate_ghosts_or_lots,death_effect_captures_every_eligible_pack_item_once,death_effect_excludes_only_frozen_ineligible_classes,ghost_warning_canonicalization_is_stable_and_bounded; keep real auction-buyback gameplay gate unchecked. - AC.10:
autosave_crossing_600_and_inn_bulk_advance_writes_once,same_tick_render_does_not_repeat_autosave,panic_subprocess_writes_bounded_live_snapshot,second_writer_fails_cleanly_and_crash_releases_lock,binary_arena_seed_changes_hash, platform-specific atomic replacement tests. Run cargo fmt --check, workspace all-target tests, strict workspace clippy -Dwarnings and applicable release hash/replay timings from final exact staged snapshots; no source dead-code allowance to make the production gate pass.
Required refinements of the broad AC mappings above (each names an observable regression, not a placeholder category):
- AC.1/AC.3:
corehash_v4_changes_for_each_runknowledge_category_and_idf_count,corehash_v4_preserves_all_v3_domain_order_via_independent_golden_encoder,same_seed_empty_vs_nonempty_lineage_preserves_world_and_rng. - AC.2/AC.3:
max_valid_vault_projection_fits_run_and_replay_with_64_plus_64_images,lineage_category_cap_plus_one_rejects_before_allocation,lineage_strings_share_identity_events_and_dual_image_budget,auction_lot_all_fields_roundtrip,duplicate_or_conflicting_lineage_keys_reject,initial_lineage_does_not_change_world_catalog_rng_or_starting_kit,initial_town_learning_is_in_initial_checkpoint,successful_transition_learning_is_in_event_post_state,failed_transition_does_not_learn,resume_does_not_reapply_initial_lineage,latest_story_checkpoint_retains_knowledge_after_multiple_returns,v4_replay_preserves_latest_story_checkpoint_reference. - AC.5/AC.6:
recovery_rejects_substituted_run_same_seed,recovery_rejects_directory_identity_mismatch,recovery_requires_exact_recorded_content_for_auction_price,changed_current_content_cannot_reprice_pending_death,external_replay_cannot_satisfy_managed_recovery. - AC.6:
recover_after_terminal_rename_before_pending,recover_after_artifact_rename,recover_after_vault_rename_before_committed,recover_after_committed_rename_before_run_unlink,recover_after_run_unlink_before_pending_unlink,applied_receipt_same_hash_different_effect_conflicts,committed_receipt_requires_matching_vault_effect,cold_recovery_without_terminal_snapshot_refuses_before_commit,cold_cleanup_without_terminal_snapshot_requires_fully_verified_commit. - AC.6/AC.10 allocation:
allocation_failure_after_vault_commit_consumes_id_and_retry_uses_next,allocation_failure_before_vault_commit_reuses_uncommitted_id,death_merge_preserves_next_run_sequence,counter_overflow_is_nonmutating,startup_recovery_precedes_lineage_snapshot_and_allocation,sequential_processes_allocate_unique_run_ids,allocation_sync_uncertainty_reloads_counter_before_retry. - AC.8:
headless_grammar_rejects_unknown_trailing_and_overflow,advance_cap_and_checked_total_work_reject_before_tick,multiple_commands_charge_all_work,command_failure_stops_later_inputs,milestone_ack_occurs_after_durable_boundary,headless_has_no_state_injection_or_arbitrary_path_command,observation_and_save_request_caps_reject_before_expensive_work. - AC.10 durability/platform:
durable_replace_existing_and_first_create,durable_replace_rejects_directory_and_temp_collision,post_rename_sync_failure_is_uncertain_and_retried_by_digest,post_unlink_sync_failure_is_uncertain_and_retried_by_verified_absence,windows_mutating_commands_refuse_before_any_write,windows_read_only_replay_needs_no_artifact_store. Linux/macOS writer tests and Windows refusal/read-only tests are target-gated and must run on their targets before platform certification.
All tests/builds use TMPDIR=/var/tmp; no push or release claim is implicit. Re-run final full suites after the last functional edit, not only before it.
Frontend checkpoint review — corrections in progress #
Checkpoint f061426 is review pending, not Stage 3c2 acceptance. The final implementation report and independent CLI reruns passed 106 library tests and 10 real-process tests. Parent also exercised the actual binary in an 80×24 Linux PTY: interactive choices preceded lineage allocation, melee warning appeared, Map/Tab reached their terminal paths, and quit created managed save/vault files. This is functional TTY evidence, not visual/accessibility or cross-platform certification.
Independent application review rejected the checkpoint; presentation review required scoped corrections. Parent directly confirmed full canonical hashing in per-frame panic refresh, queued terminal inputs surviving Story rollback, fabricated lamp/burden HUD values, and unbounded Command::output() in the process harness. Required corrections:
- Install an atomic managed-signal receipt latch before managed startup. Observe it before root mutation/recovery and again after recovery before allocation/new/load and exposure of play. During play stop new dispatch and perform controlled save or terminal recovery on the main thread. Observe signals between bulk ticks; synchronous I/O completes or fails before the next safe check. No async-handler filesystem work or simulation mutation.
- Cache last canonical hash and its tick at legitimate canonical boundaries; per-frame crash refresh copies the cache and cheap diagnostic hash, never scans the full world. Bound crash formatting before allocation/append and preserve UTF-8 boundaries. The hook uses nonblocking lock access; best-effort filesystem calls do not carry a bounded-latency or crash-durability guarantee.
- Expose lifecycle boundaries so terminal drops the remaining drained pre-death input batch and resets elapsed scheduling after Story rollback. Each headless line remains a distinct sequential command: later lines may continue after a successfully saved resurrection; a single advance stops at first death and never spends its remaining pre-death ticks after rollback.
- Replace string-based exit classification with explicit classes; classify bounded-read I/O as 4 and size/work limits as 5. Refuse Windows file replay export before writes. Reject silently ignored standalone-operation options.
- Render authoritative light/burden and contextual hints, stable melee-warning placement, truthful tactical timing status, and cell-width-safe Unicode lines. Verify Story pause-step policy against the bible before wiring it; do not claim pause from a label alone.
- Replace process capture with concurrent bounded drains and monotonic deadlines, including kill/reap on overflow/timeout. Test timeout and cap enforcement while the child is running, not only after exit.
Evidence boundary: actual binary tests prove positive Classic milestone ordering, save/load, death-produced replay, descendant map inheritance without automatic gear, and repeated Story resurrection/resume. ArtifactStore eight-boundary reopen tests and its killed test-helper process are component/compositional recovery evidence, not eight interrupted hexwork executions. Exact binary interruption, panic subprocess/freshness, further negative reader/identity cases, and detailed lineage consequence checks must be tracked at their actual evidence level. A parent-observed ACK followed by kill alone does not prove that no subsequent durable operation occurred before the kill.
Correction verification checkpoint #
The frontend correction milestone is accepted after independent UI/queue/harness acceptance, application/signal/error/crash closure, and a fresh parent run of workspace formatting, strict locked workspace all-target clippy, and locked workspace all-target tests after the final terminal-signal edit. This closes the reported frontend review findings, not the full Phase 9–11 production acceptance matrix or 1.0 ship gate. Node A/B corrections include origin-typed session errors (including nested runtime and persistence-limit propagation), a real managed-application panic fixture, and a genuine Story death queue regression.
- Parent reran the bounded process-harness tests: 7 passed. Final harness cleanup also passed strict CLI all-target clippy without suppression. Limits are enforced during concurrent drain, with finite process/cleanup deadlines; inherited descendant pipes can still leave detached readers until those pipes close, and whole-process-tree cleanup is not claimed.
- The Story queue regression passed through
dispatch_drained_batch, the same adapter now used by terminal input. Actual RuntimePack/worldplan routes and ordinary reciprocal melee reach the first Story death; the queued movement is discarded, the same movement works as a fresh batch, and independent managed reload matches the saved resurrection boundary rather than the later unsaved move. This is production-adapter/Application integration evidence, not a PTY burst. - Crash tests now distinguish oversized formatter proof from a managed-Application panic subprocess. The latter executes normal Wait, captures actual identity and hash metadata, updates the hook snapshot, and proves current tick 1 versus cached canonical tick 0 under correct labels. Four focused crash tests passed. The subprocess is the Rust test executable, not a shipped panic CLI flag or actual
hexworkpanic. - Parent actual-binary Linux 100×24 PTY smoke passed: new Story with explicit choices reached play and an initial managed save; SIGTERM produced
signal-received/exit 4 and retained a nonempty managed save; a second actualhexwork --loadprocess resumed and quit with exit 0. Capture was bounded at 4 MiB and each child had a monotonic deadline; private/var/tmproot removed. This proves a play-boundary signal/save/resume path, not all startup/commit signal windows or platform certification. - Node B reported 155 UI library tests and eight targeted correction tests passing; its attempted full CLI library run timed out and is not counted as passing. Parent default-toolchain strict CLI/UI all-target clippy passed after integration; final full workspace gates are rerun after the last constructor refinements.
Independent UI/queue/harness closure review accepted its scope without findings. Parent final workspace formatting, strict locked all-target clippy, and locked workspace all-target tests all passed on that correction tree. Application/signal/error/crash closure nevertheless rejected one remaining HIGH defect: a signal latched during successful Classic terminal recovery can cause headless finalization to start the terminal transaction again (committed-marker conflict), while terminal's immediate game-over exit can omit the signal result. All other items in that focused application review were accepted.
That HIGH finding is now closed and independently accepted. TerminalLifecycle::{Active, DispatchFrozen, Completed} distinguishes the pre-persistence dispatch freeze from successful terminal recovery; Completed is assigned only after recovery succeeds. Only completion permits signal finalization to return signal-received/class 4 without repeating the transaction. Terminal and headless observe latches after lifecycle boundaries and before successful exits; incomplete errors still propagate. New ordinary-route managed Classic and Story regressions trigger a private output-observer latch at real durable records, verify class 4, and prove terminal milestones/artifacts are not repeated or changed by a later finalizer. Reviewer reran both tests successfully. Parent then reran workspace formatting, strict locked all-target clippy, and full locked workspace all-target tests successfully after the last functional edit. These latch regressions are managed-Application/adapter evidence, not OS-signal or exact binary-interruption certification.
Full game release, broad accessibility, exact multi-window binary crash evidence and unsupported-platform certification remain separate gates.
Next production-evidence tranche (after accepted frontend correction) #
The next test-only work closes bounded gaps without adding gameplay or changing accepted persistence semantics:
- Negative real-binary inputs and load constraints (new nested process-test module). Exercise missing script/replay as I/O class 4; oversized script/replay via sparse cap-plus-one files as limit class 5; invalid managed RunId/path-shaped inputs as usage class 2; explicit load seed/mode mismatch and corrupt managed run/vault as format/integrity class 3. Include real old-format fixtures and changed-content mismatch where existing fixtures permit a bounded faithful test. Every case must assert no successful play/observation and preservation of preexisting evidence; cases rejected before root opening must prove no artifact root creation. Preserve the distinction between malformed input fixtures and ordinary gameplay evidence. No large literal in-memory cap fixtures, script-raised work limits, or production parser bypass.
- Classic/descendant/provenance assertions (existing real-death process scenario). Reuse ordinary generated route and reciprocal melee. Capture a real prelethal managed save where needed, then kill through a normal loaded input. Check all eligible inherited provenance fields against actual saved item authority, known-only knowledge, lowest-ID ghost selection and canonical warning, fresh descendant kit/wallet/no debt, unique run IDs, retired-run load refusal, and repeated actual
--recoverwith unchanged receipts/ghosts/lots/artifacts. Replay the death-produced archive after changed as well as removed external vault. Where a category cannot be acquired by current normal gameplay, report the missing category or clearly separate a recorded-lineage fixture; do not invent an identification/recipe action or count a fixture as acquisition gameplay. - Read-only recovery-evidence investigation. Determine which exact CLI failure/restart states can be forced with ordinary filesystem collisions or bounded OS control under the current startup preflight. Distinguish deterministic operation failure from process interruption. No hidden pause/kill/injection verbs or changed production semantics merely to make tests pass. ACK-then-kill cannot establish an exact inter-operation stop; inspect actual surviving files and report only achieved states. Freeze any subsequent implementation contract from concrete findings.
Parent verification corrected the recovery investigation: Application::load_with_output_checked calls recovery before managed load, and recovery inventory classifies all destinations before its living-snapshot skip. Therefore a precreated directory at death.pending or replay.hexdemo is startup rejection, not an interior death-transaction failure. The next recovery module instead tests startup destination collisions, future-sequence directory rejection with unchanged allocation counter, and the source-grounded hypothesis that a conflicting regular archive file passes living-snapshot startup but blocks later death recovery after terminal/pending publication. Actual observed milestones and surviving bytes must establish that hypothesis; no result is presumed. It also tests ordinary-input Inn sleep crossing autosave cadence without raw advance commands.
These nodes use private /var/tmp roots and the accepted bounded process harness. Their ownership is disjoint; parent registers the new nested module after its file exists, integrates results, and runs the actual binary tests. New test failures exposing runtime defects are reported for a separate narrow correction rather than changing expected exit classes to hide them. Full Phase 12, acquisition verbs, auction buyback, volume and ship gates remain open.
Production-evidence tranche verification — accepted bounded tranche #
- Parent ran the registered
negative::actual-binary suite: 7 passed. Cases cover missing files, sparse cap-plus-one inputs, malformed run IDs, explicit load constraints, retained corrupt evidence, genuine old formats, and changed content. Independent read-only review accepted this module. - The strengthened Classic process scenario passed, and independent review accepted its scope: ordinary-route prelethal save/load then final confirm; full replay prefix plus one added event; auction fields compared to the actual prelethal pack under unchanged melee semantics; lowest-ID ghost and warning equality with emitted epitaph cause text; retired load refusal; unchanged retained artifacts/effects after two recoveries; archive replay with changed/removed external vault; sequential descendant ID, kit template composition, default wallet, no chapel debt, and exact recorded InitialLineage. This is not full equipment-field equality, all-debt-system coverage, a standalone general terminal-pack theorem, or proof of no transient retirement-stage churn. Recipe/translation/known-template acquisition sets are empty on this route and remain explicitly unproven by normal gameplay.
- The registered recovery/Inn module passed all four tests after correcting test lifetime and canonical future-RunId fixture errors. Actual binary tests prove startup destination-type rejection, counter-preserving future-sequence collision/retry, and a regular archive conflict after terminal/pending publication with preserved evidence and successful independent recovery after removing only the obstacle. Recovery-path validation makes the retained terminal and pending matching valid authority before the specific replay mismatch; the test does not directly decode those two files. This is deterministic operation-failure/restart proof, not an exact process-crash instant.
- Independent review caught an initially vacuous Inn autosave assertion (quit could have supplied the save). The corrected exact Inn test passed and reviewer closure accepted it: the tick-8,640 snapshot belongs to the dynamically derived Inn confirm command, precedes its acknowledgment, and quit emits only
save-clean. The ordinary route uses no raw advance, pays 8 copper from 25, and records one Inn service event. - Parent final workspace formatting, strict locked workspace all-target clippy, and locked workspace all-target tests passed after the last assertion edit. Independent negative/Classic/collision and Inn-origin closure reviews accepted this bounded tranche. No production runtime changes were required. Broad acquisition verbs, all-debt/equipment semantics, exact interruption windows, cross-platform certification, live Phase 12 integration and ship acceptance are not inferred from these results.
Phase 12 continuation #
The first live physical-key remapping node is now accepted; see phase12-wiring-plan.md for its reviewed contract and evidence. Parent review corrected the initial research's unbounded-loader and single-input-callsite assumptions before implementation. The production loader now enforces a 64 KiB/type-checked missing-only contract, rooted at selected data directory; both chargen and play use one immutable mapped backend with truthful labels. Actual PTY new/load tests validate identity and saved movement. Independent term/CLI closure and parent post-edit full workspace gates passed. Normalized headless grammar and canonical replay authority remain unchanged.
Live boot presentation preferences are now accepted as well: strict bounded presentation.json, interactive gamma/FOV/five-palette controls, explicit writes under the application-owned root lock, source-change protection, actual render-helper pixel/byte invariance evidence, and PTY persistence/reload/lock-order tests. A parallel renderer correction carries explicit tactical slice metadata instead of inferring depth from eye height, with negative-depth and zoom pixel regressions. Independent reviews and parent final combined workspace gates passed; see phase12-wiring-plan.md.
Bounded live recent feedback is now accepted too: cell-safe chronological recent messages, explicit clipping/omission, player-facing inventory summaries, and private direct-input rejection handling that preserves fatal/headless semantics. Source matrices, real PTY/headless tests, independent closure and parent final workspace gates passed. This is not full searchable history or semantic screenreader narration.
A real read-only carried Inventory/Look browser is now accepted, with bounded stable-ID paging, truthful bounded tooltips, modal lifecycle/clock handling and actual PTY/headless purity evidence. Independent query/UI/caller closures and parent final workspace gates passed. Closing and first-world rendering use true all-cell redraw, including blank cells; opaque clearing covers the physical viewport beyond UI caps.
Confirmed own-cell pickup/drop is now accepted; see phase12-wiring-plan.md T.0–T.13. Core/Session atomically move exact instances between existing ownership tables after shared structural preflight; tags 9/10 deliberately extend the existing v4 action vocabulary without changing state hashing or invalidating genuine old-v4 fixtures. Explicit prompts, optional bindings and terminal batch barriers are live. Ordinary generated-loot binary/80×24 PTY tests, independent load/replay, actual Story checkpoint rollback and Classic terminal/pending/archive-conflict recovery are reviewed and passing. Later raw/compressed-byte/proof/filesystem failures remain fatal save-boundary failures and do not roll back committed actions. Cursed-drop rejection has fixture-backed Core/App/batch evidence only; no ordinary cursed-gear acquisition, equipment effects, item physics, full sale/buyback or broad playable-loop completion is claimed. In-game settings UI, world-look, screenreader/audio integration, and live visual/accessibility certification remain open. Tactical slice-selection range is still a separate policy gap. Next priority is a focused first-session usability playthrough before selecting another large system.
Review Strategy #
This plan requires parent + independent plan review before implementation, particularly core knowledge ownership, complete initial snapshot, run allocation/idempotency, death write ordering, immutable recovery references, and bounded replay/export costs. Review core/schema and artifact transaction independently before caller wiring. Review final process tests against actual production paths. Fix/rebut critical/high findings and obtain closure on substantive changes. Do not accept green tests that silently inject state, omit crash windows, or conflate single-file replacement with whole-transaction durability.
Documentation Strategy #
Update docs03/04 only to clarify typed auction records versus automatic gear inheritance, never delete promised buyback behavior. Document artifact layout, root selection, mode/load semantics, format versions/refusals, recovery workflow, limits and platform constraints. Update docs06 process/lineage gates only on actual process evidence. Mark incomplete acquisition verbs, auction purchase, broader services and accessibility honestly. Preserve the parent-reviewed research corrections above as rationale.
Risks, Blockers, and Required Decisions #
- This is a proposed handoff, not a completion claim. Run ID allocation and the full initial-lineage/vault shapes must be reviewed together before v4 is emitted; no placeholder opaque tail for missing state.
- Adding RunKnowledge must not bypass explicit StateImage/hash coverage or create a second knowledge authority. Retaining terminal knowledge changes old death behavior deliberately; old replay formats reject instead of silently replaying changed semantics.
- Multiple files are not atomically committed together. Durable terminal snapshot/intent, applied-death receipt and committed receipt plus ordered recovery provide a defined state machine. Filesystem/directory sync failures must be surfaced or explicitly classified; power-loss guarantees need platform testing.
- Replay/export limits can reject legitimately long histories. Never delete a run when replay export exceeds the trusted budget; retain terminal state/intent and expose an explicit trusted higher-budget recovery option. Event-cap rollover remains a later product policy, not silent truncation.
- The fs2 lock is advisory and local-filesystem oriented. It prevents cooperative concurrent instances, not a hostile external editor. Hashes/receipts are consistency evidence, not authentication.
- Initial ghosts/auction lots are recorded for deterministic identity and provenance, but complete encounters/purchase interactions are not claimed by merely persisting their records. No automatic gear inheritance.
- Live user settings/audio/remap persistence and full accessibility signoff remain separate; consume existing component APIs only where needed and do not label Phase12 complete.
Runtime debt in scope #
--arena --seedmust root arena state (implemented; binary-level regression test required in the final gate).- Tactical Z changes only from explicit map input (implemented; render must remain observational).
- Panic hook must actually write
crash-epitaph.mdwith live seed/tick/hash/log. - Actor rendering derives from authoritative identity, never a generic rat fallback.
- Content root resolution must not silently become an empty pack hash outside the repository CWD.
- Stale phase placeholders must be removed or explicitly describe historical fixtures.
- Vault Choice is private and implementation-enforced write-once (implemented and gate-tested).
Acceptance gates #
- Playable loop: production chargen → Milthollow → contract → generated Deep → objective → return/reward → service/sleep → save/quit/reload with identical canonical state.
- Classic lineage: production death writes demo and lineage atomically; descendant loads inherited knowledge while gear does not persist.
- Replay: production
.hexdemoapplies typed actions at recorded ticks and reaches its stored final hash. - Story: death restores the stored town checkpoint and applies chapel debt.
- Authority: every live actor has one authoritative
Actor; keyed metadata cannot create a second simulation entity; ECS remains empty/dormant in this pass. - Truthful status: checklist marks integrated/gate only after these production-path tests pass.