Generated/ audit — engine-misuse findings
Historical note (2026-04-25): this audit predates the FNV-1a-64
ulongmigration; examples mentioninguintidentifier 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, declareslocation walks_to Settlement, and rejects undeclared walks withSECS0111; generatedCanBuildqueries 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 StorehouseFoodrows 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 throughsettlement.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.Destroyremoves 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-authoredOnDestroyedmethod 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)
| API | Call sites | Subdirectories |
|---|---|---|
RegisterChannelSource(owner, target, channelId, int) | 24 | Templates/Buildings (18), Templates/Features (4), Templates/Bare (1) |
RegisterDynamicChannelSource(owner, target, channelId, formulaId) | 6 | Templates/Buildings (6) |
RegisterChannelSourceFloat | 0 | — (unused) |
RegisterChannelSourceBool | 0 | — (unused) |
AddModifier | 17 | Events (7), Templates/Buildings (6), Templates/Features (5 adds incl. Dungeon's dual-stage), OnActions (0) |
RemoveModifier | 0 call sites (one TODO comment in Dungeon.cs:35) | — |
RemoveAllBindings | 0 | — (unused) |
RemoveAllChannelSources | 0 | — (unused) |
IncrementScopeField | 12 | Systems (7), Events (6), OnActions (1), Actions (2) |
RemoveModifiersByTag | 0 | — (unused) |
RemoveModifiersByTagFromOwner | 0 | — (unused) |
TickContext / ISecsHostReads / ChannelResolver
| API | Call sites | Subdirectories |
|---|---|---|
ChannelResolver.ResolveIntFast | 15 | Systems (8), Events (5), Actions (2) |
ChannelResolver.ResolveInt (non-fast variant) | 0 | — (unused by Generated) |
ChannelResolver.ResolveFloatFast / ResolveFloat / ResolveBool / ResolveFloatFast | 0 | — (unused) |
host.ResolveStatInt | 7 | Triggers (4), Formulas (3) |
host.ResolveStatFloat / ResolveStatBool | 0 | — (unused) |
host.WalkScope | 4 | Formulas (3), Templates/Buildings (1 — Farm.CanBuild) |
host.ReadInt | 5 | Formulas (3), Systems (1 — ResourceProductionSystem), Templates/Buildings (1 — Farm.CanBuild) |
host.ReadFloat / ReadBool / ReadEntity | 0 | — (unused) |
host.CallIntMethod | 10 | Templates/Buildings (10 — the BuildingCount query) |
host.CallBoolMethod | 0 | — (unused) |
host.GetChildren | 0 | — (unused) |
ctx.Commands (buffer access) | 35 | All non-template subdirectories |
ctx.CommandProcessor.Process | 22 | All non-template subdirectories |
ctx.InstanceStore.AllByContract | 2 | Systems (ResourceProductionSystem twice — once for settlements, once for locations) |
ctx.InstanceStore.AllByContractForTick | 2 | Systems (TaxCollectionSystem, PopulationGrowthSystem) |
ctx.InstanceStore.ByScope / ByScopeAndContract / CountByContract / TryGetInstance / TryGetArchetype | 0 | — (unused) |
ctx.Events.FireOnAction | 1 | Events (DemonRaidEvent:55) |
ctx.SaveScope | 1 | Events (DemonRaidEvent:54) |
ctx.GetSavedScope | 3 | Events (CelebrationBonusEvent, RaidResolvedVictoryEvent, RaidResolvedDefeatEvent) |
ctx.TryGetSavedScope / ctx.ClearSavedScopes | 0 | — (unused, engine auto-clears) |
ctx.AddModifierWhen | 1 | Events (CelebrationBonusEvent:54) |
ctx.Spawn(scope) / (scope, template) / (scope, template, parent) | 5 | Systems (MapGenerationSystem) |
ctx.PickRandomEntity | 1 | Systems (MapGenerationSystem:91) |
ctx.PickRandomTemplateForScope | 1 | Systems (MapGenerationSystem:96) |
ctx.RandomInt | 3 | Systems (MapGenerationSystem) |
ctx.RandomFloat | 0 | — (unused) |
ctx.TickNumber | 4 | Systems (2), Events (DemonRaidEvent:30) |
ctx.Host | 1 | Systems (ResourceProductionSystem:55 — ctx.Host.ReadInt) |
ctx.DirtySet / ctx.BindingStore / ctx.TickDispatcher / ctx.BindingUpdater / ctx.Registry / ctx.IdGenerator / ctx.HostWrites / ctx.Activator | 0 | — (unused from Generated) |
Outlier APIs (called from exactly one file):
ctx.Events.FireOnAction— onlyDemonRaidEvent.csctx.SaveScope— onlyDemonRaidEvent.csctx.AddModifierWhen— onlyCelebrationBonusEvent.csctx.PickRandomEntity,ctx.PickRandomTemplateForScope,ctx.Spawn*,ctx.RandomInt— onlyMapGenerationSystem.csctx.Host.ReadInt(direct host read from a system) — onlyResourceProductionSystem.cs:55host.WalkScopefrom template-side (non-formula) — onlyFarm.cs:32
Outlier categories (called from exactly one subdirectory):
Spawn*/RandomInt/PickRandom*— Systems onlyFireOnAction/SaveScope/GetSavedScope/AddModifierWhen— Events onlyCallIntMethod(H.Location, H.Scope_Location_BuildingCount_TemplateId_Int, …)— Templates/Buildings only (uniform 10 sites)ResolveStatInton the host object (notctx.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:
| Template | Scope | Hook that adds | Modifier | Has remove hook? | Duration |
|---|---|---|---|---|---|
| Barracks | Location | OnBuilt:29-32 | H.Fortified | No (no OnDestroyed method declared) | -1 (permanent) |
| TrainingGrounds | Location | OnBuilt:43-46 | H.Fortified | No | -1 |
| TrainingGroundsTier2 | Location | OnBuilt:36-39 | H.Fortified | No | -1 |
| TrainingGroundsTier3 | Location | OnBuilt:36-39 | H.Fortified | No | -1 |
| Tavern | Location | OnBuilt:42-45 | H.FestivalSpirit | No | -1 |
| Granary | Location | OnBuilt:33-36 | H.BountifulHarvest | No | -1 |
| Ruins | Feature | OnDiscovered:26-29 | H.RuinsExplored | No | -1 |
| AncientShrine | Feature | OnDiscovered:26-29 | H.ShrineBlessed | No | -1 |
| AbandonedFortress | Feature | OnDiscovered:26-29 | H.FortifiableSite | No | -1 |
| OreVein | Feature | OnDiscovered:26-29 | H.RichOre | No | -1 |
| MysticGrove | Feature | OnDiscovered:26-29 | H.GroveAttunement | No | -1 |
| Dungeon | Feature | OnSpawned:28-31 | H.DungeonAuraOfDread | OnCleared: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.
OnDestroyedis declared on theBuildingContract(Declarations.cs:326) and bound as that contract's deactivation method, but no building template implements it. The engine resolvesContractLifecycleIds.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.OnClearedis declared on theFeatureContract(Declarations.cs:337). OnlyDungeonimplements it, and its implementation is the documented-as-wrong stub (adds a second modifier rather than removing the first).Granary.cs:23-25contains the comment "Granary works via its OnBuilt modifier" — pattern consistent with design note in02-templates.md:1124that Granary is the Model-(a)-aligned exemplar; it still has noOnDestroyedpair.
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):
| Contract | Root scope | Declared method ids | Methods actually implemented by templates |
|---|---|---|---|
BuildingContract | Location | OnBuilt, OnDestroyed, OnDayTick | Only OnBuilt (6 of 29 templates). OnDestroyed never implemented. OnDayTick never implemented. |
SettlementContract | Settlement | OnCreated, OnDayTick, OnSeasonTick | None of the three is implemented on any template (there is no Settlement template in Generated; the MapGenerationSystem spawns bare map entities only). |
BuildOrderContract | Location | (empty) | — |
RegionContract / AreaContract / ProvinceContract | Region/Area/Province | OnLoaded | None of the four Bare templates implements OnLoaded. |
LocationContract | Location | OnLoaded, OnClaimed, OnDesignated | BareLocation.cs implements none. |
FeatureContract | Feature | OnSpawned, OnDiscovered, OnInteract, OnCleared | Dungeon implements OnSpawned + OnCleared; others implement OnDiscovered only. OnInteract never implemented. |
CharacterContract | Character | OnSpawned | MainCharacter / 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:
| Site | source entity's scope | Target scope | Reachable via declared ParentScopeId? |
|---|---|---|---|
Formula_FarmFood.cs:17 | Building/Location (owner is the Farm's building scope, root scope target = Location) | H.Location | Yes — identity/self or Building → Location |
Formula_IrrigatedFarmFood.cs:17 | Building/Location | H.Location | Yes |
Formula_TerracedFarmFood.cs:17 | Building/Location | H.Location | Yes |
Farm.cs:32 | Location (root target passed into the CanBuild query) | H.Settlement | No — 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.OwnerIdand returning the owning Settlement), in which case the declaration is the source of drift; or - The walk really does fail, and
Farm.CanBuildis 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 walkowner → H.Location.owneris the Farm building entity; per02-templates.md, building templates haveRootScopeId = H.Location, so the "walk" is semantically identity (or at most a lookup throughBuilding → LocationperDeclarations.cs:12). Both arms are declared; the walks are sound. ResourceProductionSystem.cs:55takes the opposite path: rather than walking from a Location up to Settlement, it readsLocation.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 (seeDeclarations.cs:60). It works; the question the audit raises is whyFarm.CanBuilddid not use the same idiom.- There is no other instance of
WalkScopefrom template-side code. The generated buildingCanBuildqueries now read location building counts throughLocationScopeQueries.BuildingCount(...), the typed facade overISecsHostReads.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:
| Site | Pattern | Notes |
|---|---|---|
ResourceProductionSystem.cs:66-89 | Per-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-47 | Two commands queued (AddModifier + IncrementScopeField), then one Process/Clear. | Batched (2 ops / 1 flush). Differs from the 1-op-per-flush elsewhere. |
FamineStartsEvent.cs:41-48 | Two 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-48 | Two commands (AddModifier + IncrementScopeField), one flush. | Same shape as PlagueSpreadsEvent. |
CelebrationBonusEvent.cs:34-64 | Four distinct Process/Clear pairs, one per logical effect. | Most verbose shape. |
RecruitGarrisonAction.cs:48-51 | Two commands, one flush. | Same as PlagueSpreads. |
TaxCollectionSystem.cs:34-36 | One command, one flush — inside the per-entity foreach. | Canonical shape. |
PopulationGrowthSystem.cs:56-58 | One command, one flush per entity. | Canonical. |
OnActionDeclarations.cs:37-39 | One 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 CommandBuffer → CommandProcessor → 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):
| File | Lines | Has CanBuild query | Has GetFieldInt | Has Methods | Comment |
|---|---|---|---|---|---|
Farm.cs | 43 | yes (stage gate) | yes | no | canonical |
GreatHall.cs | 31 | yes (BuildingCount) | yes (returns 0) | no | GetField => 0 — shape differs |
House.cs | 33 | null (explicit) | yes | no | only explicit-null in corpus |
Forge.cs | 38 | yes (BuildingCount) | yes | no | same shape as GreatHall |
Watchtower.cs | 39 | yes (BuildingCount) | yes | no | same shape as GreatHall |
Walls.cs | 38 | yes (BuildingCount) | yes | no | same shape as GreatHall |
Storehouse.cs | 37 | yes (BuildingCount) | yes | no | same shape as GreatHall |
TrainingGrounds.cs | 47 | yes (BuildingCount) | yes | yes (OnBuilt) | installs Fortified |
Temple.cs | 37 | yes (BuildingCount) | yes | no | same shape as GreatHall |
Barracks.cs | 48 | yes (BuildingCount) | yes | yes (OnBuilt) | installs Fortified, brace-brace init |
Tavern.cs | 46 | yes (BuildingCount) | yes | yes (OnBuilt) | installs FestivalSpirit |
IrrigatedFarm.cs | 23 | no | no | no | no stage gate (unlike Farm) |
TerracedFarm.cs | 23 | no | no | no | no stage gate |
Housing.cs | 29 | yes (BuildingCount < 2) | no | no | costs undeclared |
Granary.cs | 37 | yes (BuildingCount) | no | yes (OnBuilt) | only Methods-bearing building without flat channels |
Workshop.cs | 21 | no | no | no | shortest building |
AdvancedWorkshop.cs | 21 | no | no | no | shortest building |
WatchtowerTier2/3.cs | 30 | no | yes | no | no tier gate |
ForgeTier2/3.cs | 31 | no | yes | no | no tier gate |
GreatHallTier2/3.cs | 30 | no | yes | no | no tier gate |
WallsTier2/3.cs | 31 | no | yes | no | no tier gate |
StorehouseTier2/3.cs | 30 | no | yes | no | no tier gate |
TrainingGroundsTier2.cs | 40 | no | yes | yes (OnBuilt) | installs Fortified |
TrainingGroundsTier3.cs | 40 | no | yes | yes (OnBuilt) | installs Fortified |
TempleTier2/3.cs | 30 | no | yes | no | no tier gate |
Key observations:
House.cs:15was the only template in the corpus that explicitly set the legacy activation-query delegate tonullrather than omitting the init. Trivial stylistic divergence in the hand-written stand-in.Housing.cs,Workshop.cs,AdvancedWorkshop.cshave noGetFieldIntslot at all. Their costs are undeclared — the engine returns 0 by default viaTemplateEntry.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 viaGetField.GreatHall.cs:30hasGetField => 0; // Free — already built— a unique switch-free implementation that returns 0 regardless of field id. Every otherGetFieldis a switch expression keyed onfieldId.- All 12 tier-variants never declare a
CanBuildquery. The intent is presumably "a tier transition bypasses theBuildingCount == 0query 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>withTemplateCommand.NoArgs(...); the future compiler should keep one canonical initializer form. - Historical
StorehouseTier2.cs:20,StorehouseTier3.cs:20finding: at audit time these templates registeredH.Food(settlement-scope channel) as a flat int on a Location-scope Building template. Current Valenar generated output no longer does this; Storehouse tiers attachFoodCapacitymodifiers to the owning settlement. Granary.csis the only building with populatedMethods+ non-emptyOnBuilt+ zero flat channel contributions inActivate.02-templates.md:1124calls 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}.
| File | Difference from Ruins.cs |
|---|---|
Dungeon.cs:16-19 | Uses [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 / Ruins | Identical 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.
| File | Difference from BareProvince.cs |
|---|---|
BareLocation.cs:25 | Registers 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-22 | Activate signature matches but body is only a comment. No divergence. |
| All Bare templates | None 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).
| File | Difference from TaxCollectionSystem.cs |
|---|---|
PopulationGrowthSystem.cs | Identical shape. |
ResourceProductionSystem.cs:34-42 | No 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:55 | Only 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.cs | Frequency => 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).
| File | Difference from DemonRaidEvent.cs |
|---|---|
PlagueSpreadsEvent.cs | Only event with player choices. Uses BuildOptions(EventOptions, EventContext) and option handler methods rather than Execute. |
CelebrationBonusEvent.cs | No Condition override — relies on the SecsEvent base default (Condition => true). Also no Frequency / Chance overrides (defaults: 1 / 100). |
RaidResolvedVictoryEvent.cs, RaidResolvedDefeatEvent.cs, FamineStartsEvent.cs | TriggerType = 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.
| File | Difference from Formula_FarmFood.cs |
|---|---|
Formula_WatchtowerDefense.cs, Formula_FortifiedDefense.cs, Formula_HousePopCap.cs | Do 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.cs | Walk 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).
| File | Difference from Trigger_FoodLt0.cs |
|---|---|
Trigger_MoraleGte80.cs | Identical shape. |
Trigger_DefenseGte50.cs | Identical shape. |
Trigger_MoraleGte60.cs:10-11 | Only 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 inDungeon.cs:35)RemoveAllBindingsRemoveAllChannelSourcesRemoveModifiersByTagRemoveModifiersByTagFromOwner
ISecsHostReads:
ReadFloatReadBoolReadEntityGetChildrenCallBoolMethodResolveStatFloatResolveStatBool
ISecsHostWrites: all three (WriteInt, WriteFloat, WriteBool) — intentional; writes go through commands.
TickContext:
RandomFloatClearSavedScopesTryGetSavedScopeDirtySet,BindingStore,TickDispatcher,BindingUpdater,Registry,IdGenerator,HostWrites,Activator(every auxiliary field exceptCommands/CommandProcessor/InstanceStore/Host/ChannelResolver/Events/Random/TickNumber)
InstanceStore:
Add(only called by engine)RemoveByScopeByScopeAndContractTryGetInstanceCountByContractTryGetArchetype
ChannelResolver:
ResolveInt(the rich variant that returnsStatResultwith decomposition)ResolveFloatResolveFloatFastResolveBool
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.RemoveAllBindingsis now part of engine-driven template destruction rather than a source construct.TryGetSavedScope— theGetSavedScope+IsNullcheck idiom inCelebrationBonusEvent.cs:41-42is a hand-rolled TryGet. The future compiler may preferTryGetSavedScope.
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 areSecsScalarType.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:
ResourceProductionSystem.cshas noFrequencydeclaration and usesAllByContract(notAllByContractForTick). It runs every tick at O(settlements × locations); all three other systems use per-tick distribution. It also carries the corpus's onlyctx.Host.ReadIntcall from a system (line 55), bypassing the scope walk idiom used by formulas.Trigger_MoraleGte60.cs:10-11branches oncaptured0.IsNullto decide which entity to resolve against. This is a silent mode switch with no compile-time enforcement; callers that forgetcaptured0get the wrong semantics.CelebrationBonusEvent.csomitsCondition— relies onSecsEventbase defaulttrue. Divergence is minor because the event is intentionally unconditional.- 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.
- Resolved 2026-04-26:
Farm.cs:32walkedLocation → Settlement, which was not reachable via the old single-parent chain. Current generated declarations useParentScopeIds,location walks_to Settlementis explicit, and the generated query returnsfalsewhen 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.