State Machines and Transitions
Context
This page is the binding hub doc for every named state machine in Valenar.
Foundation Hardening (Wave 3a) introduced four new state-machine families
through the cross-cutting canonical-pattern ADR
../lore/adr/ad-0013-scope-state-channel-template-archetype-pattern.md:
the Quest Thread / Mission lifecycle pair (ad-0007), the
ThreatSource / FrontCandidate emergence ladder (ad-0008), the Realm
PolityRank ladder (ad-0009), and the Act Progression ladder (ad-0010).
The True Harm placeholder convention
(ad-0011) and
the character-conditions-and-injuries Modifier-template shape
(ad-0012)
land alongside that quartet.
The new families do not stand alone. The existing Front lifecycle
(FrontStatus enum on Content/forces/scopes.secs), the Force lifecycle
(LifecycleState enum on the same scope file), the Operation Phase and
Status enums (also on Content/forces/scopes.secs), and the three
discovery / knowledge / content state axes from
./gd-state-axes-and-thresholds.md
are existing precedents that this hub must also bind into one mechanical
table.
The Foundation Hardening goal is concrete: every transition row in
Valenar carries an explicit predicate, an explicit trigger, explicit
required facts, named Config thresholds, named side-effects, named
runtime owner, and an explicit fallback. No silent default
transitions. No day-count predicates. No real-time clock predicates. No
magic threshold constants. Where the originating ADR explicitly invited
inline authoring this wave, the row carries the committed predicate.
Where the originating ADR explicitly deferred the predicate to Wave 5a
runtime-backing closure, the row carries the canonical
<deferred-to-Wave-5a-runtime-backing> marker plus a one-line
design-intent note sourced from the ADR.
Scope
This doc OWNS:
- The canonical 12-column transition row format used by every state machine table below.
- The canonical
<deferred-to-Wave-5a-runtime-backing>marker form for Wave 5a closure handoffs. - The aggregated Open Follow-On Items list at the bottom of the doc: the canonical deferral registry that Wave 5a authors consume to plan the runtime-backing work.
This doc CITES (does not re-author):
- Per-state-machine state enumerations: each owner doc (gd-quest-thread- and-mission-state-machine.md, gd-front-emergence.md, gd-act-progression.md, gd-character-conditions-and-injuries.md, gd-realm-ranks-and-polity.md, gd-defense-zones.md, gd-fronts.md, gd-forces.md, gd-operations.md, gd-state-axes-and-thresholds.md) holds the design rationale and the committed enum labels.
- Per-Act gate predicates: the seven
../acts/gd-act-N-gate-spec.mddocs each own their own predicate. The Act ladder section below cites the gate-spec docs by name in thetrigger_event_or_predicatecell; it does not duplicate the predicate text. - Per-MC-condition ladder rungs: gd-character-conditions-and-injuries.md owns the rung labels. The MC Condition Ladders section below cites that doc; it does not re-author the rungs.
Column Schema
Every transition table uses the same 12-column row form. One logical transition per row. The column rules are normative.
| Column | Content rule |
|---|---|
source_state | The named enum value or scope state the row leaves. MUST be a committed value (see ad-0007 / ad-0008 / ad-0009 / ad-0010 / Content/forces/scopes.secs / Generated/Territories/FeaturePlacement/FeaturePlacementTypes.cs / Generated/Territories/TerritoryKnowledgeTypes.cs / Generated/Territories/FeaturePlacement/FeatureContentTypes.cs / gd-fronts.md / gd-act-0-mission-spine.md). Inventing a state is a forbidden-pattern FAIL. |
trigger_event_or_predicate | The on_action label OR named state predicate (e.g., core_established == true, Realm.CoveredSettlements.Count >= 2 && RouteCoverage(Realm)) that drives the transition. NEVER a day count, NEVER a real-time clock value (per ad-0010 lines 96-99). If the originating ADR defers the predicate to Wave 5a, the cell carries the canonical <deferred-to-Wave-5a-runtime-backing> marker plus a one-line design-intent note. |
required_facts | The named state predicates that must read true at the trigger moment. Each fact names an existing channel, scope field, or enum read — no invented facts. Example: MainCharacter exists && CoreBuilding.IsBuilt && Settlement.Stage >= I. |
config_thresholds | Named Config.<Subsystem>.<FieldName> references (UI-exposed Config fields with world-size-parameterized defaults). Magic constants are a forbidden-pattern FAIL. If the threshold's Config field name is not yet declared, cell is <deferred-to-Wave-5a-runtime-backing>. |
blocking_conditions | Named predicates that, if true, refuse the transition even when the trigger fires. Example: MissionLifecycleState == Suspended blocks Active → Resolved. |
side_effects | The complete list of events fired (on_action labels with (working name — promotion to committed metadata deferred to Wave 5a) annotation where applicable), Journal entries written (specific entry_id shape per gd-journal.md), Modifier applies / clears, scope rows created / retired, channel writes. Each line is one mechanical effect, not prose. |
target_state | The committed enum value or scope state the row enters. |
runtime_owner | The proposed Host/Systems/<Name>.cs class that owns the body. None of these systems exist today; each is a Wave 5a follow-up per the originating ADR's "Next closure wave" line. |
read_model_surface | The Host/ReadModels/<Name>.cs projection that surfaces the new state. Some surfaces already exist as files but project nothing today (per gd-forces-phase-b-design-questions.md line 492 — ForcesReadModel, FrontsReadModel, OperationsReadModel, DefenseZonesReadModel, ProvinceDefensePlansReadModel). For new state machines, name the proposed read model and mark <contract-only; runtime-backing in Wave 5a>. |
ui_readback | The client screen(s) under ux/ that surface this transition. |
tests_required | Placeholder format <deferred-to-wave-7>: <PascalCaseTransitionTestName>. |
fallback_behavior | Explicit error per the wording bank below. Silent defaults are forbidden. |
Sample Row
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
QuestThreadLifecycleState.Hidden | Discovery predicate from template<QuestThread>.DiscoveryTrigger evaluates true under dynamic quest pressure generator output (per ad-0007 Consequences paragraph 2 and gd-dynamic-quest-pressure-model.md generator contract) | template<QuestThread> registered; scope QuestThread row exists with DefinitionTemplateId valid; ActState.CurrentAct >= template.MinAct; template's DiscoveryTrigger predicate (named state, never a day count) reads true | Config.QuestThread.DiscoveryEvaluationCadence | none | Fires on_action QuestThreadAvailable(threadId) (working name; promotion to committed metadata deferred to Wave 5a per ad-0007); Journal entry of class quest-available per gd-journal.md; Current Plan urgency rank updated per gd-current-plan.md | QuestThreadLifecycleState.Available | Host/Systems/QuestThreadSystem.cs (proposed; contract-only per ad-0007) | Host/ReadModels/QuestThreadsReadModel.cs (proposed; contract-only) | gd-current-plan.md, gd-objectives-screen.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadHiddenToAvailableTest | throw InvalidStateException("Required predicate 'template.DiscoveryTrigger' not present at transition Hidden -> Available for QuestThreadId=<id>") |
Fallback Wording Bank
Every fallback_behavior cell commits to one of these explicit-error
forms (per .claude/rules/valenar-contract-backing.md
gate item 4 — silent fallbacks are forbidden). Exception type names are
proposals; the Wave 5a implementer consults legacy/v1/src/SECS.Engine/
for the committed convention before locking the names.
| Situation | Wording |
|---|---|
| Missing required fact at transition time | throw InvalidStateException("Required fact 'X' not present at transition Y -> Z for <ScopeKind>Id=<id>") |
| Missing Config field at runtime | throw ConfigurationException("Config field 'X' not registered before <Subsystem>Tick") |
| Blocking condition met (transition refused) | Transition refused; remains in source_state; raises on_action BlockedTransition(scopeId, sourceState, attemptedTarget, blockingCondition) and writes Journal entry of class transition-blocked. |
| Ambiguous transitions (two predicates both fire) | throw AmbiguousTransitionException("Multiple eligible transitions from <sourceState>: <list>") |
| Target state unreachable from current | throw IllegalTransitionException("X is not a valid target from Y in <state-machine-name>") |
| Required runtime owner not registered at startup | throw StartupValidationException("<HostSystem>.cs registration missing; required by <state-machine-name>") (checked at SecsModule.Initialize time) |
| Required scope row missing | throw MissingScopeException("scope <Kind> row required but not present for transition X -> Y") |
| Defense-Province with no committed plan | throw ConfigurationException("Province <id> has no committed ProvinceDefensePlan in Committed state; <Subsystem> requires a plan in Committed before <activity>") |
State Machines
Quest Thread Lifecycle
Owner doc: gd-quest-thread-and-mission-state-machine.md.
Binding ADR: ../lore/adr/ad-0007-quest-thread-and-mission-scope-primitives.md.
States (per ad-0007 lines 67-70): Hidden, Available, Active,
Suspended, Resolved, Failed, Abandoned.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Hidden | template<QuestThread>.DiscoveryTrigger predicate reads true under dynamic quest pressure generator output (per ad-0007 Consequences paragraph 2 and gd-dynamic-quest-pressure-model.md) | template registered; scope row with valid DefinitionTemplateId; ActState.CurrentAct >= template.MinAct; DiscoveryTrigger predicate reads true | Config.QuestThread.DiscoveryEvaluationCadence | none | Fires on_action QuestThreadAvailable(threadId) (working name — promotion deferred to Wave 5a); Journal entry quest-available; Current Plan urgency updated | Available | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed; contract-only) | gd-current-plan.md, gd-objectives-screen.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadHiddenToAvailableTest | throw InvalidStateException("Required predicate 'template.DiscoveryTrigger' not present at transition Hidden -> Available for QuestThreadId=<id>") |
Available | Player or policy accepts a child Mission under this thread (per ad-0007 Consequences paragraph 2) | child scope Mission row exists in Accepted state with ParentThreadId == this.Id | none today (acceptance shape <deferred-to-Wave-5a-runtime-backing> per ad-0007 line 56-57) | thread already Suspended from a prior precondition violation | Fires on_action QuestThreadActivated(threadId) (working name); Journal entry quest-activated; Current Plan promotes thread to active rank | Active | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadAvailableToActiveTest | throw InvalidStateException("No Accepted Mission with ParentThreadId=<id> at transition Available -> Active") |
Active | Precondition predicate authored on template<QuestThread>.SuspensionPredicate reads true (<deferred-to-Wave-5a-runtime-backing> — precondition violation per ad-0007 line 56-57) | template's SuspensionPredicate predicate reads true | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action QuestThreadSuspended(threadId, reasonId) (working name); Journal entry quest-suspended; Current Plan demotes urgency rank | Suspended | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadActiveToSuspendedTest | throw InvalidStateException("Suspension predicate not defined on template for QuestThreadId=<id>") |
Suspended | Same precondition predicate reads false again (<deferred-to-Wave-5a-runtime-backing> — precondition restored per ad-0007 line 56-57) | template's SuspensionPredicate reads false | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action QuestThreadResumed(threadId) (working name); Journal entry quest-resumed; Current Plan restores urgency rank | Active | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadSuspendedToActiveTest | throw InvalidStateException("Resume predicate not defined on template for QuestThreadId=<id>") |
Active | All required child Missions resolved per template<QuestThread>.ResolutionRule (<deferred-to-Wave-5a-runtime-backing> — resolution predicate per ad-0007 line 56-57) | every required child Mission has LifecycleState == Resolved | <deferred-to-Wave-5a-runtime-backing> | any required child Mission in Failed and template marks it critical-path | Fires on_action QuestThreadResolved(threadId) (working name); Journal entry quest-resolved; Current Plan retires entry | Resolved | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadActiveToResolvedTest | throw InvalidStateException("ResolutionRule not defined on template for QuestThreadId=<id>") |
Active | Any required child Mission with LifecycleState == Failed and template<QuestThread>.ResolutionRule marks it critical-path (<deferred-to-Wave-5a-runtime-backing> — failure predicate per ad-0007 line 56-57) | child Mission LifecycleState == Failed; thread's template marks that mission slot critical-path | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action QuestThreadFailed(threadId, failedMissionId) (working name); Journal entry quest-failed; Current Plan retires entry | Failed | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadActiveToFailedTest | throw InvalidStateException("Critical-path slot annotation missing on ResolutionRule for QuestThreadId=<id>") |
Active | Player explicitly abandons the thread (activity binding <deferred-to-Wave-5a-runtime-backing> — abandonment activity per ad-0007 line 56-57) | abandonment activity issued by player or Crown-scale policy | none | none | Fires on_action QuestThreadAbandoned(threadId) (working name); Journal entry quest-abandoned; Current Plan retires entry | Abandoned | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadActiveToAbandonedTest | throw InvalidStateException("Abandonment activity not bound for QuestThreadId=<id>") |
Available | Player dismisses the thread before accepting any Mission under it (activity binding <deferred-to-Wave-5a-runtime-backing> — dismissal activity per ad-0007 line 56-57) | dismissal activity issued by player | none | none | Fires on_action QuestThreadAbandoned(threadId) (working name); Journal entry quest-dismissed | Abandoned | Host/Systems/QuestThreadSystem.cs (proposed) | Host/ReadModels/QuestThreadsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: QuestThreadAvailableToAbandonedTest | throw InvalidStateException("Dismissal activity not bound for QuestThreadId=<id>") |
Mission Lifecycle
Owner doc: gd-quest-thread-and-mission-state-machine.md.
Binding ADR: ../lore/adr/ad-0007-quest-thread-and-mission-scope-primitives.md.
States (per ad-0007 lines 70-71): Pending, Available, Accepted,
Active, Resolved, Failed, Abandoned.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Pending | Parent scope QuestThread enters Active AND mission's template<Mission>.PreconditionPredicate reads true (<deferred-to-Wave-5a-runtime-backing> — precondition predicate per ad-0007 line 56-57) | parent QuestThread LifecycleState == Active; mission's preconditions read true | <deferred-to-Wave-5a-runtime-backing> | parent thread in Suspended | Fires on_action MissionAvailable(missionId, parentThreadId) (working name); Journal entry mission-available; Objectives screen surfaces mission | Available | Host/Systems/MissionSystem.cs (proposed) | Host/ReadModels/MissionsReadModel.cs (proposed; contract-only) | gd-objectives-screen.md, gd-journal.md | <deferred-to-wave-7>: MissionPendingToAvailableTest | throw InvalidStateException("Parent QuestThread not Active at transition Pending -> Available for MissionId=<id>") |
Available | Player or policy commits the mission (activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 line 56-57) | acceptance activity issued | none | parent thread in Suspended | Fires on_action MissionAccepted(missionId) (working name); Journal entry mission-accepted; Current Plan queues mission | Accepted | Host/Systems/MissionSystem.cs (proposed) | Host/ReadModels/MissionsReadModel.cs (proposed) | gd-objectives-screen.md, gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: MissionAvailableToAcceptedTest | throw InvalidStateException("Acceptance activity not bound for MissionId=<id>") |
Accepted | Activity executor begins running the mission's authored activity beats (per gd-queue-and-activity-execution.md) | mission row's first ActivityBeatRef enters executor; mission queued by the executor | none | executor at capacity (<deferred-to-Wave-5a-runtime-backing>) | Fires on_action MissionActivated(missionId) (working name); Journal entry mission-active; Current Plan promotes mission rank | Active | Host/Systems/MissionSystem.cs (proposed) | Host/ReadModels/MissionsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: MissionAcceptedToActiveTest | throw InvalidStateException("ActivityBeatRef list empty at transition Accepted -> Active for MissionId=<id>") |
Active | All required activity beats resolved per template<Mission>.ResolutionRule (<deferred-to-Wave-5a-runtime-backing> — resolution predicate per ad-0007 line 56-57) | every required beat resolved | <deferred-to-Wave-5a-runtime-backing> | any required beat in Failed and template marks it critical-path | Fires on_action MissionResolved(missionId) (working name); Journal entry mission-resolved; Current Plan retires mission; parent QuestThread re-evaluates ResolutionRule | Resolved | Host/Systems/MissionSystem.cs (proposed) | Host/ReadModels/MissionsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: MissionActiveToResolvedTest | throw InvalidStateException("ResolutionRule not defined on template for MissionId=<id>") |
Active | Any required activity beat with Failed outcome and template marks it critical-path (<deferred-to-Wave-5a-runtime-backing> — failure predicate per ad-0007 line 56-57) | required beat in Failed; template marks beat critical-path | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action MissionFailed(missionId, failedBeatId) (working name); Journal entry mission-failed; parent QuestThread re-evaluates failure cascade | Failed | Host/Systems/MissionSystem.cs (proposed) | Host/ReadModels/MissionsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: MissionActiveToFailedTest | throw InvalidStateException("Critical-path beat annotation missing on ResolutionRule for MissionId=<id>") |
Active | Player explicitly abandons the mission (activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 line 56-57) | abandonment activity issued | none | none | Fires on_action MissionAbandoned(missionId) (working name); Journal entry mission-abandoned; Current Plan retires entry | Abandoned | Host/Systems/MissionSystem.cs (proposed) | Host/ReadModels/MissionsReadModel.cs (proposed) | gd-current-plan.md, gd-journal.md | <deferred-to-wave-7>: MissionActiveToAbandonedTest | throw InvalidStateException("Abandonment activity not bound for MissionId=<id>") |
Available | Player dismisses the mission before accepting (activity binding <deferred-to-Wave-5a-runtime-backing> per ad-0007 line 56-57) | dismissal activity issued | none | none | Fires on_action MissionAbandoned(missionId) (working name); Journal entry mission-dismissed | Abandoned | Host/Systems/MissionSystem.cs (proposed) | Host/ReadModels/MissionsReadModel.cs (proposed) | gd-objectives-screen.md, gd-journal.md | <deferred-to-wave-7>: MissionAvailableToAbandonedTest | throw InvalidStateException("Dismissal activity not bound for MissionId=<id>") |
Act Progression
Owner doc: gd-act-progression.md. Binding
ADR: ../lore/adr/ad-0010-act-progression-actstate-scope.md.
States (per ad-0010): Act 0, Act 1, Act 2, Act 3, Act 4,
Act 5, Act 6, Act 7, CampaignEnded. The runtime carrier is the
integer ActState.CurrentAct field on the saga-scoped singleton
scope ActState; CampaignEnded is reached when Act 7's
OnActCompleted fires (CurrentAct is not raised beyond 7).
Per-Act gate predicates are owned by the seven
../acts/gd-act-N-gate-spec.md docs. This table cites the
gate-spec doc by name in the trigger_event_or_predicate cell; the
predicate text itself lives in the cited doc. No predicate may name a
day count or a real-time clock value (ad-0010 lines 96-99).
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Act 0 | core_established == true named-state fact written by the Act 0 mission spine (per gd-act-0-arrival-and-core.md line 163 and ad-0010 lines 87-90) | MainCharacter.CoreEstablished == true | none today (gate predicate carries no Config threshold) | none | Fires on_action OnActCompleted(0) then OnActStarted(1) (working names; promotion deferred to Wave 5a per ad-0010); Journal entry act-transition; ActState updates PreviousAct=0, CurrentAct=1, EnteredOnDay=<currentDay> | Act 1 | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed; contract-only) | gd-core-screen.md saga arc display | <deferred-to-wave-7>: ActZeroToOneTest | throw InvalidStateException("MainCharacter.CoreEstablished not true at transition Act 0 -> Act 1") |
Act 1 | Gate predicate authored at ../acts/gd-act-1-gate-spec.md | per the cited gate spec (MainCharacter.CoreEstablished, Core.NamedResidents, Core.ShelterCapacity, Core.WatchAndWardingState, MainCharacter.Camp.HasScoutedAdjacentTerritories) | Config.Act1.NamedResidentThreshold, Config.Act1.ScoutedAdjacentThreshold (named in the gate spec; defaults world-size parameterized per Discipline 14) | none | Fires on_action OnActCompleted(1) then OnActStarted(2) (working names); Journal entry act-transition; ActState updates | Act 2 | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed) | gd-core-screen.md saga arc display | <deferred-to-wave-7>: ActOneToTwoTest | throw InvalidStateException("Gate predicate from gd-act-1-gate-spec.md not satisfied at transition Act 1 -> Act 2") |
Act 2 | Gate predicate authored at ../acts/gd-act-2-gate-spec.md | per the cited gate spec | per the cited gate spec | none | Fires on_action OnActCompleted(2) then OnActStarted(3); Journal entry act-transition; ActState updates | Act 3 | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed) | gd-core-screen.md | <deferred-to-wave-7>: ActTwoToThreeTest | throw InvalidStateException("Gate predicate from gd-act-2-gate-spec.md not satisfied at transition Act 2 -> Act 3") |
Act 3 | Gate predicate authored at ../acts/gd-act-3-gate-spec.md | per the cited gate spec | per the cited gate spec | none | Fires on_action OnActCompleted(3) then OnActStarted(4); Journal entry act-transition; ActState updates | Act 4 | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed) | gd-core-screen.md | <deferred-to-wave-7>: ActThreeToFourTest | throw InvalidStateException("Gate predicate from gd-act-3-gate-spec.md not satisfied at transition Act 3 -> Act 4") |
Act 4 | Gate predicate authored at ../acts/gd-act-4-gate-spec.md | per the cited gate spec | per the cited gate spec | none | Fires on_action OnActCompleted(4) then OnActStarted(5); Journal entry act-transition; ActState updates; Realm.PolityRank re-evaluation invited | Act 5 | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed) | gd-core-screen.md | <deferred-to-wave-7>: ActFourToFiveTest | throw InvalidStateException("Gate predicate from gd-act-4-gate-spec.md not satisfied at transition Act 4 -> Act 5") |
Act 5 | Gate predicate authored at ../acts/gd-act-5-gate-spec.md | per the cited gate spec | per the cited gate spec | none | Fires on_action OnActCompleted(5) then OnActStarted(6); Journal entry act-transition; ActState updates | Act 6 | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed) | gd-core-screen.md | <deferred-to-wave-7>: ActFiveToSixTest | throw InvalidStateException("Gate predicate from gd-act-5-gate-spec.md not satisfied at transition Act 5 -> Act 6") |
Act 6 | Gate predicate authored at ../acts/gd-act-6-gate-spec.md | per the cited gate spec | per the cited gate spec | none | Fires on_action OnActCompleted(6) then OnActStarted(7); Journal entry act-transition; ActState updates | Act 7 | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed) | gd-core-screen.md | <deferred-to-wave-7>: ActSixToSevenTest | throw InvalidStateException("Gate predicate from gd-act-6-gate-spec.md not satisfied at transition Act 6 -> Act 7") |
Act 7 | Gate predicate authored at ../acts/gd-act-7-gate-spec.md (per ad-0010 lines 101-104) | per the cited gate spec | per the cited gate spec | none | Fires on_action OnActCompleted(7) then the follow-on CampaignEnded event (per ad-0010 lines 101-104); Journal entry campaign-ended; saga-arc finalizer queued (binding <deferred-to-Wave-5a-runtime-backing>) | CampaignEnded | Host/Systems/ActProgressionSystem.cs (proposed) | Host/ReadModels/ActStateReadModel.cs (proposed) | gd-core-screen.md | <deferred-to-wave-7>: ActSevenToCampaignEndedTest | throw InvalidStateException("Gate predicate from gd-act-7-gate-spec.md not satisfied at transition Act 7 -> CampaignEnded") |
CampaignEnded is terminal. No transition leaves it; the saga-arc
finalizer activity (binding <deferred-to-Wave-5a-runtime-backing> per
ad-0010 lines 101-104) consumes the state.
Polity Rank
Owner doc: gd-realm-ranks-and-polity.md.
Binding ADR: ../lore/adr/ad-0009-realm-scope-and-polity-rank-orthogonality.md.
States (per ad-0009 lines 56-58): Outpost, BannerNetwork, Province,
Crown. Forward predicates are authored inline (per resolver decision
R13 — ad-0009 lines 76-87 explicitly invited Wave 3a authoring).
Regression predicates are deferred to Wave 5a (per ad-0009 line 172).
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Outpost | Realm.CoveredSettlements.Count >= Config.Realm.BannerNetworkSettlementThreshold && RouteCoverage(Realm) (per ad-0009 lines 76-80; RouteCoverage(Realm) authored inline in gd-realm-ranks-and-polity.md as the connectivity check over the live Route graph filtered to non-Severed Routes) | Realm.CoveredSettlements populated; Route graph reachable; no Severed Routes break connectivity for the vertex pairs | Config.Realm.BannerNetworkSettlementThreshold (default 2, world-size parameterized) | none | Fires on_action PolityRankAdvanced(BannerNetwork) (working name; promotion deferred to Wave 5a per ad-0009 line 172); Journal entry polity-advanced; Settlement screen tier indicator updates; standing-Force authoring unlocks | BannerNetwork | Host/Systems/RealmRankSystem.cs (proposed) | Host/ReadModels/RealmReadModel.cs (proposed; contract-only) | Crown-scale strategic map polity overlay; gd-settlement-screen.md tier indicator | <deferred-to-wave-7>: PolityOutpostToBannerNetworkTest | throw InvalidStateException("RouteCoverage(Realm) returned false at transition Outpost -> BannerNetwork for RealmId=<id>") |
BannerNetwork | Realm.Settlements.Any(s => s.SettlementRole == ProvinceCapital) (per ad-0009 lines 81-84) | at least one Settlement under the Realm carries SettlementRole == ProvinceCapital | none today (the predicate is a count threshold of one) | none | Fires on_action PolityRankAdvanced(Province) (working name); Journal entry polity-advanced; DefenseZone authoring unlocks per ad-0009 lines 89-92; ProvinceDefensePlan capability surfaces | Province | Host/Systems/RealmRankSystem.cs (proposed) | Host/ReadModels/RealmReadModel.cs (proposed) | Crown-scale strategic map; gd-settlement-screen.md tier indicator | <deferred-to-wave-7>: PolityBannerNetworkToProvinceTest | throw InvalidStateException("No Settlement with SettlementRole=ProvinceCapital under RealmId=<id> at transition BannerNetwork -> Province") |
Province | Realm.Provinces.Count(p => p.HasProvinceCapital) >= Config.Realm.CrownProvinceThreshold (per ad-0009 lines 85-87; default 2, world-size parameterized) | at least two Provinces with ProvinceCapitals exist under the Realm | Config.Realm.CrownProvinceThreshold (default 2, world-size parameterized) | none | Fires on_action PolityRankAdvanced(Crown) (working name); Journal entry polity-advanced; Front authoring unlocks per ad-0009 lines 89-92; multi-Province campaign authority surfaces | Crown | Host/Systems/RealmRankSystem.cs (proposed) | Host/ReadModels/RealmReadModel.cs (proposed) | Crown-scale strategic map; gd-settlement-screen.md tier indicator | <deferred-to-wave-7>: PolityProvinceToCrownTest | throw InvalidStateException("Province count with ProvinceCapital under threshold at transition Province -> Crown for RealmId=<id>") |
BannerNetwork | <deferred-to-Wave-5a-runtime-backing> — Realm regression when Route coverage fails (connecting Route enters Severed with no alternative path) or covered-Settlements count falls below threshold; predicate shape deferred per ad-0009 line 172 | predicate facts deferred | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action PolityRankRegressed(Outpost) (working name); Journal entry polity-regressed; standing-Force authoring relocks | Outpost | Host/Systems/RealmRankSystem.cs (proposed) | Host/ReadModels/RealmReadModel.cs (proposed) | Crown-scale strategic map | <deferred-to-wave-7>: PolityBannerNetworkToOutpostTest | throw InvalidStateException("Regression predicate not defined for transition BannerNetwork -> Outpost on RealmId=<id>") |
Province | <deferred-to-Wave-5a-runtime-backing> — Realm regression on loss of every ProvinceCapital (capital destroyed, demoted, or captured); predicate shape deferred per ad-0009 line 172 | predicate facts deferred | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action PolityRankRegressed(BannerNetwork) (working name); Journal entry polity-regressed; DefenseZone authoring relocks; existing committed plans flagged for re-evaluation | BannerNetwork | Host/Systems/RealmRankSystem.cs (proposed) | Host/ReadModels/RealmReadModel.cs (proposed) | Crown-scale strategic map | <deferred-to-wave-7>: PolityProvinceToBannerNetworkTest | throw InvalidStateException("Regression predicate not defined for transition Province -> BannerNetwork on RealmId=<id>") |
Crown | <deferred-to-Wave-5a-runtime-backing> — Realm regression on Crown-scale catastrophe (loss of all but one ProvinceCapital); predicate shape deferred per ad-0009 line 172 | predicate facts deferred | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action PolityRankRegressed(Province) (working name); Journal entry polity-regressed; Front authoring relocks; existing committed Fronts flagged for re-evaluation | Province | Host/Systems/RealmRankSystem.cs (proposed) | Host/ReadModels/RealmReadModel.cs (proposed) | Crown-scale strategic map | <deferred-to-wave-7>: PolityCrownToProvinceTest | throw InvalidStateException("Regression predicate not defined for transition Crown -> Province on RealmId=<id>") |
Front Emergence (ThreatSource and FrontCandidate)
Owner doc: gd-front-emergence.md. Binding
ADR: ../lore/adr/ad-0008-pressure-to-front-emergence-scope-state-machine.md.
Two state ladders sit beneath the Front lifecycle. The ThreatSource
scope carries a DiscoveryState ladder (per ad-0008 lines 83-85);
the FrontCandidate scope carries an implicit lifecycle (per ad-0008
lines 90-100, 112-117). Both ladders are persistent named state, not
transient events (ad-0008 Tier 6 line 112-120 is explicit).
ThreatSource DiscoveryState
States (per ad-0008 lines 83-85): Hidden, Suspected, Confirmed,
Reported. ThreatSource lifecycle (created at materialization,
retired when underlying cause resolves) carries a separate implicit
shape; its retirement enum is <deferred-to-Wave-5a-runtime-backing>
per ad-0008 lines 198-202.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
| (no prior row — creation) | Province-level pressure channel (ProvinceThreatPressure, ProvinceWardIntegrity, or ProvinceRouteSafety) crosses its committed clamp value under the 6-phase channel pipeline (per ad-0008 lines 70-77; thresholds live as clamp values, not as aggregator constants) | per-Province contributory channel resolves above clamp; anchor Territory / Site / Route exists for the Province | clamp values on the three Province channels (declared in Content/provinces/channels.secs; field names <deferred-to-Wave-5a-runtime-backing> per ad-0008 lines 62-66) | none | Creates scope ThreatSource row with Kind, AnchorTerritoryId / AnchorSiteId / AnchorRouteId, DiscoveryState = Hidden, Severity channel attached; fires on_action ThreatSourceMaterialized(threatSourceId) per ad-0008 lines 112-114; Journal entry threat-source-materialized | Hidden | host-side; aggregation flows through the 6-phase channel pipeline (no per-tick aggregator system per ad-0008 lines 74-77) | Host/ReadModels/ThreatSourcesReadModel.cs (proposed; contract-only) | Crown-scale strategic map; gd-territory-dossier.md threat markers | <deferred-to-wave-7>: ThreatSourceMaterializedTest | throw InvalidStateException("Province channel clamp not declared at ThreatSource materialization for ProvinceId=<id>") |
Hidden | clue or scout observable resolves (<deferred-to-Wave-5a-runtime-backing> — observable predicate per ad-0008 line 83-85) | observable predicate reads true | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action ThreatSourceSuspected(threatSourceId) (working name); Journal entry threat-source-suspected; Territory dossier surfaces hint | Suspected | host-side observable evaluator (proposed; contract-only) | Host/ReadModels/ThreatSourcesReadModel.cs (proposed) | gd-territory-dossier.md | <deferred-to-wave-7>: ThreatSourceHiddenToSuspectedTest | throw InvalidStateException("Observable predicate not defined for ThreatSourceId=<id> at transition Hidden -> Suspected") |
Suspected | scout activity or clue chain confirms identity (<deferred-to-Wave-5a-runtime-backing> — confirmation predicate per ad-0008 line 83-85) | confirmation predicate reads true | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action ThreatSourceConfirmed(threatSourceId) (working name); Journal entry threat-source-confirmed; Territory dossier surfaces named threat | Confirmed | host-side observable evaluator (proposed) | Host/ReadModels/ThreatSourcesReadModel.cs (proposed) | gd-territory-dossier.md | <deferred-to-wave-7>: ThreatSourceSuspectedToConfirmedTest | throw InvalidStateException("Confirmation predicate not defined for ThreatSourceId=<id> at transition Suspected -> Confirmed") |
Confirmed | named reporter (Crown-scale messenger, scout return, Front intelligence) surfaces the threat to the Crown player (<deferred-to-Wave-5a-runtime-backing> — reporting predicate per ad-0008 line 83-85) | reporting predicate reads true | <deferred-to-Wave-5a-runtime-backing> | none | Fires on_action ThreatSourceReported(threatSourceId) (working name); Journal entry threat-source-reported; Crown-scale map roster surfaces threat | Reported | host-side reporter evaluator (proposed) | Host/ReadModels/ThreatSourcesReadModel.cs (proposed) | Crown-scale strategic map; gd-territory-dossier.md | <deferred-to-wave-7>: ThreatSourceConfirmedToReportedTest | throw InvalidStateException("Reporting predicate not defined for ThreatSourceId=<id> at transition Confirmed -> Reported") |
| any DiscoveryState | <deferred-to-Wave-5a-runtime-backing> — underlying cause resolves (Site cleansed, gate scarring sealed, faction encroachment repulsed, ward-line restored); retirement enum shape deferred per ad-0008 lines 198-202 | resolution predicate reads true | <deferred-to-Wave-5a-runtime-backing> | none | Retires scope ThreatSource row; fires on_action ThreatSourceRetired(threatSourceId) (working name); Journal entry threat-source-retired; downstream FrontCandidates re-evaluate aggregation | retired | host-side resolution evaluator (proposed) | Host/ReadModels/ThreatSourcesReadModel.cs (proposed) | Crown-scale strategic map | <deferred-to-wave-7>: ThreatSourceRetiredTest | throw InvalidStateException("Resolution predicate not defined for ThreatSourceId=<id>") |
FrontCandidate Lifecycle
The FrontCandidate scope carries an implicit lifecycle (ad-0008 lines
90-100). The states named below are working labels (<deferred-to-Wave-5a-runtime-backing>
for promotion to a committed enum). The transitions themselves are
committed by ad-0008.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
| (no prior row — creation) | one or more ThreatSources aggregate; AggregatedSeverity channel on the proposed FrontCandidate resolves above its clamp value (per ad-0008 lines 90-100) | at least one scope ThreatSource row in Confirmed or Reported exists under the Realm; aggregation pipeline produces AggregatedSeverity above clamp | clamp value on AggregatedSeverity (declared in Content/fronts/scopes.secs extension; field name <deferred-to-Wave-5a-runtime-backing>) | none | Creates scope FrontCandidate row with ProposedHqTerritoryId, CoveredProvinceIds, AggregatedSeverity, ProposedCampaignGoals, DismissedByCrown=0; fires on_action FrontCandidateRaised(candidateId) per ad-0008 lines 114-115; Journal entry front-candidate-raised; Crown-scale map surfaces candidate | Raised (working label) | host-side; aggregation flows through the 6-phase channel pipeline (no per-tick aggregator per ad-0008 lines 74-77) | Host/ReadModels/FrontCandidatesReadModel.cs (proposed; contract-only) | Crown-scale strategic map FrontCandidate roster | <deferred-to-wave-7>: FrontCandidateRaisedTest | throw InvalidStateException("AggregatedSeverity clamp not declared at FrontCandidate creation") |
Raised (working label) | Crown player sets DismissedByCrown = 1 via dismissal activity (per ad-0008 lines 90-100; activity binding <deferred-to-Wave-5a-runtime-backing>) | Crown-scale dismissal activity issued | none | candidate already elevated to a Front in this tick | Sets DismissedByCrown = 1; fires on_action FrontCandidateDismissed(candidateId) (working name); Journal entry front-candidate-dismissed; Crown-scale map removes candidate marker | Dismissed (working label) | host-side dismissal handler (proposed) | Host/ReadModels/FrontCandidatesReadModel.cs (proposed) | Crown-scale strategic map | <deferred-to-wave-7>: FrontCandidateDismissedTest | throw InvalidStateException("Dismissal activity not bound for FrontCandidateId=<id>") |
Raised (working label) | Crown-scale Activity issued by player calls Front.SetStatus(Forming) and binds the FrontCandidate's ThreatSources / ProposedHqTerritoryId / ProposedCampaignGoals into a new Front row (per ad-0008 lines 102-110) | Crown-scale elevation activity issued; Realm.PolityRank == Crown per ad-0009 lines 89-92 | none | Realm.PolityRank != Crown (Front authoring locked); candidate already Dismissed | Creates scope Front row with Status = Forming (the existing FrontStatus enum per Content/forces/scopes.secs line 222); fires on_action FrontDeclared(frontId, candidateId) per ad-0008 lines 116-117; Journal entry front-declared; FrontCandidate row retired or repurposed (binding <deferred-to-Wave-5a-runtime-backing>) | Elevated (working label; the Front itself enters FrontStatus.Forming) | host-side elevation handler (proposed) | Host/ReadModels/FrontCandidatesReadModel.cs + existing Host/ReadModels/FrontsReadModel.cs | Crown-scale strategic map; Front roster screen | <deferred-to-wave-7>: FrontCandidateElevatedTest | throw InvalidStateException("Elevation activity issued but Realm.PolityRank != Crown for FrontCandidateId=<id>") |
Front Lifecycle (existing)
Owner doc: gd-fronts.md. Committed enum:
FrontStatus per Content/forces/scopes.secs line 222 (0 Forming, 1 Active, 2 Paused, 3 Concluded, 4 Failed). The Front itself is
existing committed surface; this hub binds its transitions into the
canonical row format.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
| (no prior row — creation by Crown elevation) | FrontCandidate elevation activity calls Front.SetStatus(Forming) (per ad-0008 lines 102-110 and Content/forces/scopes.secs line 245 method void SetStatus(int status)) | elevation activity issued from a Raised FrontCandidate; Realm.PolityRank == Crown | none | none | Creates scope Front row with Status = Forming; fires on_action FrontDeclared(frontId, candidateId) per ad-0008 line 116-117; Journal entry front-declared | Forming | existing Host/Data/FrontData.cs (no new system needed for lifecycle alone) | existing Host/ReadModels/FrontsReadModel.cs (projects nothing today per gd-forces-phase-b-design-questions.md line 492; populated in Wave 5a) | Crown-scale strategic map; Front roster screen | <deferred-to-wave-7>: FrontFormingCreationTest | throw InvalidStateException("Realm.PolityRank != Crown at Front creation") |
Forming | <deferred-to-Wave-5a-runtime-backing> — Crown player commits Province plans and Forces per gd-fronts.md line 181 (concrete trigger predicate deferred per ad-0009 line 172 working-name promotion deferral and per ad-0008 line 102-110 activity binding deferral) | committed ProvinceDefensePlan rows reference the Front; at least one Force assigned to a Front objective | <deferred-to-Wave-5a-runtime-backing> | no Force assigned | Sets Front.Status = Active via SetStatus(Active); fires on_action FrontActivated(frontId) (working name; promotion deferred to Wave 5a); Journal entry front-activated | Active | existing Host/Data/FrontData.cs | Host/ReadModels/FrontsReadModel.cs | Crown-scale strategic map; Front roster screen | <deferred-to-wave-7>: FrontFormingToActiveTest | throw InvalidStateException("No Force assigned to FrontId=<id> at transition Forming -> Active") |
Active | Crown player explicit SetStatus(Paused) activity (per Content/forces/scopes.secs line 245) | Crown-scale pause activity issued | none | none | Sets Front.Status = Paused; fires on_action FrontPaused(frontId) (working name); Journal entry front-paused; Forces under the Front halt advance | Paused | existing Host/Data/FrontData.cs | Host/ReadModels/FrontsReadModel.cs | Front roster screen | <deferred-to-wave-7>: FrontActiveToPausedTest | throw InvalidStateException("Pause activity not bound for FrontId=<id>") |
Paused | Crown player explicit SetStatus(Active) activity | Crown-scale resume activity issued | none | none | Sets Front.Status = Active; fires on_action FrontResumed(frontId) (working name); Journal entry front-resumed; Forces resume advance | Active | existing Host/Data/FrontData.cs | Host/ReadModels/FrontsReadModel.cs | Front roster screen | <deferred-to-wave-7>: FrontPausedToActiveTest | throw InvalidStateException("Resume activity not bound for FrontId=<id>") |
Active | Crown player explicit SetStatus(Concluded) activity (per gd-fronts.md lines 192-195 — a Front is not auto-concluded; the Crown player must acknowledge conclusion explicitly) | Crown-scale conclusion activity issued; player acknowledges Campaign Goals met | none | none | Sets Front.Status = Concluded; fires on_action FrontConcluded(frontId) (working name); Journal entry front-concluded; Forces released; existing on_action FrontConcluded surfaces in Saga recap | Concluded | existing Host/Data/FrontData.cs | Host/ReadModels/FrontsReadModel.cs | Front roster screen; Saga recap | <deferred-to-wave-7>: FrontActiveToConcludedTest | throw InvalidStateException("Conclusion activity not bound for FrontId=<id>") |
Active | Crown player explicit SetStatus(Failed) activity (per gd-fronts.md lines 192-195 — failure is acknowledged explicitly) | Crown-scale failure activity issued; player acknowledges Campaign Goals unreachable | none | none | Sets Front.Status = Failed; fires on_action FrontFailed(frontId) (working name); Journal entry front-failed; Forces released; Saga recap records failure | Failed | existing Host/Data/FrontData.cs | Host/ReadModels/FrontsReadModel.cs | Front roster screen; Saga recap | <deferred-to-wave-7>: FrontActiveToFailedTest | throw InvalidStateException("Failure activity not bound for FrontId=<id>") |
ProvinceDefensePlan PlanState
Owner doc: gd-defense-zones.md (the
## PlanState section authored by Foundation Hardening). Authoring
authority: Foundation Hardening resolver decision R4 (2026-05-17). No
separate ADR file backs this enum because the state machine itself is
the authoritative record; the authoring routing decision is recorded in
the owner doc.
States (per gd-defense-zones.md ## PlanState): Unplanned (0),
Drafting (1), Committed (2), Failed (3), Recovered (4). Every
Province with polity ownership carries a ProvinceDefensePlan row from
the moment the Province is created; Unplanned is the explicit
starting state, not the absence of a row.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Unplanned | Crown opens the plan UI for the Province (activity binding <deferred-to-Wave-5a-runtime-backing>) | Crown-scale draft activity issued; Realm.PolityRank >= Province per ad-0009 lines 89-92 | none | Realm.PolityRank < Province (DefenseZone authoring locked) | Sets PlanState = Drafting; fires on_action PlanDrafting(planId) (working name; promotion deferred to Wave 5a); Journal entry plan-drafting | Drafting | Host/Systems/ProvinceDefensePlanSystem.cs (proposed) | existing Host/ReadModels/ProvinceDefensePlansReadModel.cs (projects nothing today; populated in Wave 5a) | gd-territory-dossier.md; Defense Zone screen (<screen not yet authored>) | <deferred-to-wave-7>: PlanUnplannedToDraftingTest | throw InvalidStateException("Realm.PolityRank < Province at transition Unplanned -> Drafting for ProvinceId=<id>") |
Drafting | Crown finalizes the plan via Commit activity (activity binding <deferred-to-Wave-5a-runtime-backing>) | at least one DefenseZone with valid Posture and assigned Force exists on the plan (per gd-defense-zones.md ## PlanState) | none | no DefenseZone with valid Posture and Force assignment | Sets PlanState = Committed; fires on_action PlanCommitted(planId) (working name); Journal entry plan-committed; tick pipeline honors Force assignments; DefaultDoctrine and DefaultReservePercent take effect | Committed | Host/Systems/ProvinceDefensePlanSystem.cs (proposed) | Host/ReadModels/ProvinceDefensePlansReadModel.cs | gd-territory-dossier.md; Defense Zone screen | <deferred-to-wave-7>: PlanDraftingToCommittedTest | throw InvalidStateException("Commit refused: no DefenseZone with valid Posture and Force assignment for PlanId=<id>") |
Committed | DefenseZone failure cascades to plan-level failure (per gd-defense-zones.md ## Zone Failure And Recovery; cascade predicate <deferred-to-Wave-5a-runtime-backing>) | a DefenseZone under the plan has failed (Capital zone lost, supply Route severed beyond endurance, or equivalent committed failure mode) | <deferred-to-Wave-5a-runtime-backing> | none | Sets PlanState = Failed; fires on_action PlanFailed(planId, failedZoneId) (working name); Journal entry plan-failed; Forces under the plan released or relocated; Current Plan urgency promotes | Failed | Host/Systems/ProvinceDefensePlanSystem.cs (proposed) | Host/ReadModels/ProvinceDefensePlansReadModel.cs | gd-territory-dossier.md | <deferred-to-wave-7>: PlanCommittedToFailedTest | throw InvalidStateException("Failure cascade predicate not defined for PlanId=<id>") |
Failed | Crown re-commits the plan via Recommit activity (activity binding <deferred-to-Wave-5a-runtime-backing>) | Crown-scale recommit activity issued; at least one DefenseZone with valid Posture and Force assignment exists | none | no DefenseZone with valid Posture and Force assignment | Sets PlanState = Recovered; fires on_action PlanRecovered(planId) (working name); Journal entry plan-recovered; tick pipeline resumes honoring Force assignments | Recovered | Host/Systems/ProvinceDefensePlanSystem.cs (proposed) | Host/ReadModels/ProvinceDefensePlansReadModel.cs | gd-territory-dossier.md | <deferred-to-wave-7>: PlanFailedToRecoveredTest | throw InvalidStateException("Recommit refused: no DefenseZone with valid Posture and Force assignment for PlanId=<id>") |
Recovered | hysteresis window elapses with no new DefenseZone failures (hysteresis predicate <deferred-to-Wave-5a-runtime-backing>; the design intent is that Recovered collapses back to Committed after stability, so the historical record can distinguish first-commit from recovery while the running state matches normal Committed) | no DefenseZone failure during the hysteresis window | Config.ProvinceDefensePlan.RecoveryHysteresisWindow (named; default world-size parameterized; <deferred-to-Wave-5a-runtime-backing>) | a DefenseZone failure occurred during the window | Sets PlanState = Committed; fires on_action PlanRecoveryComplete(planId) (working name); Journal entry plan-recovery-complete | Committed | Host/Systems/ProvinceDefensePlanSystem.cs (proposed) | Host/ReadModels/ProvinceDefensePlansReadModel.cs | gd-territory-dossier.md | <deferred-to-wave-7>: PlanRecoveredToCommittedTest | throw InvalidStateException("Hysteresis window not defined for PlanId=<id> at transition Recovered -> Committed") |
Force Lifecycle (existing)
Owner doc: gd-forces.md. Committed enum:
LifecycleState per Content/forces/scopes.secs lines 58-60 (0 Forming, 1 Available, 2 Operating, 3 Returning, 4 Disbanding, 5 Lost).
The existing doc enumerates lifecycle states without committing every
transition predicate inline; predicate cells <deferred-to-Wave-5a-runtime-backing>
where the existing doc is silent.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Forming | <deferred-to-Wave-5a-runtime-backing> — recruitment / equipment fill reaches Strength >= MinStrengthThreshold per gd-forces.md lines 259-261; threshold name <deferred-to-Wave-5a-runtime-backing> | Strength channel above threshold; detachment roster populated | Config.Force.MinStrengthThreshold (named; <deferred-to-Wave-5a-runtime-backing>) | none | Fires on_action ForceAvailable(forceId) (working name); Journal entry force-available; Force becomes assignable to Operations | Available | existing Host/Data/ForceData.cs (proposed system <deferred-to-Wave-5a-runtime-backing>) | existing Host/ReadModels/ForcesReadModel.cs (projects nothing today; populated in Wave 5a) | Force order-of-battle screen (<screen not yet authored>) | <deferred-to-wave-7>: ForceFormingToAvailableTest | throw InvalidStateException("MinStrengthThreshold not declared for ForceId=<id>") |
Available | Operation begin activity fires (Operation.Begin() method per Content/forces/scopes.secs line 134 and gd-forces.md line 262) | a child Operation row enters Status = Active | none | Force already Operating on a different Operation (single-Operation-per-Force constraint) | Fires on_action ForceOperating(forceId, operationId) (working name); Journal entry force-operating | Operating | existing Host/Data/ForceData.cs | Host/ReadModels/ForcesReadModel.cs | Force order-of-battle screen | <deferred-to-wave-7>: ForceAvailableToOperatingTest | throw InvalidStateException("Operation.Begin() called but Operation not in Pending for ForceId=<id>") |
Operating | Operation reaches Status = Completed / Cancelled / Failed and the Force enters return movement (<deferred-to-Wave-5a-runtime-backing> — return predicate per gd-forces.md line 269-270) | child Operation row reaches terminal Status | none | none | Fires on_action ForceReturning(forceId) (working name); Journal entry force-returning | Returning | existing Host/Data/ForceData.cs | Host/ReadModels/ForcesReadModel.cs | Force order-of-battle screen | <deferred-to-wave-7>: ForceOperatingToReturningTest | throw InvalidStateException("Return predicate not defined for ForceId=<id>") |
Returning | Force reaches home Territory or depot (<deferred-to-Wave-5a-runtime-backing> — arrival predicate per gd-forces.md line 269-270) | Force HomeTerritoryId reached or depot anchor satisfied | none | none | Fires on_action ForceAvailable(forceId) (working name); Journal entry force-available | Available | existing Host/Data/ForceData.cs | Host/ReadModels/ForcesReadModel.cs | Force order-of-battle screen | <deferred-to-wave-7>: ForceReturningToAvailableTest | throw InvalidStateException("Arrival predicate not defined for ForceId=<id>") |
Available or Returning | Crown player issues disband activity (activity binding <deferred-to-Wave-5a-runtime-backing>; gd-forces.md line 269-270 notes disband is reversible only until detachments absorbed elsewhere) | Crown-scale disband activity issued | none | Force Operating (cannot disband mid-Operation) | Sets LifecycleState = Disbanding; fires on_action ForceDisbanding(forceId) (working name); Journal entry force-disbanding; detachments queued for absorption | Disbanding | existing Host/Data/ForceData.cs | Host/ReadModels/ForcesReadModel.cs | Force order-of-battle screen | <deferred-to-wave-7>: ForceToDisbandingTest | throw InvalidStateException("Disband activity issued while Force Operating for ForceId=<id>") |
Disbanding | detachments absorbed elsewhere or released (<deferred-to-Wave-5a-runtime-backing> — absorption predicate per gd-forces.md line 269-270) | every detachment under the Force absorbed by another Force or released | none | any detachment still in IsCommitted = 1 | Retires scope Force row; fires on_action ForceRetired(forceId) (working name); Journal entry force-retired | retired | existing Host/Data/ForceData.cs | Host/ReadModels/ForcesReadModel.cs | Force order-of-battle screen | <deferred-to-wave-7>: ForceDisbandingToRetiredTest | throw InvalidStateException("Absorption predicate not defined for ForceId=<id>") |
any non-Lost | Force destroyed in combat or by catastrophic supply collapse (<deferred-to-Wave-5a-runtime-backing> — loss predicate per gd-forces.md line 269-270; explicit per the doc: a Lost Force cannot be recovered) | loss predicate reads true | none | none | Sets LifecycleState = Lost; fires on_action ForceLost(forceId) (working name); Journal entry force-lost; Operations under the Force fail; Saga recap records loss | Lost | existing Host/Data/ForceData.cs | Host/ReadModels/ForcesReadModel.cs | Force order-of-battle screen; Saga recap | <deferred-to-wave-7>: ForceToLostTest | throw InvalidStateException("Loss predicate not defined for ForceId=<id>") |
Operation Lifecycle (existing)
Owner doc: gd-operations.md. Two committed axes
per Content/forces/scopes.secs lines 109-114: Phase (7 values
Scouting / Securing / Staging / Screening / Main / Consolidation / Withdrawal, per lines 108-109) and Status (5 values Pending / Active / Completed / Cancelled / Failed, per lines 110-112). Operations
advance both axes; the canonical phase descriptions live at
gd-operations.md lines 141-153. Per-phase predicates are deferred to
Wave 5a per resolver R13 (the existing doc enumerates phases but does
not commit every advance predicate).
Operation Phase
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Scouting | Operation.AdvancePhase() per Content/forces/scopes.secs line 133; per-phase predicate <deferred-to-Wave-5a-runtime-backing> per gd-operations.md lines 141-143 | scouting beat resolved per the decomposition pattern | <deferred-to-Wave-5a-runtime-backing> | scouting beat unresolved | Fires on_action OperationPhaseAdvanced(operationId, Securing) (working name); Journal entry operation-phase | Securing | existing Host/Data/OperationData.cs (proposed system <deferred-to-Wave-5a-runtime-backing>) | existing Host/ReadModels/OperationsReadModel.cs (projects nothing today; populated in Wave 5a) | strategic map plan-icon roster; Operation 7-phase drill-in | <deferred-to-wave-7>: OperationScoutingToSecuringTest | throw InvalidStateException("Advance predicate not defined for Scouting -> Securing on OperationId=<id>") |
Securing | Operation.AdvancePhase(); per-phase predicate <deferred-to-Wave-5a-runtime-backing> | securing beat resolved | <deferred-to-Wave-5a-runtime-backing> | securing beat unresolved | Fires on_action OperationPhaseAdvanced(operationId, Staging); Journal entry operation-phase | Staging | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map; Operation drill-in | <deferred-to-wave-7>: OperationSecuringToStagingTest | throw InvalidStateException("Advance predicate not defined for Securing -> Staging on OperationId=<id>") |
Staging | Operation.AdvancePhase(); per-phase predicate <deferred-to-Wave-5a-runtime-backing> | staging beat resolved | <deferred-to-Wave-5a-runtime-backing> | staging beat unresolved | Fires on_action OperationPhaseAdvanced(operationId, Screening); Journal entry operation-phase | Screening | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map; Operation drill-in | <deferred-to-wave-7>: OperationStagingToScreeningTest | throw InvalidStateException("Advance predicate not defined for Staging -> Screening on OperationId=<id>") |
Screening | Operation.AdvancePhase(); per-phase predicate <deferred-to-Wave-5a-runtime-backing> | screening beat resolved | <deferred-to-Wave-5a-runtime-backing> | screening beat unresolved | Fires on_action OperationPhaseAdvanced(operationId, Main); Journal entry operation-phase | Main | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map; Operation drill-in | <deferred-to-wave-7>: OperationScreeningToMainTest | throw InvalidStateException("Advance predicate not defined for Screening -> Main on OperationId=<id>") |
Main | Operation.AdvancePhase(); per-phase predicate <deferred-to-Wave-5a-runtime-backing> | main beat resolved | <deferred-to-Wave-5a-runtime-backing> | main beat unresolved | Fires on_action OperationPhaseAdvanced(operationId, Consolidation); Journal entry operation-phase | Consolidation | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map; Operation drill-in | <deferred-to-wave-7>: OperationMainToConsolidationTest | throw InvalidStateException("Advance predicate not defined for Main -> Consolidation on OperationId=<id>") |
Consolidation | Operation.AdvancePhase(); per-phase predicate <deferred-to-Wave-5a-runtime-backing> | consolidation beat resolved | <deferred-to-Wave-5a-runtime-backing> | consolidation beat unresolved | Fires on_action OperationPhaseAdvanced(operationId, Withdrawal); Journal entry operation-phase | Withdrawal | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map; Operation drill-in | <deferred-to-wave-7>: OperationConsolidationToWithdrawalTest | throw InvalidStateException("Advance predicate not defined for Consolidation -> Withdrawal on OperationId=<id>") |
Withdrawal | Operation.AdvancePhase() followed by Status transition to Completed; per-phase predicate <deferred-to-Wave-5a-runtime-backing> | withdrawal beat resolved; Force returned home or arrived at depot | <deferred-to-Wave-5a-runtime-backing> | withdrawal beat unresolved | Sets Status = Completed; fires on_action OperationCompleted(operationId) (working name); Journal entry operation-completed; parent Force enters Returning evaluation | Withdrawal (terminal Phase; Status moves to Completed) | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map; Operation drill-in | <deferred-to-wave-7>: OperationWithdrawalToCompletedTest | throw InvalidStateException("Withdrawal completion predicate not defined for OperationId=<id>") |
Operation Status
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
Pending | Operation.Begin() per Content/forces/scopes.secs line 134; required scouting precondition met per gd-operations.md line 141 | scouting beat preconditions met; parent Force in Available | none | parent Force in Operating, Forming, Disbanding, or Lost | Sets Status = Active; fires on_action OperationActivated(operationId) (working name); Journal entry operation-active; parent Force enters Operating | Active | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map; Operation drill-in | <deferred-to-wave-7>: OperationPendingToActiveTest | throw InvalidStateException("Operation.Begin() refused: parent Force not Available for OperationId=<id>") |
Active | Withdrawal Phase resolves and Operation completion predicate reads true (<deferred-to-Wave-5a-runtime-backing> per gd-operations.md lines 141-153) | Phase reaches Withdrawal; completion predicate reads true | <deferred-to-Wave-5a-runtime-backing> | Phase not Withdrawal | Sets Status = Completed; fires on_action OperationCompleted(operationId); Journal entry operation-completed | Completed | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map | <deferred-to-wave-7>: OperationActiveToCompletedTest | throw InvalidStateException("Completion predicate not defined for OperationId=<id>") |
Active | Crown player issues Operation.Cancel() per Content/forces/scopes.secs line 135 | Crown-scale cancel activity issued | none | none | Sets Status = Cancelled; fires on_action OperationCancelled(operationId) (working name); Journal entry operation-cancelled; parent Force enters Returning | Cancelled | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map | <deferred-to-wave-7>: OperationActiveToCancelledTest | throw InvalidStateException("Cancel activity not bound for OperationId=<id>") |
Active | Operation failure predicate reads true (<deferred-to-Wave-5a-runtime-backing> per gd-operations.md lines 141-153; e.g., target lost, supply collapse, Force loss) | failure predicate reads true | <deferred-to-Wave-5a-runtime-backing> | none | Sets Status = Failed; fires on_action OperationFailed(operationId) (working name); Journal entry operation-failed; parent Force enters Returning or Lost | Failed | existing Host/Data/OperationData.cs | Host/ReadModels/OperationsReadModel.cs | strategic map | <deferred-to-wave-7>: OperationActiveToFailedTest | throw InvalidStateException("Failure predicate not defined for OperationId=<id>") |
Feature / Territory / Site State Ladders (cite-only)
Three committed state-axis enums live in the Generated tree and are
owned by gd-state-axes-and-thresholds.md.
This hub does NOT redefine them; transition cells reference the
generation-contract docs that author the read-model-side transitions.
FeatureDiscoveryState(9 states perGenerated/Territories/FeaturePlacement/FeaturePlacementTypes.csand gd-state-axes-and-thresholds.md lines 42-57):Hidden,Hinted,Discovered,Surveyed,Handled,Secured,Developed,Depleted,Destroyed.TerritoryKnowledgeState(8 states perGenerated/Territories/TerritoryKnowledgeTypes.csand gd-state-axes-and-thresholds.md lines 110-127).TerritoryContentState(10 states perGenerated/Territories/FeaturePlacement/FeatureContentTypes.csand gd-state-axes-and-thresholds.md lines 153-164).
Transition cells: <authored in [gd-feature-generation-contract.md](/secs/valenar/generation/gd-feature-generation-contract/) / [gd-territory-generation-contract.md](/secs/valenar/generation/gd-territory-generation-contract/) — read-model-side, not authored here>. The transitions belong to the
discovery / knowledge / content pipelines owned by the feature- and
territory-generation contracts. Future Wave 5a (or a separate
read-model-side wave) authors the transition-row table for those three
ladders against the same 12-column schema; this hub will link those
tables in place of the cite-only notice.
MC Condition Ladders
Owner doc: gd-character-conditions-and-injuries.md.
Binding ADR: ../lore/adr/ad-0012-character-conditions-and-injuries-as-modifiers.md.
Conditions are Modifier-attached, not scope-state-channel (per ad-0012 lines 111-126 and ad-0013 lines 111-124 — conditions are not an instantiation of the scope + state-channel + template-archetype pattern). The transitions are Modifier apply / decay / clear events, not scope-state transitions. The ladder rungs are authoritative text per ad-0012 lines 102-110, not a runtime enum.
Seven conditions per ad-0012 lines 113-126 and gd-character-conditions-and-injuries.md condition catalog. Each condition's rung labels are owned by the owner doc and cited here rather than re-authored:
| Condition | Rung Ladder (owner doc) |
|---|---|
| Hungry | well-fed -> satisfied -> peckish -> hungry -> starving per gd-character-conditions-and-injuries.md (ad-0012 lines 46-48 seed) |
| Cold | warm -> cool -> chilly -> cold -> freezing per gd-character-conditions-and-injuries.md |
| Exhausted | rested -> tired -> weary -> exhausted -> spent per gd-character-conditions-and-injuries.md |
| Afraid | composed -> wary -> afraid -> terrified -> broken per gd-character-conditions-and-injuries.md |
| Bleeding | clean -> grazed -> bleeding -> hemorrhaging -> exsanguinating per gd-character-conditions-and-injuries.md |
| Tainted | pure -> exposed -> tainted -> corrupted -> consumed per gd-character-conditions-and-injuries.md |
| Poisoned | unaffected -> afflicted -> poisoned -> toxic -> dying per gd-character-conditions-and-injuries.md |
Each ladder runs better-to-worse. The leftmost rung is the absence of the condition; the rightmost rung is the terminal state. The transitions are Modifier-attached, with the apply / tend / clear binding rules owned by gd-character-conditions-and-injuries.md.
| source_state | trigger | required_facts | config_thresholds | blocking_conditions | side_effects | target_state | runtime_owner | read_model | ui_readback | tests | fallback |
|---|---|---|---|---|---|---|---|---|---|---|---|
| any rung (excluding the worst rung) | Worsening pressure source applies a step of the Modifier (apply binding <deferred-to-Wave-5a-runtime-backing> per ad-0012 lines 187-194) | apply predicate from gd-character-conditions-and-injuries.md reads true (e.g., subsistence absent for Hungry; environmental cold exposure for Cold; demonic taint contact for Tainted) | per-condition Config field <deferred-to-Wave-5a-runtime-backing> per ad-0012 lines 102-110 (the exact rung thresholds are a balance pass) | terminal rung already attained | Applies the next-worse ladder-rung Modifier on the named character scope; fires on_action ConditionApplied(characterId, conditionId, newRung) (working name; promotion deferred to Wave 5a); Journal entry condition-applied; Character screen condition strip updates | next-worse rung | existing ModifierBindingStore in src/SECS.Engine/ plus apply / tend / clear Activities authored in Wave 5a | character info read-model reads Modifier presence on character scope | gd-character-screen.md per ad-0012 lines 200-205 | <deferred-to-wave-7>: ConditionWorsenTest | throw InvalidStateException("Apply predicate not defined for ConditionId=<id> on CharacterId=<id>") |
| any rung (excluding the best rung) | Tending or recovery binding clears one step (tend / clear binding <deferred-to-Wave-5a-runtime-backing> per ad-0012 lines 187-194) | recovery predicate reads true (e.g., eating returns Hungry toward well-fed; warming Items / shelter returns Cold toward warm; ward-stone or ritual returns Tainted toward pure) | per-condition Config field <deferred-to-Wave-5a-runtime-backing> | best rung already attained | Clears the current ladder-rung Modifier and applies the next-better rung Modifier (or clears entirely if at second-best rung); fires on_action ConditionImproved(characterId, conditionId, newRung) (working name); Journal entry condition-improved; Character screen condition strip updates | next-better rung | existing ModifierBindingStore plus tend / clear Activities authored in Wave 5a | character info read-model | gd-character-screen.md | <deferred-to-wave-7>: ConditionImproveTest | throw InvalidStateException("Recovery predicate not defined for ConditionId=<id> on CharacterId=<id>") |
| terminal rung | Sustained terminal exposure resolves with an acquired-trait conversion (per ad-0012 lines 95-104 and gd-character-conditions-and-injuries.md ## Acquired Trait Conversion) | terminal-rung Modifier remains applied for the conversion duration (<deferred-to-Wave-5a-runtime-backing>); conversion predicate reads true | Config.Condition.<Name>.TerminalConversionDuration (named; <deferred-to-Wave-5a-runtime-backing>) | character died from the terminal condition before conversion | Applies the acquired trait per the per-condition conversion target rule (per ad-0012 lines 95-104); fires on_action ConditionConverted(characterId, conditionId, traitId) (working name); Journal entry condition-converted | terminal rung cleared; acquired trait applied | existing ModifierBindingStore plus conversion Activity authored in Wave 5a | character info read-model | gd-character-screen.md | <deferred-to-wave-7>: ConditionTerminalConversionTest | throw InvalidStateException("Terminal conversion predicate not defined for ConditionId=<id> on CharacterId=<id>") |
Injuries are single-rung Modifiers (per gd-character-conditions-and-injuries.md
## Injury Catalog); they do not carry a per-injury rung ladder.
Injury apply / heal transitions follow the same Modifier-attached
shape as the condition rung-step transitions above; the per-injury
binding rules are owned by gd-character-conditions-and-injuries.md
and are not re-tabled here.
Glossary Propagation
The following terms are introduced or aggregated by this hub. Wave 3c
(canonical propagation) propagates them to
gd-glossary.md, gd-canon.md,
and README.md per
.claude/rules/valenar-contract-backing.md § Canonical Glossary Targets.
- Transition row — a single row in any state-machine table on this page; carries the 12-column schema defined above.
<deferred-to-Wave-5a-runtime-backing>— the canonical marker for a transition cell whose authoritative predicate or threshold field is deferred to Wave 5a runtime-backing closure. The marker is greppable: a flat list of every cell carrying it is the open follow-on list below.<deferred-to-wave-7>— the canonical marker for thetests_requiredcolumn. Each test name is greppable so the Wave 7 test-authoring wave can produce a flat list of every test row this hub generates.<screen not yet authored>— the canonical marker for a UI read-back surface that does not yet have a committedux/gd-*.mdowner doc.(working name)— the canonical annotation for anon_actionlabel whose promotion to committed metadata is deferred to Wave 5a.- Source state / target state / trigger / required facts / config thresholds / blocking conditions / side effects / runtime owner / read model surface / ui readback / tests required / fallback behavior — the 12 column names of the canonical transition row schema.
- PlanState — the new
ProvinceDefensePlan.PlanStateenum (Foundation Hardening resolver decision R4 on 2026-05-17); owned by gd-defense-zones.md. - DiscoveryState — the
ThreatSource.DiscoveryStateenum per ad-0008 lines 83-85. - Quest Thread Lifecycle / Mission Lifecycle / Polity Rank / Act Progression / Front Emergence — names of the five new state-machine families introduced by Foundation Hardening.
Cross-References
State-machine owner docs (the per-doc detail surfaces):
gd-quest-thread-and-mission-state-machine.mdgd-front-emergence.mdgd-act-progression.mdgd-character-conditions-and-injuries.mdgd-realm-ranks-and-polity.mdgd-defense-zones.md
Per-Act gate specifications (each owns its own Act N -> N+1 predicate):
../acts/gd-act-1-gate-spec.md../acts/gd-act-2-gate-spec.md../acts/gd-act-3-gate-spec.md../acts/gd-act-4-gate-spec.md../acts/gd-act-5-gate-spec.md../acts/gd-act-6-gate-spec.md../acts/gd-act-7-gate-spec.md../acts/gd-act-0-mission-spine.md
Existing precedent docs (cited, not redefined here):
gd-fronts.md(FrontStatus 5-state enum and the Crown-explicit conclusion rule)gd-forces.md(Force LifecycleState 6-state enum)gd-operations.md(Operation Phase and Status axes)gd-state-axes-and-thresholds.md(FeatureDiscoveryState / TerritoryKnowledgeState / TerritoryContentState)gd-settlements-and-outposts.md(Settlement.Stage — orthogonal to PolityRank per ad-0009)
Planning vocabulary and pressure model:
gd-objectives-clues-missions.mdgd-quest-and-lore-design.mdgd-dynamic-quest-pressure-model.mdgd-queue-and-activity-execution.md
UX read-back surfaces:
../ux/gd-current-plan.md../ux/gd-journal.md../ux/gd-objectives-screen.md../ux/gd-territory-dossier.md../ux/gd-core-screen.md../ux/gd-character-screen.md../ux/gd-settlement-screen.md
ADRs:
../lore/adr/ad-0007-quest-thread-and-mission-scope-primitives.md../lore/adr/ad-0008-pressure-to-front-emergence-scope-state-machine.md../lore/adr/ad-0009-realm-scope-and-polity-rank-orthogonality.md../lore/adr/ad-0010-act-progression-actstate-scope.md../lore/adr/ad-0011-true-harm-placeholder-convention.md../lore/adr/ad-0012-character-conditions-and-injuries-as-modifiers.md../lore/adr/ad-0013-scope-state-channel-template-archetype-pattern.md
Quest threads index and lanes:
../quest-threads/gd-quest-thread-index.mdplus the 11 lane docs under the same directory.
Rules and authoring authority:
.claude/rules/secs-concepts.md.claude/rules/behavior-vocabulary.md.claude/rules/valenar-contract-backing.md- Foundation Hardening resolver decision R4 (2026-05-17) — PlanState
authoring authority for gd-defense-zones.md
## PlanState.
Open Follow-On Items
Aggregated registry of every <deferred-to-Wave-5a-runtime-backing>
cell in the tables above. Wave 5a authors consume this list to plan the
runtime-backing work. One bullet per deferred predicate / threshold /
binding; the cited owner doc and ADR identify the authoritative source.
Quest Thread Lifecycle:
- QuestThread
Active -> Suspended: precondition violation predicate. Design intent: a thread suspends when its template'sSuspensionPredicatereads true; predicate shape deferred per ad-0007 lines 56-57. - QuestThread
Suspended -> Active: precondition restored predicate; symmetric to the above; deferred per ad-0007 lines 56-57. - QuestThread
Active -> Resolved: ResolutionRule predicate against required child Missions; deferred per ad-0007 lines 56-57. - QuestThread
Active -> Failed: critical-path failure predicate against child Missions; deferred per ad-0007 lines 56-57. - QuestThread
Active -> AbandonedandAvailable -> Abandoned: abandonment / dismissal activity bindings; deferred per ad-0007 lines 56-57.
Mission Lifecycle:
- Mission
Pending -> Available: precondition predicate authored ontemplate<Mission>.PreconditionPredicate; deferred per ad-0007 lines 56-57. - Mission
Available -> Accepted: acceptance activity binding; deferred per ad-0007 lines 56-57. - Mission
Active -> Resolved: ResolutionRule predicate against required activity beats; deferred per ad-0007 lines 56-57. - Mission
Active -> Failed: critical-path beat failure predicate; deferred per ad-0007 lines 56-57. - Mission
Active -> AbandonedandAvailable -> Abandoned: abandonment / dismissal activity bindings; deferred per ad-0007 lines 56-57.
Act Progression:
- Act 7 -> CampaignEnded saga-arc finalizer activity binding; deferred per ad-0010 lines 101-104.
- Working-name promotion for every
on_action OnActCompleted(N)andOnActStarted(N+1)label; deferred per ad-0010.
Polity Rank:
- PolityRank
BannerNetwork -> Outpostregression predicate; design intent: Realm regression when Route coverage fails (SeveredRoute with no alternative path) or covered-Settlements count falls below threshold; deferred per ad-0009 line 172. - PolityRank
Province -> BannerNetworkregression predicate; design intent: Realm regression on loss of every ProvinceCapital (capital destroyed, demoted, or captured); deferred per ad-0009 line 172. - PolityRank
Crown -> Provinceregression predicate; design intent: Realm regression on Crown-scale catastrophe (loss of all but one ProvinceCapital); deferred per ad-0009 line 172. - Working-name promotion for
on_action PolityRankAdvancedandPolityRankRegressedlabels; deferred per ad-0009 line 172.
Front Emergence (ThreatSource and FrontCandidate):
- Territory-level input channel field names (
gate-activity,ward-failure,route-safety); deferred per ad-0008 lines 62-66. - Province-level aggregation channel field names
(
ProvinceThreatPressure,ProvinceWardIntegrity,ProvinceRouteSafety); deferred per ad-0008 lines 62-66. ThreatSource.Kindenum value list; deferred per ad-0008 lines 198-202.Severitychannel's exact 6-phase composition (clamp values, multiplicative contributions); deferred per ad-0008 lines 198-202.ThreatSourceretirement enum shape; deferred per ad-0008 lines 198-202.- ThreatSource
Hidden -> Suspectedobservable predicate; deferred per ad-0008 lines 83-85. - ThreatSource
Suspected -> Confirmedconfirmation predicate; deferred per ad-0008 lines 83-85. - ThreatSource
Confirmed -> Reportedreporting predicate; deferred per ad-0008 lines 83-85. - FrontCandidate working state-label enum (
Raised,Active,Dismissed,Elevated) promotion to a committed enum; deferred per ad-0008 lines 90-100. - FrontCandidate
AggregatedSeverityclamp value; deferred per ad-0008 lines 90-100. - FrontCandidate dismissal and elevation activity bindings; deferred per ad-0008 lines 90-110.
Front Lifecycle (existing):
- Front
Forming -> Activepredicate: Crown player commits Province plans and Forces per gd-fronts.md line 181; predicate shape deferred per ad-0009 working-name promotion and per ad-0008 activity-binding deferral. - Working-name promotion for
on_action FrontActivated,FrontPaused,FrontResumed,FrontConcluded,FrontFailedlabels; deferred per ad-0008.
ProvinceDefensePlan PlanState:
- PlanState
Committed -> Failedcascade predicate from DefenseZone failure modes (Capital zone lost, supply Route severed beyond endurance, or equivalent); deferred per gd-defense-zones.md## Zone Failure And Recovery. - PlanState
Recovered -> Committedhysteresis window (Config.ProvinceDefensePlan.RecoveryHysteresisWindow); deferred per gd-defense-zones.md## PlanState. - Plan draft / commit / recommit activity bindings; deferred per gd-defense-zones.md.
- Working-name promotion for
on_action PlanDrafting,PlanCommitted,PlanFailed,PlanRecovered,PlanRecoveryCompletelabels; deferred per Foundation Hardening resolver R4 (2026-05-17).
Force Lifecycle (existing):
- Force
Forming -> AvailableMinStrengthThresholdConfig field name; deferred per gd-forces.md lines 259-261. - Force
Operating -> Returningreturn predicate; deferred per gd-forces.md line 269-270. - Force
Returning -> Availablearrival predicate; deferred per gd-forces.md line 269-270. - Force
Disbanding -> retiredabsorption predicate; deferred per gd-forces.md line 269-270. - Force any-state ->
Lostloss predicate; deferred per gd-forces.md line 269-270. - Working-name promotion for
on_action ForceAvailable,ForceOperating,ForceReturning,ForceDisbanding,ForceLost,ForceRetiredlabels; deferred to Wave 5a.
Operation Lifecycle (existing):
- Per-phase advance predicates for all six Phase transitions
(
Scouting -> Securing,Securing -> Staging,Staging -> Screening,Screening -> Main,Main -> Consolidation,Consolidation -> Withdrawal); deferred per gd-operations.md lines 141-153. - Operation
Active -> Completedcompletion predicate; deferred per gd-operations.md lines 141-153. - Operation
Active -> Failedfailure predicate; deferred per gd-operations.md lines 141-153. - Working-name promotion for
on_action OperationActivated,OperationPhaseAdvanced,OperationCompleted,OperationCancelled,OperationFailedlabels; deferred to Wave 5a.
MC Condition Ladders:
- Per-condition apply / tend / clear activity bindings; deferred per ad-0012 lines 187-194.
- Per-condition rung-step Config field names (the exact threshold shape for each ladder rung); deferred per ad-0012 lines 102-110 (a balance pass owned by Wave 5a runtime-backing).
- Per-condition terminal-rung acquired-trait conversion duration
(
Config.Condition.<Name>.TerminalConversionDuration); deferred per ad-0012 lines 95-104. - Working-name promotion for
on_action ConditionApplied,ConditionImproved,ConditionConvertedlabels; deferred per ad-0012.
Per-Act gate predicates:
- Acts 1 -> 2 through 6 -> 7 predicates are authored in the seven
cited
gd-act-N-gate-spec.mddocs. Each gate spec owns Config field names for its predicate thresholds; Wave 5a runtime-backing closes the Config wiring and the named-state runtime evaluation.
Feature / Territory / Site State Ladders:
- Transition row tables for FeatureDiscoveryState (9 transitions), TerritoryKnowledgeState (per gd-state-axes-and-thresholds.md lines 110-127), and TerritoryContentState (per gd-state-axes-and-thresholds.md lines 153-164) are deferred to a follow-up read-model-side wave that authors them against this hub's 12-column schema.
Runtime Backing Status
Status: contract-only. No state-machine runtime exists today for any of the five new families. The existing Front / Force / Operation / DefenseZone surfaces have committed scopes and read-model files, but the read-model files project nothing today (per gd-forces-phase-b-design-questions.md line 492); Wave 5a populates them.
- Host/runtime owners (proposed; none exist today):
legacy/v1/examples/valenar/Host/Systems/QuestThreadSystem.cs,MissionSystem.cs,ActProgressionSystem.cs,RealmRankSystem.cs,ProvinceDefensePlanSystem.cs. The ThreatSource / FrontCandidate aggregation flows through the 6-phase channel pipeline rather than a per-tick aggregator system (per ad-0008 lines 74-77, 170-180). - Generated /
.secsowners (proposed; none exist today):Content/quests/scopes.secsandContent/quests/templates/*.secsfor QuestThread / Mission;Content/acts/scopes.secsandContent/acts/systems/act_progression.secsfor ActState;Content/realm/scopes.secsandContent/realm/systems/rank_transition.secsfor Realm; extensions onContent/territories/channels.secs,Content/provinces/channels.secs,Content/threat/scopes.secs, andContent/fronts/scopes.secsfor emergence. MatchingGenerated/stand-ins are authored in the same Wave 5a runtime-backing wave. - Read-model / UI owners: existing Front / Force / Operation /
DefenseZone read-model files exist but project nothing today;
proposed new files
Host/ReadModels/QuestThreadsReadModel.cs,MissionsReadModel.cs,ActStateReadModel.cs,RealmReadModel.cs,ThreatSourcesReadModel.cs,FrontCandidatesReadModel.cs. UI surfaces consuming these read-models include the Crown-scale strategic map, the Current Plan rail, the Journal, the Objectives screen, the Territory Dossier, the Core Screen saga-arc display, the Character Screen condition strip, and the Settlement Screen tier indicator. - Tests: deferred to Wave 7 (post-Wave 5a runtime-backing). Each
transition row carries a
<deferred-to-wave-7>test name in thetests_requiredcolumn; the names are greppable so Wave 7 authors can produce a flat list of every test row this hub generates. - Known gaps: every
<deferred-to-Wave-5a-runtime-backing>cell in the tables above. The aggregated registry of those cells is the## Open Follow-On Itemssection above; Wave 5a authors consume that registry to plan the runtime-backing work. - Illegal fallback behavior: NO silent default transition. NO
day-count predicates. NO real-time-clock predicates. NO magic
threshold constants — use named
Config.<Subsystem>.<FieldName>Config fields with world-size parameterized defaults (per Discipline 14 of.claude/rules/orchestrator-mode.md). NO Modifier bound as a state carrier for any of the five new state-machine families (Modifiers are the carrier ONLY for MC conditions and injuries per ad-0012 lines 111-126 and ad-0013 lines 111-124). NO silentRealm.PolityRank = Outpostdefault; the channel must read through the 6-phase pipeline. NO silent advance ofActState.CurrentActwithout a gate predicate evaluating true. NO silent inheritance of DefenseZone Posture across Province boundaries (per gd-defense-zones.md## PlanState). - Next closure wave: Wave 5a runtime-backing wave authors the host
systems, the
Content/source extensions, the matchingGenerated/stand-ins, theon_actionworking-name promotions, and the Config-field wiring for every transition row deferred above. Parallel work in the same Wave 5a closes the ad-0008 emergence channels and the dynamic-quest-pressure-model storyBias formula.