Skip to main content

Generated/ audit — engine-misuse findings

Historical note (2026-04-25): this audit predates the FNV-1a-64 ulong migration; examples mentioning uint identifier shapes are historical unless repeated in the current lowering docs.

Resolution note (2026-04-26): the scope-walk findings below are retained as audit history. Current design/code uses ScopeDeclaration.ParentScopeIds, declares location walks_to Settlement, and rejects undeclared walks with SECS0111; generated CanBuild queries no longer use permissive null fallbacks.

Resolution note (2026-04-26): the Building-template cross-scope channel bugs excluded from this audit were also resolved in current Valenar source/generated output. Settlement effects now attach named modifiers to settlement; the old Storehouse Food rows were removed. The generic storage-capacity model is now documented as a separate capacity channel (FoodCapacity, GoldCapacity, etc.); Valenar now implements concrete food capacity content through settlement.FoodCapacity, Storehouse capacity modifiers, and a production clamp.

Resolution note (2026-04-26): the lifecycle-pairing finding below is retained as audit history. Current runtime semantics use modifier owner/target cleanup: TemplateActivator.Destroy removes channel sources owned by the entity, modifier bindings owned by the entity, and modifier bindings targeted at the entity. Permanent template-owned modifiers therefore no longer require a hand-authored OnDestroyed method solely to avoid leaks.

Date: 2026-04-24. Read-only audit. No source edits made.

Scope

Audited: every .cs file under examples/valenar/Generated/ (69 files, 3146 LOC, bin/obj stripped). Excluded from this pass at the time: the cross-scope-channel bugs that were then catalogued in docs/design/02-templates.md. Those Building-template rows have since been resolved; this file keeps the original scope of the 2026-04-24 audit as history. Audit target: engine-API misuse beyond Model (a) cross-scope — lifecycle pairing, command-flush convention, scope-walk correctness, reflection, non-determinism, host leaks, pattern divergence, unused APIs.

Canonical reference files chosen for divergence analysis:

  • Buildings: Farm.cs
  • Systems: TaxCollectionSystem.cs
  • Events: DemonRaidEvent.cs
  • Features: Ruins.cs
  • Bare: BareProvince.cs
  • Triggers: Trigger_FoodLt0.cs
  • Formulas: Formula_FarmFood.cs

1. API call frequency

CommandBufferExtensions (Abstractions)

