Skip to main content

ADR 0008 — Pressure-to-Front Emergence as Persistent Scope State Machine

Context

Valenar's Front model in ../../systems/gd-fronts.md commits the Front as a Crown-scale strategic noun grouping ProvinceDefensePlans, Forces, Territories, Routes, Sites, and Threats. The Front itself is already a typed entity with a FrontStatus enum and authored campaign goals. What is not committed is the layer beneath the Front: how world-state pressure (Corruption escalation, ward failure, gate activity, route safety breakdown, hostile faction claims) becomes a named, queryable, player-actionable emergence that can be elevated into a Front. Today every act doc, the dynamic quest pressure contract, and the realm-ranks ladder treat "pressure rising into a Front" as if a single per-tick event or a per-tick aggregator could carry the transition. That is the gap this ADR closes.

Two patterns were viable for the emergence layer. The first was a per-tick event family: a PressureSignal aggregator system runs each tick, summing inputs (Corruption, ward state, gate activity, route safety), comparing the sum against a threshold constant, and firing PressureRoseToFront when the threshold is crossed. The second was binding the emergence layer to a Modifier — declaring a Modifier(PressureCluster) shape that attaches to a province when pressure rises, with the Modifier carrying the cluster's identity. Both patterns collapse under the AAA precedent. CK3 Casus Belli is a persistent queryable claim that the player elevates by deliberate decision, not a per-tick event. CK3 Situations (Struggle, Mongol Invasion), EU4/EU5 Disasters and Situations, Stellaris Galactic Crises (Prethoryn, Contingency, Unbidden), Imperator triggers, RimWorld threat curve, and Total War campaign-map agent emergence all share one structure: the emergence layer is persistent named state with its own lifecycle, not a transient event and not a Modifier. Modifiers are reusable effect bundles, not named persistent entities; per-tick events encode the transition, not the carrier.

The Foundation Hardening needs the emergence-to-Front pathway committed before the urgency-signal binding wave can read Province-level pressure, before the dynamic quest pressure model can generate pressure missions against named emergence entities, and before the Wave 3a state-machine row for the pressure-corruption cross-act lane can land.

Decision

The pressure-to-front pathway is committed as a six-tier persistent SECS-scope state machine, with events firing AT transitions rather than as the emergence carrier itself.

Tier 1 — Territory-level inputs. The existing TerritoryData.Corruption channel remains the primary input. Three additional per-Territory channels are declared in legacy/v1/examples/valenar/Content/territories/channels.secs (and matched in the parallel Generated/Territories/): gate-activity, ward-failure, and route-safety. The exact channel kind (Base / Accumulative / Contributed) and exact field names are deferred to the Wave 5a runtime-backing wave; this ADR commits that these three Territory-level channels exist as a primitive class, not their finalized signatures.

Tier 2 — Province-level aggregation through the 6-phase channel pipeline. Three new contributory channels are declared on the Province scope: ProvinceThreatPressure, ProvinceWardIntegrity, and ProvinceRouteSafety. Aggregation flows through the committed 6-phase channel resolution pipeline (base -> additive -> multiplicative -> HardOverride -> clamp -> return) — not through an aggregator system that compares the sum against magic-constant thresholds. The "Never tune parameters as a substitute for fixing architecture" tenet applies here directly: thresholds are clamp / source data, not the architectural carrier.

Tier 3 — scope ThreatSource. A new scope rooted at Province that walks to the underlying Territory, Site, or Route causing the threat. Fields committed in this ADR: Kind: int (typed enum to be authored in Wave 5a; values include taint cluster, gate scarring, hostile faction encroachment, ward-line breach), Severity: channel (read via the 6-phase pipeline), DiscoveryState: int (Hidden / Suspected / Confirmed / Reported), AnchorTerritoryId, AnchorSiteId, and AnchorRouteId. ThreatSource is the first scope row in the emergence ladder: it represents an identified, queryable cause-of-pressure that the player or the dynamic quest pressure generator can act on.

