Compare commits

...
Author SHA1 Message Date
wushenghua 434656e593 docs(runtime): define P9.1 modular architecture 2026-08-13 09:00:21 +08:00
@@ -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.