APICall sitesSubdirectories
RegisterChannelSource(owner, target, channelId, int)24Templates/Buildings (18), Templates/Features (4), Templates/Bare (1)
RegisterDynamicChannelSource(owner, target, channelId, formulaId)6Templates/Buildings (6)
RegisterChannelSourceFloat0— (unused)
RegisterChannelSourceBool0— (unused)
AddModifier17Events (7), Templates/Buildings (6), Templates/Features (5 adds incl. Dungeon's dual-stage), OnActions (0)
RemoveModifier0 call sites (one TODO comment in Dungeon.cs:35)
RemoveAllBindings0— (unused)
RemoveAllChannelSources0— (unused)
IncrementScopeField12Systems (7), Events (6), OnActions (1), Actions (2)
RemoveModifiersByTag0— (unused)
RemoveModifiersByTagFromOwner0— (unused)

TickContext / ISecsHostReads / ChannelResolver

APICall sitesSubdirectories
ChannelResolver.ResolveIntFast15Systems (8), Events (5), Actions (2)
ChannelResolver.ResolveInt (non-fast variant)0— (unused by Generated)
ChannelResolver.ResolveFloatFast / ResolveFloat / ResolveBool / ResolveFloatFast0— (unused)
host.ResolveStatInt7Triggers (4), Formulas (3)
host.ResolveStatFloat / ResolveStatBool0— (unused)
host.WalkScope4Formulas (3), Templates/Buildings (1 — Farm.CanBuild)
host.ReadInt5Formulas (3), Systems (1 — ResourceProductionSystem), Templates/Buildings (1 — Farm.CanBuild)
host.ReadFloat / ReadBool / ReadEntity0— (unused)
host.CallIntMethod10Templates/Buildings (10 — the BuildingCount query)
host.CallBoolMethod0— (unused)
host.GetChildren0— (unused)
ctx.Commands (buffer access)35All non-template subdirectories
ctx.CommandProcessor.Process22All non-template subdirectories
ctx.InstanceStore.AllByContract2Systems (ResourceProductionSystem twice — once for settlements, once for locations)
ctx.InstanceStore.AllByContractForTick2Systems (TaxCollectionSystem, PopulationGrowthSystem)
ctx.InstanceStore.ByScope / ByScopeAndContract / CountByContract / TryGetInstance / TryGetArchetype0— (unused)
ctx.Events.FireOnAction1Events (DemonRaidEvent:55)
ctx.SaveScope1Events (DemonRaidEvent:54)
ctx.GetSavedScope3Events (CelebrationBonusEvent, RaidResolvedVictoryEvent, RaidResolvedDefeatEvent)
ctx.TryGetSavedScope / ctx.ClearSavedScopes0— (unused, engine auto-clears)
ctx.AddModifierWhen1Events (CelebrationBonusEvent:54)
ctx.Spawn(scope) / (scope, template) / (scope, template, parent)5Systems (MapGenerationSystem)
ctx.PickRandomEntity1Systems (MapGenerationSystem:91)
ctx.PickRandomTemplateForScope1Systems (MapGenerationSystem:96)
ctx.RandomInt3Systems (MapGenerationSystem)
ctx.RandomFloat0— (unused)
ctx.TickNumber4Systems (2), Events (DemonRaidEvent:30)
ctx.Host1Systems (ResourceProductionSystem:55ctx.Host.ReadInt)
ctx.DirtySet / ctx.BindingStore / ctx.TickDispatcher / ctx.BindingUpdater / ctx.Registry / ctx.IdGenerator / ctx.HostWrites / ctx.Activator0— (unused from Generated)

Outlier APIs (called from exactly one file):

  • ctx.Events.FireOnAction — only DemonRaidEvent.cs
  • ctx.SaveScope — only DemonRaidEvent.cs
  • ctx.AddModifierWhen — only CelebrationBonusEvent.cs
  • ctx.PickRandomEntity, ctx.PickRandomTemplateForScope, ctx.Spawn*, ctx.RandomInt — only MapGenerationSystem.cs
  • ctx.Host.ReadInt (direct host read from a system) — only ResourceProductionSystem.cs:55
  • host.WalkScope from template-side (non-formula) — only Farm.cs:32

Outlier categories (called from exactly one subdirectory):

  • Spawn*/RandomInt/PickRandom* — Systems only
  • FireOnAction/SaveScope/GetSavedScope/AddModifierWhen — Events only
  • CallIntMethod(H.Location, H.Scope_Location_BuildingCount_TemplateId_Int, …) — Templates/Buildings only (uniform 10 sites)
  • ResolveStatInt on the host object (not ctx.ChannelResolver) — only Triggers and Formulas

2. Lifecycle pairing (historical, resolved 2026-04-26)

Method-set inventory for every template that installs modifiers via a lifecycle hook:

TemplateScopeHook that addsModifierHas remove hook?Duration
BarracksLocationOnBuilt:29-32H.FortifiedNo (no OnDestroyed method declared)-1 (permanent)
TrainingGroundsLocationOnBuilt:43-46H.FortifiedNo-1
TrainingGroundsTier2LocationOnBuilt:36-39H.FortifiedNo-1
TrainingGroundsTier3LocationOnBuilt:36-39H.FortifiedNo-1
TavernLocationOnBuilt:42-45H.FestivalSpiritNo-1
GranaryLocationOnBuilt:33-36H.BountifulHarvestNo-1
RuinsFeatureOnDiscovered:26-29H.RuinsExploredNo-1
AncientShrineFeatureOnDiscovered:26-29H.ShrineBlessedNo-1
AbandonedFortressFeatureOnDiscovered:26-29H.FortifiableSiteNo-1
OreVeinFeatureOnDiscovered:26-29H.RichOreNo-1
MysticGroveFeatureOnDiscovered:26-29H.GroveAttunementNo-1
DungeonFeatureOnSpawned:28-31H.DungeonAuraOfDreadOnCleared:33-39 stubbed — adds DungeonClearedReward but TODO-comment notes removal is done host-side-1

Historical observations:

  • At audit time, no explicit destruction hook was implemented by any template. Current design no longer treats that as a leak by itself: template destruction performs owner/target cleanup after the contract's deactivation lifecycle binding.
  • OnDestroyed is declared on the BuildingContract (Declarations.cs:326) and bound as that contract's deactivation method, but no building template implements it. The engine resolves ContractLifecycleIds.Deactivation -> H.Contract_Building_OnDestroyed_NoArgs_Void, finds no method entry on current building templates, and skips the custom hook while still performing owner/target cleanup.
  • OnCleared is declared on the FeatureContract (Declarations.cs:337). Only Dungeon implements it, and its implementation is the documented-as-wrong stub (adds a second modifier rather than removing the first).
  • Granary.cs:23-25 contains the comment "Granary works via its OnBuilt modifier" — pattern consistent with design note in 02-templates.md:1124 that Granary is the Model-(a)-aligned exemplar; it still has no OnDestroyed pair.

Resolution: modifier leaks on permanent-duration template-owned adds are closed by the owner/target lifecycle contract. The spec now declares that owner is the lifetime source and that template destruction removes all bindings owned by, or targeted at, the destroyed entity. remove_modifier remains useful for explicit early/custom removal, not ordinary lifetime cleanup.

Cross-reference to contract declarations (Declarations.cs:324-341):

ContractRoot scopeDeclared method idsMethods actually implemented by templates
BuildingContractLocationOnBuilt, OnDestroyed, OnDayTickOnly OnBuilt (6 of 29 templates). OnDestroyed never implemented. OnDayTick never implemented.
SettlementContractSettlementOnCreated, OnDayTick, OnSeasonTickNone of the three is implemented on any template (there is no Settlement template in Generated; the MapGenerationSystem spawns bare map entities only).
BuildOrderContractLocation(empty)
RegionContract / AreaContract / ProvinceContractRegion/Area/ProvinceOnLoadedNone of the four Bare templates implements OnLoaded.
LocationContractLocationOnLoaded, OnClaimed, OnDesignatedBareLocation.cs implements none.
FeatureContractFeatureOnSpawned, OnDiscovered, OnInteract, OnClearedDungeon implements OnSpawned + OnCleared; others implement OnDiscovered only. OnInteract never implemented.
CharacterContractCharacterOnSpawnedMainCharacter / BareCharacter implement none; their Activate bodies are empty.

Declared-but-not-implemented methods (OnDestroyed, OnDayTick, OnSeasonTick, OnCreated, OnLoaded, OnClaimed, OnDesignated, OnInteract) are a different bug class from "unpaired add": they are contract surface declared in Declarations.cs but with zero implementers. Either the host never invokes these methods, or the host invokes them and every target silently no-ops.

3. Scope-walk correctness (historical, resolved 2026-04-26)

Historical single-parent chain from Generated/Declarations.cs:8-25:

Region (parent 0)
Area (parent Region)
Province (parent Area)
Location (parent Province)
Feature (parent Location)
Building (parent Location)
BuildOrder (parent Location)
Settlement (parent 0, root-level)
Character (parent 0, root-level)

At the time of this audit, Settlement was not reachable from Location via the single-parent chain — it was a root-level scope, parallel to the map hierarchy. The host's ownership link was stored as Location.OwnerId (a scope field, Declarations.cs:60), not as a declared scope edge.

Every host.WalkScope call in Generated:

Sitesource entity's scopeTarget scopeReachable via declared ParentScopeId?
Formula_FarmFood.cs:17Building/Location (owner is the Farm's building scope, root scope target = Location)H.LocationYes — identity/self or Building → Location
Formula_IrrigatedFarmFood.cs:17Building/LocationH.LocationYes
Formula_TerracedFarmFood.cs:17Building/LocationH.LocationYes
Farm.cs:32Location (root target passed into the CanBuild query)H.SettlementNo — Settlement is a root scope with parent 0, parallel to the map hierarchy. Location → Province → Area → Region → 0 never reaches Settlement.

Farm.cs:32 is the one cross-undeclared-edge walk. The code handles a null return with a permissive fallback (if (settlement.IsNull) return true;) so the Stage-gate check silently degrades to "always allowed" in production — the MinStage = 1 query is effectively dead code unless the host has extra logic overlaying WalkScope to resolve Location→Settlement through OwnerId.

Either:

  • The host implements WalkScope in a richer way than the declared hierarchy (looking up Location.OwnerId and returning the owning Settlement), in which case the declaration is the source of drift; or
  • The walk really does fail, and Farm.CanBuild is degenerate.

Both are compiler-must-catch conditions: the spec should either make Settlement a declared parent of Location (it is not, per Declarations.cs:20), or forbid WalkScope(Location, Settlement) at the lowering stage.

Additional walk-chain notes:

  • The three Farm-family formulas (Formula_FarmFood, Formula_IrrigatedFarmFood, Formula_TerracedFarmFood) all walk owner → H.Location. owner is the Farm building entity; per 02-templates.md, building templates have RootScopeId = H.Location, so the "walk" is semantically identity (or at most a lookup through Building → Location per Declarations.cs:12). Both arms are declared; the walks are sound.
  • ResourceProductionSystem.cs:55 takes the opposite path: rather than walking from a Location up to Settlement, it reads Location.OwnerId (a scope field, not a scope parent) and compares against the settlement's entity value. This is the documented Valenar idiom for settlement↔location ownership (see Declarations.cs:60). It works; the question the audit raises is why Farm.CanBuild did not use the same idiom.
  • There is no other instance of WalkScope from template-side code. The generated building CanBuild queries now read location building counts through LocationScopeQueries.BuildingCount(...), the typed facade over ISecsHostReads.CallScopeQuery<ulong, int>.

4. Command-flush convention

Observed pattern — every non-template site follows the sequence:

ctx.Commands.<op>(…);
ctx.CommandProcessor.Process(ctx.Commands);
ctx.Commands.Clear();

Templates follow a different pattern: Activate and Methods[…] receive CommandBuffer cmds as a parameter, call cmds.RegisterChannelSource / cmds.AddModifier / etc., and never call Process or Clear themselves (see Farm.cs:26, Ruins.cs:23,28, etc.). The engine's TemplateActivator flushes that buffer (src/SECS.Engine/TemplateActivator.cs). This is correct and by design.

Divergences from the per-op-flush pattern in non-template code:

SitePatternNotes
ResourceProductionSystem.cs:66-89Per-resource-kind flush inside inner foreach-over-settlements loop (not per-inner-loc-iteration). Correctly batches location output sums into 1-4 IncrementScopeField calls per settlement, each flushed independently.OK — 4 separate-direction writes, flushed together per resource.
PlagueSpreadsEvent.cs:44-47Two commands queued (AddModifier + IncrementScopeField), then one Process/Clear.Batched (2 ops / 1 flush). Differs from the 1-op-per-flush elsewhere.
FamineStartsEvent.cs:41-48Two separate Process/Clear pairs for two ops that could have been batched into one flush.Differs from PlagueSpreadsEvent.cs — same file-shape, different granularity.
DemonRaidEvent.cs:44-48Two commands (AddModifier + IncrementScopeField), one flush.Same shape as PlagueSpreadsEvent.
CelebrationBonusEvent.cs:34-64Four distinct Process/Clear pairs, one per logical effect.Most verbose shape.
RecruitGarrisonAction.cs:48-51Two commands, one flush.Same as PlagueSpreads.
TaxCollectionSystem.cs:34-36One command, one flush — inside the per-entity foreach.Canonical shape.
PopulationGrowthSystem.cs:56-58One command, one flush per entity.Canonical.
OnActionDeclarations.cs:37-39One command, one flush.Canonical.

No Generated file defers flushing to the end of the iteration. Every site flushes during the iteration, either per-op or per-logical-effect. The net effect: every iteration of every system/event does its own Process call — 1-4 times per entity. The convention is "flush while iterating"; the question of whether to batch 2 related commands or flush separately is inconsistent across files. The Events directory is the divergent hotspot.

There is a corner case worth flagging: TaxCollectionSystem.cs:27-37 and PopulationGrowthSystem.cs:40-59 call Process/Clear inside the AllByContractForTick enumeration. AllByContractForTick returns a DistributedEntityEnumerable ref struct over the instance store's entity list; CommandProcessor.Process may (in theory) mutate the instance store via modifier bindings or through the host. The current event/system corpus never destroys entities mid-iteration, so the bug is latent. But the contract "flush inside foreach over AllByContractForTick" is one compile-time check away from breaking whenever a command that affects iteration order is added.

5. Reflection / dynamic dispatch

Zero hits. No Type.GetType, Activator.CreateInstance, System.Reflection, typeof(...).GetMethod, or dynamic uses anywhere under Generated/ (excluding auto-generated obj/*.AssemblyInfo.cs, which is compiler-emitted boilerplate, not SECS lowering output).

ctx.Activator in TickContext is not reflection; it is the engine's TemplateActivator singleton, and it is never accessed from Generated code (zero references).

6. Non-determinism

Zero hits. No DateTime.*, Guid.NewGuid, new Random(), Environment.*, Thread.*, or Task.* references anywhere in Generated/. Every random draw goes through ctx.Random / ctx.RandomInt / ctx.PickRandomEntity / ctx.PickRandomTemplateForScope.

Note: EventDispatcher itself (in SECS.Engine) holds a private readonly Random random = new(); field (src/SECS.Engine/Events/EventDispatcher.cs:19) that is used for pulse chance rolls at line 65. That is an engine-side non-determinism, not a Generated-side one, but it is the one path where event fire-or-not is seeded off a separate RNG from ctx.Random. Outside the audit scope but worth noting; the Generated code cannot influence it.

7. Host-side leaks

Zero hits. No Valenar.Host, GameWorld, HostBridge, SettlementData, LocationData, ProvinceData, AreaData, RegionData, GameState, or GameRuntime symbols referenced from anywhere under Generated/. Every interaction with host data goes through ISecsHostReads / ISecsHostWrites interfaces, or through the CommandBufferCommandProcessor → host pipeline.

The using imports in every Generated file are limited to SECS.Abstractions.* / SECS.Engine.* / Valenar.Generated.*; see GlobalUsings.cs.

8. Pattern divergence

Templates/Buildings (canonical: Farm.cs)

Canonical shape: public const ulong Id, public static readonly TemplateEntry Entry with TemplateId / Name / ContractId = H.BuildingContract / RootScopeId = H.Location / Activate / query dictionary / GetFieldInt, then Activate/CanBuild/GetField methods in that order.

Full per-file inventory (all 29 building templates):

FileLinesHas CanBuild queryHas GetFieldIntHas MethodsComment
Farm.cs43yes (stage gate)yesnocanonical
GreatHall.cs31yes (BuildingCount)yes (returns 0)noGetField => 0 — shape differs
House.cs33null (explicit)yesnoonly explicit-null in corpus
Forge.cs38yes (BuildingCount)yesnosame shape as GreatHall
Watchtower.cs39yes (BuildingCount)yesnosame shape as GreatHall
Walls.cs38yes (BuildingCount)yesnosame shape as GreatHall
Storehouse.cs37yes (BuildingCount)yesnosame shape as GreatHall
TrainingGrounds.cs47yes (BuildingCount)yesyes (OnBuilt)installs Fortified
Temple.cs37yes (BuildingCount)yesnosame shape as GreatHall
Barracks.cs48yes (BuildingCount)yesyes (OnBuilt)installs Fortified, brace-brace init
Tavern.cs46yes (BuildingCount)yesyes (OnBuilt)installs FestivalSpirit
IrrigatedFarm.cs23nononono stage gate (unlike Farm)
TerracedFarm.cs23nononono stage gate
Housing.cs29yes (BuildingCount < 2)nonocosts undeclared
Granary.cs37yes (BuildingCount)noyes (OnBuilt)only Methods-bearing building without flat channels
Workshop.cs21nononoshortest building
AdvancedWorkshop.cs21nononoshortest building
WatchtowerTier2/3.cs30noyesnono tier gate
ForgeTier2/3.cs31noyesnono tier gate
GreatHallTier2/3.cs30noyesnono tier gate
WallsTier2/3.cs31noyesnono tier gate
StorehouseTier2/3.cs30noyesnono tier gate
TrainingGroundsTier2.cs40noyesyes (OnBuilt)installs Fortified
TrainingGroundsTier3.cs40noyesyes (OnBuilt)installs Fortified
TempleTier2/3.cs30noyesnono tier gate

Key observations:

  • House.cs:15 was the only template in the corpus that explicitly set the legacy activation-query delegate to null rather than omitting the init. Trivial stylistic divergence in the hand-written stand-in.
  • Housing.cs, Workshop.cs, AdvancedWorkshop.cs have no GetFieldInt slot at all. Their costs are undeclared — the engine returns 0 by default via TemplateEntry.GetFieldInt is null. Either costs are irrelevant for these (the host never reads them) or the host defaults them elsewhere. Divergence from every other building that declares cost via GetField.
  • GreatHall.cs:30 has GetField => 0; // Free — already built — a unique switch-free implementation that returns 0 regardless of field id. Every other GetField is a switch expression keyed on fieldId.
  • All 12 tier-variants never declare a CanBuild query. The intent is presumably "a tier transition bypasses the BuildingCount == 0 query since there is already a prior-tier instance on the tile," but this intent is undocumented in code.
  • Historical dictionary-init syntax finding: at audit time, building and feature templates used two different legacy method-registration shapes. Current generated output uses Dictionary<ulong, ITemplateCommand> with TemplateCommand.NoArgs(...); the future compiler should keep one canonical initializer form.
  • Historical StorehouseTier2.cs:20, StorehouseTier3.cs:20 finding: at audit time these templates registered H.Food (settlement-scope channel) as a flat int on a Location-scope Building template. Current Valenar generated output no longer does this; Storehouse tiers attach FoodCapacity modifiers to the owning settlement.
  • Granary.cs is the only building with populated Methods + non-empty OnBuilt + zero flat channel contributions in Activate. 02-templates.md:1124 calls it the Model-(a)-aligned exemplar. Shape is legal but unique.

Templates/Features (canonical: Ruins.cs)

Canonical shape: public const ulong Id, TemplateEntry with TemplateId / Name / ContractId = H.FeatureContract / RootScopeId = H.Feature / Activate / Methods = {[H.Contract_Feature_OnDiscovered_NoArgs_Void] = OnDiscovered}.

FileDifference from Ruins.cs
Dungeon.cs:16-19Uses [H.Contract_Feature_OnSpawned_NoArgs_Void] and [H.Contract_Feature_OnCleared_NoArgs_Void] — the only feature template with two method entries. OnDiscovered is not in its Methods dictionary (Dungeon uses OnSpawned as its activation-adjacent hook instead).
AncientShrine / AbandonedFortress / OreVein / MysticGrove / RuinsIdentical shape — all use OnDiscovered only.

Templates/Bare (canonical: BareProvince.cs)

Canonical shape: no-op Activate that returns without registering anything. TemplateEntry has TemplateId / Name / ContractId / RootScopeId / Activate.

FileDifference from BareProvince.cs
BareLocation.cs:25Registers H.Slots = 4 — the only Bare template that registers a channel source. Slots is a location-scope channel, so this is consistent with Model (a). The surprise is the contrast with BareRegion/BareArea/BareProvince/BareCharacter, all of which are no-ops.
BareCharacter.cs:18-22Activate signature matches but body is only a comment. No divergence.
All Bare templatesNone declare Deactivate, none declare Methods. Consistent.

Templates/Characters

Only MainCharacter.cs. Activate is an empty-body comment-only method. Name = "Hero" — the memory feedback_valenar_genre explicitly forbids calling MC "Hero". This is content, not engine-misuse, so reporting it as a note: MainCharacter.cs:15 carries a name that violates the narrative-setting rule but does not break the engine API contract.

Systems (canonical: TaxCollectionSystem.cs)

Canonical shape: sealed class : ITickSystem, Registration field, DistributedFrequency const, Frequency => 1 and Phase => H.PhaseX, Execute(ctx) iterating AllByContractForTick(contract, ctx.TickNumber, DistributedFrequency).

FileDifference from TaxCollectionSystem.cs
PopulationGrowthSystem.csIdentical shape.
ResourceProductionSystem.cs:34-42No Frequency property declared — interface default of 1 is used. No DistributedFrequency gating. Iterates AllByContract (full scan), not AllByContractForTick. Every tick, the system walks every location twice (outer settlement loop × inner location loop). This is O(settlements × locations) per tick with no distribution. For the current Valenar scale (1 settlement, hundreds of locations) this is OK; at map-generation scale (3 regions × 3 areas × 3 provinces × ~15 locations = ~135 locations) the per-tick cost is 135 WalkScope-free ReadInt + 4×135 ResolveIntFast calls.
ResourceProductionSystem.cs:55Only site in the corpus where a system uses ctx.Host.ReadInt directly rather than going through the scope hierarchy via host.WalkScope. Tracking OwnerId through scope fields (not scope parents) is the intended pattern per the Location.OwnerId field declaration, but direct ctx.Host.ReadInt bypasses the usual host.* parameter wiring that formulas/triggers use.
MapGenerationSystem.csFrequency => SystemFrequency.Once. Only one-shot system. Uses Spawn / PickRandomEntity / PickRandomTemplateForScope / RandomInt — APIs called from nowhere else. Its Execute body is 54 lines, five times the size of the other systems.

Events (canonical: DemonRaidEvent.cs)

Canonical shape after the 2026-04-30 event/on-action cleanup: sealed class : SecsEvent, Registration with TriggerType, singleton Instance, optional Frequency / Chance, and ordinary overrides such as Condition(EventContext), Execute(EventContext), and BuildOptions(EventOptions, EventContext).

FileDifference from DemonRaidEvent.cs
PlagueSpreadsEvent.csOnly event with player choices. Uses BuildOptions(EventOptions, EventContext) and option handler methods rather than Execute.
CelebrationBonusEvent.csNo Condition override — relies on the SecsEvent base default (Condition => true). Also no Frequency / Chance overrides (defaults: 1 / 100).
RaidResolvedVictoryEvent.cs, RaidResolvedDefeatEvent.cs, FamineStartsEvent.csTriggerType = EventTriggerType.OnAction; OnActionId field populated. No Frequency / Chance (not meaningful for OnAction events). Weight field populated for the two RaidResolved events. FamineStartsEvent.cs:29 sets Priority = 0 — the only event that explicitly sets Priority.

No event declares its own state fields today; every Valenar event is stateless even though SecsEvent leaves class state available for future use.

Formulas (canonical: Formula_FarmFood.cs)

Canonical shape: public static class Formula_X, single public static int Evaluate(target, owner, captured1, host) method.

Signatures are uniform: (EntityHandle target, EntityHandle owner, EntityHandle captured1, ISecsHostReads host). The captured1 parameter is never used by any formula in the corpus.

FileDifference from Formula_FarmFood.cs
Formula_WatchtowerDefense.cs, Formula_FortifiedDefense.cs, Formula_HousePopCap.csDo not call WalkScope — they operate on target directly (the settlement entity whose channel is being resolved). Semantically these are settlement-side modifier formulas.
Formula_FarmFood.cs, Formula_IrrigatedFarmFood.cs, Formula_TerracedFarmFood.csWalk owner → Location and read Fertility. Semantically these are building-side channel-source formulas operating on owner's location.

Split is intentional (channel-source formulas use owner; modifier-effect formulas use target). Good pattern.

Triggers (canonical: Trigger_FoodLt0.cs)

Canonical shape: public static class Trigger_X, single public static bool Evaluate(target, captured0, captured1, host).

FileDifference from Trigger_FoodLt0.cs
Trigger_MoraleGte80.csIdentical shape.
Trigger_DefenseGte50.csIdentical shape.
Trigger_MoraleGte60.cs:10-11Only trigger that uses captured0 — "When used with captured root: target = district, captured0 = settlement". The trigger chooses captured0.IsNull ? target : captured0 and resolves Morale on the chosen entity. This is a meaningful divergence: the trigger's semantics depend on how it was captured at binding time, which is information the trigger has to guess because there is no binding-mode field. Callers that forget to pass a captured entity silently get the wrong behavior (resolve on district, not settlement).

9. Unused engine APIs

APIs exposed by SECS.Abstractions / SECS.Engine that have zero call sites in Generated/:

CommandBufferExtensions:

  • RegisterChannelSourceFloat (float channels)
  • RegisterChannelSourceBool (bool channels)
  • RemoveModifier (cited as TODO in Dungeon.cs:35)
  • RemoveAllBindings
  • RemoveAllChannelSources
  • RemoveModifiersByTag
  • RemoveModifiersByTagFromOwner

ISecsHostReads:

  • ReadFloat
  • ReadBool
  • ReadEntity
  • GetChildren
  • CallBoolMethod
  • ResolveStatFloat
  • ResolveStatBool

ISecsHostWrites: all three (WriteInt, WriteFloat, WriteBool) — intentional; writes go through commands.

TickContext:

  • RandomFloat
  • ClearSavedScopes
  • TryGetSavedScope
  • DirtySet, BindingStore, TickDispatcher, BindingUpdater, Registry, IdGenerator, HostWrites, Activator (every auxiliary field except Commands/CommandProcessor/InstanceStore/Host/ChannelResolver/Events/Random/TickNumber)

InstanceStore:

  • Add (only called by engine)
  • Remove
  • ByScope
  • ByScopeAndContract
  • TryGetInstance
  • CountByContract
  • TryGetArchetype

ChannelResolver:

  • ResolveInt (the rich variant that returns StatResult with decomposition)
  • ResolveFloat
  • ResolveFloatFast
  • ResolveBool

ModifierBindingStore: all public methods (every caller is engine-internal).

TemplateActivator: Activate, Create, Destroy, DestroyWithChildren, Upgrade, CallMethod — zero Generated calls.

SecsRegistry: TryGetChannelDeclaration, GetModifiersByTag, DescribeChannelSource, AllTemplateIdsByContract, FindTemplateIdByName, AllChannelNames, GetTagName, FindContractByRootScope, GetDeclaredDependencyGraph, GetFormulaDependencies, GetFormulaContributions — zero Generated calls (these are host-side lookup APIs).

Candidates for "compiler must implement later":

  • RemoveModifier — required for source-authored early/custom removal. RemoveAllBindings is now part of engine-driven template destruction rather than a source construct.
  • TryGetSavedScope — the GetSavedScope + IsNull check idiom in CelebrationBonusEvent.cs:41-42 is a hand-rolled TryGet. The future compiler may prefer TryGetSavedScope.

Candidates for deprecation (no call sites + no plausible compiler need):

  • RandomFloat — Generated has no float random draws yet; may be needed when weather/percentage modifiers are added.
  • ReadFloat / WriteFloat / ReadBool / WriteBool — all channel-source declarations are SecsScalarType.Int. Float/bool support is currently an engine surface with zero consumers; either reserve or cull.
  • ReadEntity / GetChildren — no entity-valued scope fields in declarations.

Summary

Top findings by severity:

  1. ResourceProductionSystem.cs has no Frequency declaration and uses AllByContract (not AllByContractForTick). It runs every tick at O(settlements × locations); all three other systems use per-tick distribution. It also carries the corpus's only ctx.Host.ReadInt call from a system (line 55), bypassing the scope walk idiom used by formulas.
  2. Trigger_MoraleGte60.cs:10-11 branches on captured0.IsNull to decide which entity to resolve against. This is a silent mode switch with no compile-time enforcement; callers that forget captured0 get the wrong semantics.
  3. CelebrationBonusEvent.cs omits Condition — relies on SecsEvent base default true. Divergence is minor because the event is intentionally unconditional.
  4. Resolved 2026-04-26: lifecycle pairing leak audit. Permanent template-owned modifiers no longer require hand-authored removal hooks; owner/target cleanup removes owned and targeted bindings during template destruction. Historical details remain in § 2.
  5. Resolved 2026-04-26: Farm.cs:32 walked Location → Settlement, which was not reachable via the old single-parent chain. Current generated declarations use ParentScopeIds, location walks_to Settlement is explicit, and the generated query returns false when the runtime relationship is absent instead of treating null as allowed.

Commentary (under 200 words):

The corpus is cleanly disciplined on three axes — no reflection, no non-determinism, no host-type leaks — which is exactly what the compiler contract needs. The lifecycle and scope-graph findings from the original audit have since been resolved: permanent template-owned modifiers are cleaned up by owner/target destruction, and Farm's location-to-settlement walk is now declared. The remaining weaknesses are mostly around command-flush convention, unused API surface, and generated-output consistency. Roughly half of the exposed engine API surface (float reads/writes, bool reads/writes, tag-based modifier ops, RemoveModifier, ByScope, CountByContract) has zero direct content call sites — either candidates for deprecation or flagged for compiler-phase later. Compiler bring-up will want to canonicalize the Methods init syntax (brace-brace vs indexer-init) and validate all WalkScope(x, Y) calls against declared walks_to edges at emission time.