Tier 4 — scope FrontCandidate. A new scope rooted at Crown (or at the Realm scope committed in ./ad-0009-realm-scope-and-polity-rank-orthogonality.md) that aggregates one-or-more ThreatSources into a candidate Front. Fields committed in this ADR: ProposedHqTerritoryId, CoveredProvinceIds: ScopedList<FrontCandidateProvinceRef>, AggregatedSeverity: channel, ProposedCampaignGoals: ScopedList<FrontCandidateObjective>, and DismissedByCrown: int. The FrontCandidate is the layer the Crown player sees on the Crown-scale strategic map BEFORE the Front itself exists: a recommended Front the player can elevate, dismiss, or ignore.

Tier 5 — Front declaration by Crown decision. The Crown player elevates a FrontCandidate to a Front by explicit decision (a queued Crown-scale Activity that calls SetStatus(Forming) on the existing Front scope and binds the elevated FrontCandidate's ThreatSources, ProposedHqTerritoryId, and ProposedCampaignGoals into the new Front). Autonomous Crown automation that elevates FrontCandidates without player decision is explicitly deferred — the player remains the Crown until a later wave commits a Crown-AI policy.

Tier 6 — Events at transitions. Three on_action labels fire at the three state transitions: ThreatSourceMaterialized(threatSourceId) when a new ThreatSource scope row is created, FrontCandidateRaised( candidateId) when one or more ThreatSources aggregate into a new FrontCandidate, and FrontDeclared(frontId, candidateId) when the Crown player elevates a FrontCandidate to a Front.Forming row. These on_action labels are metadata for the existing SECS event-subscription machinery (per .claude/rules/behavior-vocabulary.md and the on_action allowlist in secs-concepts.md); they are not a new behavior keyword.

Alternatives Considered

Per-tick event family with a PressureSignal aggregator system. Rejected because the aggregator must hold a magic threshold constant to decide when to fire — and "Never tune parameters as a substitute for fixing architecture" forbids that pattern. The aggregator also produces no queryable identity: there is no ThreatSource the player can read, no FrontCandidate the Crown-scale map can show, and no place for the dynamic quest pressure generator to attach a pressure mission. The AAA precedent set is universal in rejecting transient-event emergence: CK3 / EU5 / Stellaris / RimWorld / Total War all carry persistent named state for the emergence layer, not transient events.

Modifier-bound emergence (e.g., Modifier(PressureCluster) attached to a province when pressure rises). Rejected because Modifiers are reusable effect bundles per secs-concepts.md, not named persistent entities. A Modifier has no Kind, no AnchorTerritoryId, no ProposedCampaignGoals field — those would force the Modifier mechanism into a parameter-tuning surface, the same anti-pattern the tenets call out. Modifiers may still attach to a ThreatSource or FrontCandidate to express ward-failure decay or pressure spread, but they are not the carrier of emergence identity.

Tenets Applied

  • "Never tune parameters as a substitute for fixing architecture." Magic-threshold aggregators are explicitly rejected; aggregation flows through the committed 6-phase channel pipeline so thresholds live as clamp values and source data rather than as architectural-decision constants.
  • "Research before designing." The AAA precedent for the emergence layer (CK3 Casus Belli; CK3 Struggle / Mongol Invasion; EU4 / EU5 Disasters and Situations; Stellaris Galactic Crises Prethoryn / Contingency / Unbidden; RimWorld threat curve; Total War campaign-map agent emergence) was consulted before committing the six-tier shape.
  • "Future-proofed solutions only." Six tiers (Territory inputs, Province aggregation, ThreatSource, FrontCandidate, Front declaration, transition events) leave room for later autonomous Crown-AI policies, additional ThreatSource kinds, and additional derived Province-level pressure channels without reshape.
  • "No backwards compatibility." Neither the per-tick aggregator pattern nor the Modifier-bound emergence pattern is preserved.

Runtime Backing Status

Status at this ADR's acceptance time: contract-only.

  • Host/runtime owner: legacy/v1/examples/valenar/Host/Data/ThreatSourceData.cs, legacy/v1/examples/valenar/Host/Data/FrontCandidateData.cs, and the existing legacy/v1/examples/valenar/Host/Data/FrontData.cs remain the host-data anchors; the new files are authored in the Wave 5a runtime-backing wave. The existing legacy/v1/examples/valenar/Host/Systems/ tree gains a PressureAggregation host-side system only if the 6-phase channel pipeline alone cannot carry the aggregation; default intent is to carry aggregation through declared channels and no per-tick host system.
  • Generated/.secs owner: legacy/v1/examples/valenar/Content/territories/channels.secs (new Territory-level input channels), legacy/v1/examples/valenar/Content/provinces/channels.secs (new ProvinceThreatPressure, ProvinceWardIntegrity, ProvinceRouteSafety contributory channels), legacy/v1/examples/valenar/Content/threat/scopes.secs (new scope ThreatSource), legacy/v1/examples/valenar/Content/fronts/scopes.secs (new scope FrontCandidate alongside the existing scope Front). Matching Generated stand-in trees authored in the same Wave 5a runtime-backing wave.
  • Read-model/UI owner: the Crown-scale strategic map view; the Valenar React client gains a FrontCandidate roster surface alongside the existing Front roster. Authored in the runtime-backing wave's UI follow-up.
  • Tests: deferred to the Wave 5a runtime-backing wave.
  • Known gaps: the exact ThreatSource.Kind enum value list; the Severity channel's exact 6-phase composition; the Crown-scale Activity that performs Front.SetStatus(Forming) from a FrontCandidate; the FrontCandidate-to-Front binding rule for ThreatSources, ProposedHqTerritoryId, and ProposedCampaignGoals.
  • Illegal fallback behavior: no per-tick aggregator system with a hardcoded threshold may stand in for the 6-phase channel pipeline. No Modifier-as-emergence-carrier may stand in for scope ThreatSource or scope FrontCandidate. No Front.Status value may be set to Forming outside the Crown player's explicit Front declaration Activity until a later wave commits autonomous Crown-AI emergence.
  • Next closure wave: Wave 5a runtime-backing wave authors the channels, the scope rows, the host data classes, the Crown-scale declaration Activity, and the matching Generated stand-ins. The urgency-signal binding wave is the earliest consumer of ProvinceThreatPressure.

Glossary Propagation

The following terms MUST land in gd-glossary.md, gd-canon.md, and README.md:

  • ThreatSource (scope primitive).
  • FrontCandidate (scope primitive).
  • PressureCluster (informal design term used to describe a ThreatSource cluster that has aggregated into a FrontCandidate; not a committed runtime entity by itself).
  • ProvinceThreatPressure, ProvinceWardIntegrity, ProvinceRouteSafety (Province-level contributory channel names).
  • ThreatSourceMaterialized, FrontCandidateRaised, FrontDeclared (on_action transition labels).

Consequences

For ../../systems/gd-fronts.md, the existing FrontStatus.Forming transition is now bound to the Crown player's explicit Front declaration Activity against a FrontCandidate. The Front doc may be extended in a later wave to reference FrontCandidate as the prior-to-Forming state; this ADR does not edit that doc.

For [../../systems/gd-corruption-reclamation.md] and the pressure-corruption cross-act lane in ./ad-0006-dynamic-quest-pressure-and-cross-act-lane-decisions.md, the lane gains a typed entity (ThreatSource) to bind missions and urgency signals against. The dynamic quest pressure generator may emit pressure missions tied to a specific ThreatSource id or to a specific FrontCandidate id.

For ../../systems/gd-realm-ranks-and-polity.md and the Crown rank, the Crown player's authority over the Front roster is extended to authority over the FrontCandidate roster: the Crown chooses which candidates to elevate, dismiss, or defer.

For future verifier checks, any doc that introduces a per-tick aggregator system with a hardcoded pressure threshold as the carrier of emergence is a tenet FAIL. Any doc that introduces an emergence shape outside the six committed tiers above must supersede this ADR.

References