diff --git a/docs/runtime/p9_1/RUNTIME_ARCHITECTURE_SPEC.md b/docs/runtime/p9_1/RUNTIME_ARCHITECTURE_SPEC.md new file mode 100644 index 00000000..42a3ce64 --- /dev/null +++ b/docs/runtime/p9_1/RUNTIME_ARCHITECTURE_SPEC.md @@ -0,0 +1,630 @@ +# P9.1 Runtime Architecture Specification + +> Status: expert input for PMO reconciliation +> Baseline: `dec9574405043624acee0f4f81963d829e8d40e9` +> Preserved product source: P9.0R10 at `41204acafa911a505f5b24a493aa032983e706d1` +> Scope: behavior-equivalent extraction boundaries and P9.1 parallel write contract + +## 1. Decision Summary + +P9.1 shall keep `runtime/main.tscn` and its root `Control` as the only scene +entry while reducing `runtime/main.gd` to a compatibility facade and +composition root. Deterministic first-session rules move into plain +`RefCounted` domain objects before new gameplay is added. Rendering and +real-time playback consume immutable snapshots and domain-emitted facts; they +do not calculate outcomes. + +The required dependency direction is: + +```text +main.gd facade / composition root + -> ui/p9_1/* presenter and Control builders + -> presentation/* clock, animation, feedback and audio adapters + -> domain/p9_1/* session state, commands, battle fixtures and copy facts + +tests -> public facade during extraction +tests -> domain/p9_1 APIs for P9.1 matrix and transaction tests +domain/p9_1 -> no Node, Control, SceneTree, Input, AudioServer or frame clock +``` + +There is no event bus singleton and no autoload in this milestone. The root +constructs dependencies explicitly. Domain commands return typed-by-contract +`Dictionary` results because this repository currently uses GDScript without a +schema library; every payload has a stable `kind`, required keys and value +types documented below. Introducing a dependency, plugin or autoload would add +global ownership and is not justified by this bounded slice. + +This document does not authorize runtime edits. The reconciled implementation +contract must preserve P9.0R10 behavior and the Owner-accepted player arc. + +## 2. Baseline Inventory + +### 2.1 Current responsibilities and concrete symbols + +`runtime/main.gd` is 3,704 lines. It currently owns all of the following: + +| Responsibility | Mutable state or concrete symbols | Current coupling | +|---|---|---| +| Boot and navigation | `AppPage`, `page`, `run_boot_verification`, `enter_game`, `start_new_game`, `go_home`, `confirm_restart` | Page mutation immediately calls `render()` and audio cues. | +| Session presentation settings | `locale`, `text_scale`, `reduced_motion`, `muted`, `toggle_*` | Localization and accessibility branches are read throughout rendering and animation. | +| Recruit selection and formation | `selected_recruits`, `front_choice`, `rear_choice`, `formation_commit_count`, `toggle_recruit`, `assign_automatic_formation`, `commit_formation` | Rules, copy, sound and rerender are in the same methods. | +| Run transactions | `coin`, `adaptation_*`, `guild_*`, `retained_item`, `added_rear`, `route_*`, `training_*`, reward flags | Selection validation, commit-once guards, spend/reward and page transition are interleaved. | +| Battle fixture state | `battle_phase`, clocks, HP, event counters, `phase_impact_committed`, five elapsed/result times | The render frame calls state mutation through `_process`, `commit_phase_impact` and `advance_battle_phase`. | +| Battle outcomes | `commit_phase_impact`, `commit_second_phase_impact`, `commit_third_phase_impact`, `commit_fourth_phase_impact`, `commit_fifth_phase_impact` | Outcome tables are page branches and call `play_cue()` plus `render()`. | +| Causal explanations | `victory_explanation`, `second_result_explanation`, route/training/build helpers | Product facts, localized prose and presentation colors share the root script. | +| Rendering | `render`, 20-plus `render_*` methods, button/style/actor helpers | Full `DisplayRoot` tree is destroyed and rebuilt on every render. Stable node names are test API. | +| Playback | `ANIMATION_SPECS`, animation-state functions, `_process`, sprite positions, projectile | Domain phase, wall-clock time, animation frames and Control coordinates are coupled. | +| Feedback and audio | `active_battle_feedback`, history, `emit_battle_feedback`, `update_battle_feedback_visual`, `play_cue` | Exactly-once outcome facts are stored in UI-shaped dictionaries and trigger platform services directly. | +| Cleanup | `clear_experience`, `_exit_tree` | One long reset method owns all transient domain and presentation state. | + +The independent `runtime/domain/run_state.gd` and current `runtime/tests/p9_1_*` +belong to an older opening-market lineage. They expose `game.run` and +`AppPage.RUN`, neither of which exists in the P9.0R10 accepted runtime. They are +not a second authority for this extraction and must not be merged into the new +P9.1 first-session model by name alone. PMO must explicitly classify them as +legacy/quarantined or separately restore that lineage before any runner claims +they are part of the P9.0R1-R10 regression suite. + +### 2.2 Current event flow + +The accepted runtime uses this loop: + +```text +Control signal callback + -> root method validates current page and mutable fields + -> method mutates values and page + -> play_cue() + -> render() rebuilds DisplayRoot + +_process(delta) + -> advances wall clocks and sprite positions + -> crossing impact time calls commit_phase_impact() + -> impact function mutates HP, appends feedback and plays audio + -> crossing phase duration calls advance_battle_phase() + -> phase completion mutates reward/page and rerenders +``` + +The problem is not that a render loop exists. The problem is that a skipped or +duplicated render frame can currently enter the outcome mutation path. P9.1 +must make the deterministic command/impact transaction callable without a +scene tree and make the real-time driver only request named impacts once. + +### 2.3 Existing regression seams + +The preserved P9.0 tests instantiate `res://main.tscn` and directly read or +write root fields. Across the suite they rely on: + +- `game.AppPage` numeric names and `game.page`; +- public state such as `front_choice`, `rear_choice`, HP, Coin, choices, + commit flags, phase clocks/counters and feedback history; +- direct calls such as `advance_battle_phase`, `commit_phase_impact`, + transaction methods, copy helpers, `render` and `clear_experience`; +- `ANIMATION_SPECS`, duration helpers, animation lookup and sprite references; +- stable Control names searched recursively and real + `Input.parse_input_event` touch delivery; +- exact marker output, strict diagnostic cleanup and fixed result values. + +This is an intentionally broad compatibility surface during extraction. Tests +shall not all be rewritten to new APIs in the foundation step because doing so +would remove the evidence that behavior was preserved. The facade keeps that +surface until equivalent domain and UI contract tests pass at the same source +revision; compatibility removal is a later integration action with explicit +criteria in section 10. + +## 3. State Ownership + +Exactly one object owns each mutable value. Snapshots are deep copies and +consumers must treat them as immutable. + +| Owner | Authoritative mutable state | Explicitly does not own | +|---|---|---| +| `RuntimeShell` (`main.gd`) | Dependency instances, boot verification fields, `render_pending`, compatibility property forwarding | Run outcomes, choice validity, HP/Coin calculations, UI-derived recommendations | +| `PresentationSettings` | `locale`, `text_scale`, `reduced_motion`, `muted`, `audio_enabled` | Gameplay state or translated gameplay facts | +| `FirstSessionState` | Current domain screen, selected members/order, all selection/commit flags, Coin, HP, rewards, route/growth state, current encounter progress, committed impact IDs and semantic fact history | Nodes, textures, positions, elapsed frame delta, audio | +| `BattleFixture` | Immutable encounter phase definitions, durations, impact IDs and deterministic transition functions | Mutable progress/history, sprite references, animation frames, pixel coordinates, localized labels | +| `BattleClock` | Playback elapsed seconds for current phase/encounter, result display time, active feedback display age | HP/Coin or whether an impact is legal | +| `SessionPresenter` | Node references it created, ephemeral hover/focus and layout data | Any decision or outcome duplicated from domain state | +| `AnimationPlayerAdapter` | Loaded textures, animation frame selection, actor base positions, projectile visual state | Domain phase transitions or impact commits | +| `FeedbackPresenter` | Current rendered number/VFX node and display age | Feedback history or damage/heal calculation | +| `AudioAdapter` | `AudioStreamPlayer` and generator playback lifecycle | Outcome choice or transaction state | + +The domain screen is not a Godot `Control`. Use stable domain names rather than +numeric UI enum values: + +```text +boot, title, main_menu, game_intro, settings, first_mission, +recruit_selection, formation_review, yard, +battle_1, result_1, adaptation, battle_2, result_2, +guild_response, battle_3, result_3, +route_board, route_review, battle_4, result_4, +growth, growth_review, battle_5, session_result +``` + +The compatibility facade maps these names to the existing `AppPage` constants. +Historical `FRONT_SELECTION` and `REAR_SELECTION` remain facade-only enum +values and are never legal domain screens. + +## 4. Module and File Contract + +### 4.1 Foundation-owned shared extraction + +The foundation change creates and wires these stable boundaries before domain, +UI and test workers start: + +| File | API and responsibility | +|---|---| +| `runtime/main.gd` | `RuntimeShell`; scene lifecycle, boot, dependency construction, facade properties/methods, render scheduling. No new P9.1 rule branches. | +| `runtime/presentation/presentation_settings.gd` | Session-local locale, text scale, reduced-motion, mute, audio availability and low-power values; emits no gameplay event. | +| `runtime/domain/p9_1/session_state.gd` | `FirstSessionState`; data container, `reset_transient()`, `snapshot()`, invariant checks. Initial implementation mirrors accepted fields exactly. | +| `runtime/domain/p9_1/session_service.gd` | `FirstSessionService`; all selection and commit commands, screen transitions, rewards and encounter start/finish. | +| `runtime/domain/p9_1/battle_fixture.gd` | Deterministic encounter definitions, `duration_for_phase`, `impact_time`, `commit_impact`, `complete_phase`; emits semantic facts without Node calls. | +| `runtime/domain/p9_1/contracts.gd` | Stable screen, command, encounter, event and rejection string constants; snapshot schema version. | +| `runtime/presentation/battle_clock.gd` | Converts `delta` to `impact_due` / `phase_due` requests; freezes during confirmation; owns elapsed display timing only. | +| `runtime/presentation/animation_adapter.gd` | Asset library and animation frame lookup extracted without changing frames/timing. | +| `runtime/presentation/audio_adapter.gd` | Existing generated SFX setup, cue playback, mute and cleanup. | +| `runtime/ui/session_presenter.gd` | Existing render dispatch and render helpers moved behavior-equivalently; consumes snapshot and callbacks. | +| `runtime/ui/widgets.gd` | Existing button, label, actor, bar and style builders; no domain reads. | +| `runtime/compat/p9_0_root_facade.gd` | Mapping helpers used by `main.gd` to preserve test-visible properties, calls and `AppPage`. No independent mutable state. | + +If GDScript property forwarding makes `runtime/compat/p9_0_root_facade.gd` +impractical as composition, keep the thin forwarding code in `main.gd`; do not +use inheritance or duplicate fields merely to satisfy this proposed filename. +The contract is one authoritative state object plus a compatible root surface. + +### 4.2 Domain APIs + +`FirstSessionService` exposes commands. Every command returns: + +```gdscript +{ + "ok": bool, + "code": StringName, # stable success or rejection code + "events": Array[Dictionary], + "snapshot": Dictionary, # deep copy after the attempt +} +``` + +Required commands for the preserved and P9.1 path are: + +```text +reset_transient +open_new_session +toggle_recruit(profession_id) +review_formation +revise_formation +commit_formation +choose_adaptation(item_id) +commit_adaptation +choose_guild_response(response_id) +commit_guild_response +choose_route(route_id) +review_route +cancel_route_review +commit_route +choose_growth(member_id) +review_growth +cancel_growth_review +commit_growth +commit_impact(encounter_id, phase, impact_id) +complete_phase(encounter_id, phase) +``` + +Selection commands may change only selection state. Commit commands revalidate +the entire precondition and either apply all effects once or apply nothing. +Failures use stable codes such as `wrong_screen`, `invalid_id`, +`selection_required`, `insufficient_coin`, `already_committed` and +`stale_encounter`; UI copy is not stored in these codes. + +Direct field mutation is not part of the new domain API. Fixtures may use a +test-only builder in `runtime/tests/support/p9_1_session_fixture.gd` to create a +validated starting snapshot. Production UI never calls that builder. + +### 4.3 Snapshot schema + +`FirstSessionState.snapshot()` returns the domain fields in this minimum +shape. `RuntimeShell.render_snapshot()` adds the `settings` section from +`PresentationSettings` to a deep copy; it never writes settings into domain +state. + +```gdscript +{ + "schema": 1, + "revision": int, # increases after successful commands only + "screen": StringName, + "settings": { # render snapshot only; composed by shell + "locale": StringName, + "text_scale": float, + "reduced_motion": bool, + "muted": bool, + "low_power": bool, + }, + "party": { + "selected_ids": Array[StringName], + "ordered_ids": Array[StringName], + "lead_id": StringName, + "second_id": StringName, + "added_id": StringName, + "formation_commits": int, + }, + "resources": {"coin": int}, + "decisions": { + "adaptation": StringName, + "guild_response": StringName, + "route": StringName, + "growth_member": StringName, + }, + "locks": { + "adaptation": bool, + "guild_response": bool, + "route": bool, + "growth": bool, + }, + "encounter": { + "id": StringName, + "phase": int, + "playing": bool, + "front_hp": int, + "rear_hp": int, + "enemy_hp": int, + "committed_impacts": Array[StringName], + "elapsed": float, + }, + "rewards": { + "battle_1": bool, + "route": bool, + "battle_5": bool, + }, + "facts": Array[Dictionary], + "available_actions": Array[StringName], +} +``` + +The pure domain snapshot omits `settings`; every other key above is domain +owned. `available_actions` is domain-derived and allows buttons to enable without +reimplementing validation. It is not a recommendation and cannot preselect. +Settings are included only in the complete render input and cannot affect a +command outcome or the domain revision. + +Every `party` member ID is stable and carries profession as member data. P9.1 +must not use UI positions such as `front`, `rear` or `added` as the durable +identity for growth. The compatibility facade may map the current two-member +fields while old tests remain. + +### 4.4 Semantic events and facts + +Domain output is an append-in-result event list, not a signal emitted from a +Node. Events use these contracts: + +| `kind` | Required payload | Consumer | +|---|---|---| +| `screen_changed` | `from`, `to`, `cause` | Shell/presenter rerender and focus policy | +| `selection_changed` | `decision`, `selected_id` | Presenter selection visuals | +| `transaction_committed` | `transaction`, `decision_id`, `revision`, resource deltas | Result copy facts and tests | +| `impact_committed` | `encounter_id`, `phase`, `impact_id`, `target_member_id` or `enemy`, `delta`, `qualifier`, `tier` | Feedback, audio and deterministic tests | +| `encounter_finished` | `encounter_id`, final HP, elapsed fixture time | Presenter/result navigation | +| `reward_committed` | `reward_id`, `coin_delta`, `coin_after` | Result facts and tests | +| `run_cleared` | `reason` | Shell clears playback/presentation state | + +`qualifier` is a semantic key such as `block`, `evade_reduction`, +`flank_counter`, `ranged_hit`, `heal`, `combo` or `finisher`, never localized +prose. UI translation maps keys to Chinese/English. The event's exact `delta` +is created inside the successful mutation, preserving the P9.0R3 rule that +feedback is never inferred from animation or page entry. + +The `facts` array in a snapshot contains durable causal facts needed for the +current diagnosis/result. Each fact has `fact_id`, `actor_id`, `action`, +`target_id`, exact `delta` where applicable, `source_decision_id` and +`encounter_id`. Result copy is formatted from these facts; it does not rerun +party classification or outcome calculations. + +### 4.5 Presenter API + +`SessionPresenter` has one public render entry: + +```gdscript +render(snapshot: Dictionary, actions: Dictionary[StringName, Callable]) -> void +``` + +The presenter may request domain actions only through named callbacks. A +callback calls the service, passes returned events to presentation adapters, +then schedules a render using the returned snapshot. The UI worker may add +P9.1 screens and copy formatting under its owned path but cannot read service +internals or calculate HP, Coin, ordering, coverage, choice value or results. + +The presenter continues to create the stable P9.0 Control names until legacy +tests are retired. New P9.1 controls receive stable semantic names in the +reconciled UX contract. Node names are input/test handles; labels are localized +and must never be used as identifiers. + +### 4.6 Battle clock handshake + +The frame adapter receives the current encounter snapshot and returns requests: + +```gdscript +advance(delta, encounter_snapshot, reduced_motion, paused) -> { + "impact_requests": Array[{"encounter_id", "phase", "impact_id"}], + "phase_complete": bool, + "phase_progress": float, +} +``` + +The shell submits requests to the domain service. The service rejects duplicate +`impact_id`, wrong phase or stale encounter. Only accepted +`impact_committed` events reach feedback/audio. `phase_progress` drives visual +interpolation and animation only. Reduced motion and low power may change +sampling and translation but never impact order, fixture durations, final +values or command availability. + +Headless tests bypass `BattleClock` and invoke named impacts/phase completion +directly, proving domain results do not depend on FPS. One integration test +must still drive large deltas across impact thresholds to prove the adapter +requests each impact exactly once. + +## 5. Compatibility Facade + +During extraction, `runtime/main.gd` remains the API expected by P9.0 tests. +Every facade value is computed from an owner; it is not separately stored. + +Required mappings include: + +- existing `AppPage` names and `page` getter/setter for fixture/capture tests; +- selection, party, Coin, choice/commit, HP, phase, reward and feedback getters; +- existing transaction method names, translating successful service events to + `play_cue` and `queue_render`; +- `phase_duration`, `phase_impact_time`, `advance_battle_phase` and + `commit_phase_impact` delegating to fixture/clock/domain boundaries; +- animation constants/helpers and sprite references delegating to presentation; +- `render` and stable Control names delegating to `SessionPresenter`; +- `clear_experience` calling domain reset plus clock/feedback cleanup exactly + once. + +Compatibility setters are allowed only for fields that existing capture/tests +set directly. They shall call a clearly named test/compat patch function which +revalidates the state before rendering. Production callbacks never use these +setters. Any field that cannot be forwarded without two sources of truth blocks +that extraction step; the foundation worker must keep it in the root until its +owner can be singular. + +The facade shall contain no P9.1-only pair branching. New behavior enters the +domain and presenter APIs and is wired by the later integration owner. + +## 6. Extraction Sequence and Regression Gates + +Each numbered step is a separate commit candidate. A step advances only when +its focused checks and the serial accepted regression set pass with strict +diagnostics clean. On failure, revert that step's commit or repair within the +same owned files; never mask the failure by weakening or rewriting preserved +assertions. + +### Step 0: Freeze evidence and classify the suite + +1. Record exact baseline source, Godot version and the ordered P9.0R1-R10 test + command/marker manifest in a runner-owned evidence file. +2. Confirm the accepted suite count from the R10 closure evidence rather than + globbing every file named `p9_1_*`. +3. Record the orphan opening-market lineage separately; do not delete it in + foundation extraction. + +Gate: clean baseline reproduces all 22 accepted R1-R10 tests, R10 matrix/touch, +strict marker scan and cleanup. If it does not, terminal is redesign required +at baseline reproducibility; extraction must not begin. + +### Step 1: Introduce state owner behind a no-op facade + +Create `contracts.gd`, `session_state.gd` and the compatibility forwarding +surface. Initialize the state from the same defaults and delegate reset. Do not +move transactions or change rendering. + +Gate: reset/default/navigation tests, Home/Restart cleanup, all direct root +field assertions and full serial regression. Rollback: remove the state object +and forwarding commit; no data migration exists. + +### Step 2: Extract selection and run transactions + +Move automatic formation, adaptation, guild response, route, growth, rewards +and screen transition validation to `FirstSessionService`, in historical order. +Keep root method names as wrappers. Every operation gains a focused atomicity +test: invalid/insufficient/duplicate attempts leave snapshot and revision +unchanged. + +Gate after each transaction family: its original focused test plus snapshot +diff against baseline fixtures, then the serial suite. Rollback is per family; +do not move all transactions in one unreviewable commit. + +### Step 3: Extract deterministic battle fixtures + +Move durations, impact timing, HP mutation and semantic event creation one +encounter at a time from battle 1 through battle 5. Root methods delegate. +First preserve exact P9.0R10 values and event order; no arbitrary-party +post-battle-two behavior is added in this step. + +Gate per encounter: exact event sequence, deltas, HP/time, once-only impacts, +reward, first-battle feedback and associated R1-R7 tests. R10 remains the final +gate. Rollback only the most recent encounter extraction. + +### Step 4: Extract clock, animation, feedback and audio adapters + +Move time accumulation and visual playback after outcomes are already domain +owned. Separate semantic event history from the single active presentation +payload. Extract animation lookup/assets and audio lifecycle without changing +timing, frames or accepted cues. + +Gate: R2 animation terminal bounds/dynamic slice, R3 exact feedback and reduced +motion, R3 audio lifecycle, large-delta impact-once integration, Restart pause, +Home cleanup and serial regression. Rollback adapters independently; domain +tests must remain green even if presentation extraction is reverted. + +### Step 5: Extract presenter and widgets + +Move page dispatch, screen builders and shared Control/style helpers. Preserve +node names, minimum touch size, layout, text scaling, locale behavior and root +sprite references through the facade. + +Gate per screen family: domain/navigation, real parsed touch with direct and +fallback zero, layout/capture at existing viewports, English 130%, reduced +motion, mute, low-power representative state, portrait notice and serial +regression. Use normal rendering, not only direct `render()` fixtures. + +### Step 6: Foundation closure + +The foundation source is ready for parallel work only when: + +- `main.gd` is a composition root/facade and has no authoritative outcome + calculation or P9.1-only rule branch; +- all mutable values have one owner and snapshot/event schema tests pass; +- domain tests run without instantiating `main.tscn`; +- all accepted P9.0R1-R10 behavior remains green at one clean revision; +- the exact parallel write paths in section 7 exist and compile; +- PMO reconciles this document with game-design and UX/visual outputs. + +Rollback at closure is the last green step revision, not a mixed partial +extraction. No parallel worker begins from a failing foundation branch. + +## 7. Non-Overlapping Parallel Writes + +The following is the implementation ownership contract after foundation is +integrated. Directory ownership includes new files only unless an exact file is +listed. Moving or renaming another role's file is a write to that file and is +forbidden. + +| Role | Exact writes | Reads/consumes | Forbidden writes | +|---|---|---|---| +| Foundation | `runtime/main.gd`, `runtime/domain/p9_1/{contracts,session_state,session_service,battle_fixture}.gd`, `runtime/presentation/{presentation_settings,battle_clock,animation_adapter,audio_adapter,feedback_presenter}.gd`, `runtime/ui/{session_presenter,widgets}.gd`, `runtime/compat/p9_0_root_facade.gd` | Accepted source/contracts/tests | New P9.1 gameplay, test expectations, product behavior | +| Domain | New files only under `runtime/domain/p9_1/content/` and `runtime/domain/p9_1/rules/` | Foundation domain APIs plus reconciled gameplay contract | `main.gd`; foundation API files; any `Control`, UI, tests or runners | +| UI | New files only under `runtime/ui/p9_1/` | Snapshot/event schema plus reconciled UX contract | Domain/presentation logic; `main.gd`; shared presenter/widgets; tests/runners | +| Tests | New files under `runtime/tests/p9_1/` and `runtime/tests/support/p9_1/` | Public domain/presenter contracts and frozen matrix | Existing P9.0 tests; production runtime; shared runner/entry files | +| Integration | `runtime/main.gd`, exact foundation shared API files when reconciliation requires, shared P9.1 runner/manifest files, `runtime/project.godot` only if the frozen integration contract explicitly requires it | Domain, UI and test commits | Rewriting accepted P9.0 assertions; product expansion | + +The integration role is the only role allowed to wire new domain/UI modules +into the shared facade, resolve schema mismatches, or edit shared runners. +Integration must not silently absorb one role's duplicated calculation. A +schema mismatch returns to the earliest producing role or receives an explicit +reconciled contract amendment before integration. + +No parallel role owns `runtime/main.tscn`; the accepted one-root scene needs no +edit for this architecture. If implementation proves otherwise, PMO must add +that exact file to integration ownership before work, never let UI and +integration both edit it. + +## 8. Test Architecture After Extraction + +### 8.1 Test layers + +1. **Pure domain tests** instantiate service/state/fixtures without a scene + tree. Cover the six unordered pairs, both adaptations, every reconciled + downstream choice, invalid transactions, deterministic events and reset. +2. **Facade regressions** keep the accepted P9.0 tests unchanged while the + facade exists. They prove property/method/node compatibility. +3. **Presenter contract tests** feed fixed snapshots/events and assert stable + controls, available/disabled actions, localized fact rendering and absence + of duplicated outcome logic. +4. **Clock integration tests** drive normal, low-FPS/large-delta, reduced-motion + and paused paths and compare the final domain snapshot. +5. **End-to-end parsed input** uses `Input.parse_input_event` ScreenTouch and + ScreenDrag through the complete session with fallback/direct zero. +6. **Normal-render evidence** covers queue/action feedback, causal decisions + and result deltas at small/reference/large landscape, Chinese/English, 130%, + reduced motion, mute and low power. +7. **Strict diagnostics** scan script/parse/resource/import/assertion/ObjectDB + and cleanup failures; the terminal marker must be unique. + +### 8.2 Contract assertions + +Add schema tests that reject: + +- mutable arrays/dictionaries leaking from snapshots; +- UI code importing `battle_fixture.gd` or rule content directly; +- domain files extending `Node`/`Control` or referencing `SceneTree`, `Input`, + `AudioServer`, textures, node paths or viewport size; +- a successful command with no revision increment, or rejected command with a + state/revision change; +- duplicate `impact_id`, reward or commit events; +- presenter calculations of Coin/HP/coverage/result facts; +- source files outside the owning role's write allowlist. + +Dependency-direction and write-scope checks may be small repository scripts; +they must use parsed GDScript metadata where practical, with `rg` guards as a +bounded additional check rather than claiming a full language parser. + +### 8.3 Baseline runner requirement + +The test worker shall create one serial runner manifest listing exact scripts, +expected unique markers, timeout, required viewport/environment and strict log +policy. It must not select tests by filename glob. The orphan opening-market +files are separately named and have no effect on the R1-R10 pass claim until a +future frozen goal reconciles them. + +## 9. Invariants + +These are mandatory at every green migration step and at P9.1 integration: + +1. A successful command is atomic and increases `revision` exactly once. +2. A rejected command changes no snapshot field, event history, resource, + reward, clock or page. +3. Formation order is derived from selected member identities, never click + order, and commits once. +4. Choice selection is revisable and free; its commit locks once and is the + only spend/state-change boundary. +5. An impact is keyed by encounter + phase + impact ID and commits at most once. +6. Feedback is the exact semantic event created inside that impact mutation. +7. Rewards commit only after their declared victory boundary and at most once. +8. `reset_transient` clears all run and playback state while preserving + session-local presentation settings. +9. Domain output is identical across render FPS, reduced motion, mute, low + power and viewport size. +10. UI enables from `available_actions`, displays domain facts and never + predicts or recalculates outcomes. +11. Chinese and English are formatting over the same semantic facts; locale + changes no domain snapshot except the shell-composed settings section. +12. No path after battle two may use the R10 `later_preview_supports_pair` + limitation once P9.1 integration claims all six complete sessions. + +## 10. Legacy Adapters and Deletion Criteria + +| Legacy path | Why retained | Delete only when | +|---|---|---| +| Root `AppPage` and `page` facade | P9.0 tests and captures set/read it | Replacements cover every accepted transition and fixture, preserved tests are migrated in a dedicated reviewed commit, and no source uses it. | +| Root public state properties | P9.0 tests directly assert/mutate them | Domain snapshot/builder tests plus facade-independent integration tests cover the same facts, and a reference search is empty. | +| Root transaction wrappers | Existing touch/domain tests call them | All callbacks and tests call the service action map, parsed touch stays green, and reference search is empty. | +| Root render/animation helpers and sprite refs | Layout/capture/animation tests inspect them | Presenter/animation contract tests and normal-render evidence replace each assertion, with no root references. | +| Historical `FRONT_SELECTION` / `REAR_SELECTION` | Enum compatibility | No preserved evidence references them and automatic selection regressions pass after removal. | +| `later_preview_supports_pair()` | R10 honestly stops exceptional pairs after battle two | All six pairs complete five battles with P9.1 matrix and end-to-end evidence; the method and UI boundary copy are then removed together. | +| Orphan `domain/run_state.gd` and old `tests/p9_1_*` | Separate prior lineage, deletion not owned here | A frozen Goal classifies their product intent and either restores them as a separate module or removes them with independent evidence. | + +Deletion is always integration-owned, source-searched, tested and committed +separately from feature addition. Compatibility aliases do not become a +permanent alternative API. + +## 11. Integration Risks and Responses + +| Risk | Earliest detectable boundary | Required response | +|---|---|---| +| Two mutable sources of truth in root and state object | Step 1 snapshot/facade assertions | Stop; keep field in one owner or redesign forwarding. Never synchronize copies per frame. | +| Rewritten tests pass while behavior regresses | Any extraction step | Preserve existing P9.0 assertions; compare exact markers, values, node names and normal rendering before test migration. | +| Frame timing changes outcomes | Step 3/4 large-delta and headless tests | Outcome must move to named domain impacts; clock only requests them. | +| Dictionary schema drifts between parallel workers | Parallel compile/schema tests | Reconcile constants/schema before wiring; no UI fallback keys or silent defaults for required fields. | +| UI duplicates pair/coverage/result logic | Presenter contract/static dependency check | Return to UI role; consume facts and available actions instead. | +| `main.gd` becomes a second integration battlefield | Parallel write audit | Only integration edits it after foundation; domain/UI/tests remain in exact owned paths. | +| Legacy opening-market files are included in the wrong pass claim | Step 0 manifest | Quarantine in an explicit runner group and report separately. | +| Full `DisplayRoot` rebuild loses focus/input or stale references | Step 5 touch/layout/cleanup | Preserve facade refs during extraction; later incremental rendering requires its own frozen behavior-equivalent step. | +| Snapshot deep copies create low-power cost | Pure benchmark after correctness | Measure first. Optimize snapshot sections/revision caching without giving UI mutable references. | +| Godot import/class cache makes isolated files appear valid | Foundation closure clean import | Run from clean import/cache evidence and compile every new script through the actual project. | +| Compatibility layer never disappears | Integration closure audit | Apply deletion criteria and record remaining adapters with owner and follow-up; do not add new facade-only feature APIs. | + +## 12. Foundation Handoff Checklist + +Before the foundation developer is dispatched, the reconciled contract must +answer each item with a stable reference: + +- final P9.1 screen names and whether `session_result` replaces or extends the + historical `THIRD_CYCLE_RESULT` label; +- stable member IDs and decision IDs supplied by game design; +- UX-owned new Control names and result fact ordering; +- exact `facts` and `available_actions` required on each P9.1 screen; +- low-power semantics that may affect presentation sampling only; +- the exact accepted P9.0R1-R10 serial runner manifest and orphan-test policy; +- final shared foundation paths and the three parallel write allowlists; +- the source revision from which all parallel worktrees branch. + +If any item is unresolved, the earliest failed boundary is specification +reconciliation; runtime extraction must remain frozen.