Mod-coverage audit — design docs
Historical note (2026-04-25): findings about FNV-1a-32 /
uintidentifier surfaces were implemented by the FNV-1a-64ulongmigration after this audit. The original finding text is retained as audit history.Resolution note (2026-04-26): several scope-walk/template-field findings in this audit have since been implemented in the current lowering docs and code:
ParentScopeIdsreplaced the old single-parent shape,location walks_to settlementis declared, and template build costs arefieldvalues resolved through the effective template-value resolver. Treat unresolved rows here as audit backlog only after checking the live docs first.Resolution note (2026-05-01): the live docs now split source sets into host-capable base/official-expansion content and third-party data-only mods. Findings below that discuss third-party mods adding host-backed scopes, fields, collections,
walks_toedges, or scope methods are historical unless restated against the host-capable expansion layer. Data-only mods consume the exported host surface; they do not ship host bridge DLLs.Resolution note (2026-04-26): modifier lifecycle findings 19, 62, and 63 are resolved by the owner/target lifecycle commitment. Template-owned modifiers clean up by owner destruction; source-level
OnDestroyedremoval is not the ordinary lifetime mechanism.Resolution note (2026-04-30): action findings 48 and 49 are superseded by the class-based action model in
04-behavior.mdand the method-level action slot rows in06-overrides-and-modding.md. The oldallow/effect/ai_will_doblock terms below are retained as audit history only.
For the canonical mod-operation architecture (operation model, slot schema, typed IDs,
canonical identity strings), see 06-overrides-and-modding.md.
Architecture note (2026-05-04): the single
override Name { ... }keyword has been replaced by explicit operations:inject,replace,try inject,try replace,inject_or_create,replace_or_create(06-overrides-and-modding.md § 3). Top-levelphase,tag, andtick_ratedeclarations are no longer SECS surface — they are plain C# (06 § 9). The system-body slot syntax changed fromphase Y;/frequency Y;to assignment formphase = Y;/frequency = Y;(06 § 6 and § 8.1). Older wording here treated aprev_tickhelper as a plain C#ctx.PrevTickcall (06 § 9.5). Superseded live contract: previous-tick access usestrack_prev = trueplus host/runtimeReadPrevTick*bridge reads. Typed ID record structs (PhaseId,TagId,ChannelId,TemplateId,ContractId,ScopeId,SystemId,EventId,ActionId,OnActionId,TickRateId,ModifierId) replace rawulong(06 § 4.1). Canonical identity strings (valenar:template/farm,mod:phase/x) are hashed via FNV-1a-64 to prevent mod collisions (06 § 4.2). Findings below that reference the oldoverridekeyword or the old slot-syntax forms are retained as audit history; the fix shapes below remain valid audit backlog items even as the new architecture supersedes the specific surface forms.
Original audit was read-only and found 64 gaps across 8 docs. Resolution notes may be added later as individual gaps are closed.
Methodology
Scope: every docs/design/*.md file (00–06 plus FUTURE_WORK). Each was read for:
ordering/positioning rules, identity/uniqueness, lifecycle/registration, diagnostic
codes, scope graph semantics, cross-declaration references, effect-mode enums, channel
declaration invariants, hash conventions, bulk-update assumptions, numeric-type
coverage, the 6-phase pipeline, localization, on-action / action contracts,
template-body constructs, and runtime-validation paths. For each rule that is
well-specified for a single-source-set base game, the doc was re-read with the
question "does this still hold when a mod adds, overrides, or interleaves content?".
Severities:
- BLOCKING — design is incoherent under mods; compiler can emit wrong output or merger produces undefined behaviour. Must address before Phase 3 ships.
- DRIFT — rule works for base but is silent on mods; mod author cannot tell what's allowed without reading engine code.
- POLISH — wording is nearly there; a one-sentence clarification closes it.
Out of scope: engine runtime bugs (those are tracked in FUTURE_WORK), .secs
grammar choices that don't intersect modding, and superseded legacy semantics
that are no longer part of the live doc set. FUTURE_WORK entries are themselves in scope when a mod author would read
them to figure out what they can do.
Findings
Doc 00 — Overview
-
DRIFT, 00-overview.md:83 — Glossary entry "Override" is single-source
- Glossary defines Override as "a mod-level replacement … resolved in the compiler's pre-binding merge pass" but the glossary says nothing about the five other mod capabilities (add new declaration, add on_action, add system, add event, add modifier effect). A reader of 00 alone would conclude modding is override-only.
- Mod scenario: Mod adds a new
template<Building> Lighthouse { … }with nooverride. The glossary does not hint that this is legal; the reader has to jump to doc 06 to confirm. - Fix shape: Add a sibling glossary entry "Mod extension — a mod-level
addition of any declaration (new template, new modifier, new system,
new event, new on_action, new action, new channel, new scope, new contract,
new tag). Distinct from
override; requires no keyword. Resolved in the same pre-binding merge pass thatoverrideuses." Cite doc 06.
-
DRIFT, 00-overview.md:164-180 — Active-diagnostics list is silent on mod origin
- The "Active diagnostics as of 2026-04-24" list gives each code a doc home
but does not say whether the check fires across the base+mod union or
only inside a single source set. Without that clarification, e.g.,
SECS0107(case-collision) is ambiguous: does it fire if base declaresGoldand a mod declaresgold? - Mod scenario: Mod author names a modifier
Fortifiedin PascalCase, not knowing base already declaredfortified. Both hash to the sameH.Fortified. Today the list does not tell them whether this is SECS0107, SECS0108, or silent shadow. - Fix shape: Add a column "Fires across source sets?" or a one-line note per diagnostic explaining whether it checks {base only, base∪mod, per-mod}. Phase 3 merger semantics require every diagnostic to be re-qualified for the mod case; the list is the natural checklist.
- The "Active diagnostics as of 2026-04-24" list gives each code a doc home
but does not say whether the check fires across the base+mod union or
only inside a single source set. Without that clarification, e.g.,
-
POLISH, 00-overview.md:184-186 — Status flags don't mention mod runtime
- "NOT YET IMPLEMENTED" and "DEFERRED" describe engine readiness but not whether the status is temporary for mods (e.g., will hot-reload ever be a status value?). A mod author reading this sees no "HOT-RELOAD" category even though the workspace's ADR talks about live re-merge.
- Fix shape: One sentence clarifying that status flags describe the lowering-contract state, not mod lifecycle. If hot-reload becomes a scoped feature, it lives in a new category.
Doc 01 — World Shape
-
BLOCKING, 01-world-shape.md:117-120 —
walks_todesign decision unresolved under mods- The doc offers two designs for
walks_tostorage: design (1) "single parent- transitive walks" vs design (2) "multi-edge declaration". The choice is
explicitly undecided. A mod that adds
walks_to Settlement;onscope locationhas radically different semantics under the two designs. Mod-ready spec cannot defer this.
- transitive walks" vs design (2) "multi-edge declaration". The choice is
explicitly undecided. A mod that adds
- Mod scenario: Base declares
scope Location { walks_to Province; }. Mod A declares (via override? via extension? — also ambiguous) an additionalwalks_to Settlement;. Under design (1) this edge may have to be computed transitively; under design (2) it is a new row in aParentScopeIdsarray. Merger behaviour is undefined. - Architecture update (06 § 8.6): Scope additions are additive, not
full-replacement overrides. Official expansions (host-capable source sets)
may declare additional
walks_toedges on existing scopes and they are merged additively. Third-party data-only mods cannot addwalks_toedges because these require host support (SECS0609). Theinject scope Location { walks_to Settlement; }form is reserved for host-capable expansions only. - Fix shape (updated): Commit to multi-edge
ParentScopeIdsstorage (design 2) as the 06 § 8.6 intent implies. Specify thatinject scope X { walks_to Y; }is the syntax for host-capable expansions, and that third-party mods attempting it receive SECS0609.
- The doc offers two designs for
-
BLOCKING, 01-world-shape.md:82-84, 150-153 — Scope-extension semantics unspecified
- The doc documents
scopedeclarations andcollectiondeclarations but nowhere says whether a mod can extend an existing base scope. Doc 06 § 8.6 covers scope slots (additivewalks_to, plus scope fields and collections) but only for host-capable source sets; third-party data-only mods explicitly cannot add host-backed surfaces (SECS0609 per 06 § 12). - Architecture update:
06-overrides-and-modding.md§ 8.6 establishes that scopewalks_toadditions are additive (not override). Third-party mods cannot add new scope fields, collections, orwalks_toedges because these require host support. The oldoverride scope Settlement { int Piety; }syntax is superseded; the new form for host-capable expansions would beinject scope Settlement { int Piety; }(pending full scope-inject spec in doc 01). - Mod scenario: Mod adds a new channel
Pietywhose source issettlement.Piety. The host bridge must know to allocate storage for this field. Data-only mods cannot do this; it requires a host-capable expansion. - Fix shape: Add a section to doc 01 and/or doc 06 describing
scope-extension rules for host-capable expansions: what
inject scope X { ... }allows (fields, collections, walks_to edges), whether host bridges must opt in to expansion-added fields, and what happens if two expansions add the same field name to the same scope.
- The doc documents
-
RESOLVED 2026-04-26, 01-world-shape.md:111-120 — single-parent scope storage was not mod-composable
- Historical finding:
ScopeDeclarationhad one parent slot, so two mods adding different walk edges could not both be represented. - Current state: the live contract uses
ScopeDeclaration.ParentScopeIdsand the docs commit to additivewalks_toedges merged into that array.location walks_to Settlementis now declared instead of being a host-only fallback.
- Historical finding:
-
RESOLVED 2026-05-01, 01-world-shape.md:173-174 — Scope methods have a host-capable boundary
- Historical finding: scope-method mod extensibility was unspecified.
- Current state: third-party data-only mods may call exported scope methods but may not add or override scope-method signatures. Base game and official expansions may add scope methods because they can ship matching host code as part of the host-capable layer.
- Resolved shape: The live docs split the cases: (a) official expansion
adds a new method on an existing scope, (b) third-party mod attempts to
add or override a scope method and receives
SECS0609, (c) replacement of an existing host implementation is an official host-code concern outside third-party modding. Tie each to a diagnostic code.
-
DRIFT, 01-world-shape.md:353-356 — Contract extensibility unspecified
- Doc 01 lists open questions for contracts: "Contract inheritance /
mixins (spec-level
extends) are not present". Doc 06 adds that mods cannot overrideroot_scopeor method lists. But mods adding new methods to existing contracts (e.g., addingOnRaidedtoBuilding) is untouched. If the rule is "only the base may define contracts", that needs to be stated. - Mod scenario: Mod introduces a new lifecycle hook
OnEnchantedfor buildings. No contract-extension story means the hook cannot be declared; the mod must work around it via on_action or events. - Fix shape: In doc 06, add to "What's NOT overridable": "Mods cannot add new methods to existing contracts. To introduce a new lifecycle hook, define a new contract and a new template hierarchy." Or commit to a scoped extension syntax and spec it.
- Doc 01 lists open questions for contracts: "Contract inheritance /
mixins (spec-level
-
BLOCKING, 01-world-shape.md:362-405 —
SecsScalarTypeenum closure under mods- "The set is closed — no further numeric types are accepted in Phase 1
of the compiler bring-up or at any later phase without an explicit
design change." This is correct for base — but doc 06 does not say
anything about mod-extensible enums. A mod cannot add
decimaltoSecsScalarType; doc 01 says this without qualifying whether modders may or may not attempt it. - Mod scenario: Mod declares
channel decimal Treasury { … }. Compiler must emit a clear diagnostic rather than generate code that the engine cannot handle. Today the rule is silent. - Fix shape: Add to the "Explicitly rejected" list: "Mods cannot
extend
SecsScalarType. Achannel <non-listed-type>declaration in a mod emits a binder diagnostic (proposed SECS01XX)."
- "The set is closed — no further numeric types are accepted in Phase 1
of the compiler bring-up or at any later phase without an explicit
design change." This is correct for base — but doc 06 does not say
anything about mod-extensible enums. A mod cannot add
-
BLOCKING, 01-world-shape.md:456-476 —
kind =required on mod-declared channels too- The rule "explicit
kind =required on every top-level channel declaration" has no mod qualifier. Reader assumes it applies base-only. - Mod scenario: Mod declares
channel int Piety { source = settlement.Piety; min = 0; }with nokind =.SECS0101fires. Good — but the doc doesn't say so explicitly, and the reverse case (for example, a legacy channel-replacement attempt targetingPopulation) isn't covered either (doc 06 says channel declarations are not overridable, so this case should beSECS0602or a new code). - Fix shape: Clarify that
SECS0101fires on all declarations regardless of source set. For mod attempts to replace channels, reference the specific diagnostic (likely SECS0602 if treated as duplicate declaration without an explicit mod operation, or a new code specific to "channel declarations are not overridable").
- The rule "explicit
-
DRIFT, 01-world-shape.md:437-438 —
Internal = falseon user channels is base-onlyChannelDeclaration.Internalflag is implied to be compiler-internal. Doc mentions "anInternalChannelKind for purely compiler-internal channels remains a potential future addition". Whether mods' declared channels always haveInternal = falseis not stated — and whether a mod could declare anInternal = truechannel for its own compiler-private bookkeeping is also absent.- Mod scenario: A mod's generated output contains internal channels
(say, cache slots) that the mod author does not want to expose to
other mods or to debug UIs. Can they mark them
Internal? No rule. - Fix shape: Add one line: "All user-declared channels — base or mod —
lower with
Internal = false. TheInternalslot is reserved for compiler-synthesised channels and is not reachable via.secssurface in either source set."
-
RESOLVED (2026-05-04), 01-world-shape.md:728-778 — FNV-1a-32 collision plan silent on mod scale
- Historical finding: FNV-1a-32 was inadequate for mod-scale identifier spaces; a popular mod ecosystem crossing 10k identifiers would reach ~5% collision probability with only 3k mods loaded.
- Resolution:
06-overrides-and-modding.md§ 4.2 commits to FNV-1a-64 as the canonical hash function, with canonical identity strings of the formvalenar:template/farmas hash inputs, which structurally prevents unrelated mods from colliding on common names. § 4.1 commits to typed ID record structs (ChannelId,TagId,PhaseId,TemplateId, etc.) wrappingulong, eliminating rawuintfrom the identity surface. The 64-bit collision boundary (~1 collision per ~5 billion identifiers) is adequate for any realistic mod ecosystem. - Remaining action: Update Generated/ stand-ins to 64-bit where still using 32-bit as historical artifact. The design commitment is settled.
-
DRIFT, 01-world-shape.md:765-770 — SECS0107 case-collision across mods
- The rule "
SECS0107: identifier collision … under case-insensitive lowering" does not say whether the check runs on base alone, on each mod in isolation, or across the full merged set. If a mod declaresGoldand base already hasgold, they collide — is this SECS0107 (error), SECS0108 (warning, use override), or silent merge? - Mod scenario: Mod author reuses a base identifier by accident with different casing, expecting case-preservation.
- Fix shape: Add to the SECS0107 spec: "fires across the union of base and every loaded mod's declarations. Two mods whose declarations collide under case-insensitive lowering both error; the base may not shadow a mod's declaration via case either."
- The rule "
-
DRIFT, 01-world-shape.md:772-774 — SECS0108 cross-assembly warning is mod-only
- "Two assemblies (base game + mod, or two mods) that declare a channel /
modifier / template / etc. with the same name share one FNV-1a-32
hash and therefore one runtime identity." Good — but the rule is
implicit that when mod A declares
Fortifiedand mod B also declaresFortified(neither withoverride), SECS0108 fires on both? Or one? What if mod A declares and mod B usesoverride? Case table is incomplete. - Mod scenario: Two mods independently introduce a
modifier Fortified { … }. Neither usesoverride. Merger must decide. Today: last-writer-wins by load order per doc 06; but SECS0108's wording ("redundant declaration, can be removed") assumes the author intended redeclaration, which is not the case when two independent mods clash. - Fix shape: Split the rule into three cases: (i) mod declares a
name already in base, no override → SECS0108 warning; (ii) mod
declares a name already in another mod, no override → SECS0601
conflict-report entry (the concept already exists in doc 06, but is
not referenced here); (iii) mod uses
overrideon a name in another mod (not base) → well-defined; last mod in load order wins. Spec all three.
- "Two assemblies (base game + mod, or two mods) that declare a channel /
modifier / template / etc. with the same name share one FNV-1a-32
hash and therefore one runtime identity." Good — but the rule is
implicit that when mod A declares
-
RESOLVED (2026-05-04), 01-world-shape.md:835-836 —
uintvsulongfor mod-added entities- Historical finding: the identity space was
uint-based, insufficient for mod-scale identifier flooding. - Resolution:
06-overrides-and-modding.md§ 4.1 commits the full typed ID record struct set:PhaseId,TagId,ChannelId,TemplateId,ContractId,ScopeId,SystemId,EventId,ActionId,OnActionId,TickRateId,ModifierId— each areadonly record structwrappingulong. Rawulongis no longer the public ID surface. The typed wrappers prevent accidental mixing ofChannelIdandTagIdeven though both wrapulong. - Remaining action: Update Generated/ stand-ins where
uinthashes are still present as historical artifacts.
- Historical finding: the identity space was
-
DRIFT, 01-world-shape.md:820-821 — Hand-picked hash sentinels block mod detection
- "Some identifiers in
Hashes.csuse non-FNV values (e.g.BuildOrder = 0xB01D0001)." These sentinels bypass the FNV check. If a mod happens to declare a name that hashes to a sentinel, no diagnostic fires, the declaration silently collides with the sentinel. - Mod scenario: Mod declares
template<Building> Watchpost; FNV-1a hash happens to equal the hand-pickedBuildOrdersentinel. Mod's template is unreachable at runtime. - Fix shape: Commit to eliminating hand-picked sentinels. Make the compiler reject them in the Phase 1 emitter. FUTURE_WORK § 3.6 already tracks the invariant violation; tie the fix to the mod- coverage story.
- "Some identifiers in
-
BLOCKING, 01-world-shape.md:843-848 — Registry collision policy underspecified
SecsRegistry.Register"uses 'last writer wins' semantics by default. Whether the compiler should emit an explicitoverridemarker to make the intent visible is open." If mods rely on the registry's last-writer-wins at runtime (e.g., two mods register the same modifier id), the mod-manager / Phase 3 conflict report (doc 06) is the only surface that warns. But the registry's silent overwrite is a latent bug class that will survive even a shipping compiler.- Mod scenario: User sideloads a mod by dropping a DLL at runtime (not merged at compile time). The registry silently replaces a base-game declaration with the mod's version. No diagnostic, no conflict report.
- Fix shape: Commit to one of: (a) registry rejects duplicate ids
after startup; (b) registry treats duplicates as explicit
Replace()calls and logs; (c) runtime DLL sideload is explicitly out of scope and all modding goes through the compile-time merger. Doc 06 states (c) but registry code isn't aligned yet.
-
DRIFT, 01-world-shape.md:865-878 — Invariant list is base-only
- "Invariants the compiler must enforce" lists nine rules. None of them
say anything about mods. E.g., "Every
SourceScopeIdreferenced in a channel declaration must correspond to an existingScopeDeclaration" — when a mod declares a channel whosesource = ...points at a base-declared scope, this is fine. When it points at a mod-A-declared scope and mod A isn't loaded, the merger must either reject or fall back. Rule is silent. - Mod scenario: Mod B's channel
source = modA.Piety. Mod A is not loaded. Merger output references a ScopeFieldDeclaration that doesn't exist. Boot fails with opaque error. - Fix shape: Annotate each invariant with its cross-mod behaviour.
Specifically, how does the merger handle unmet dependencies on other
mods? Does the mod manifest (
mod.jsonper doc 06) declare its dependencies, and does the merger validate them before merging?
- "Invariants the compiler must enforce" lists nine rules. None of them
say anything about mods. E.g., "Every
Doc 02 — Templates
-
RESOLVED, 02-templates.md:501 — Modifier auto-detach under mod overrides
- Resolved 2026-04-26. The committed pattern is owner/target cleanup:
owneris lifetime source,targetis effect anchor, and template destruction removes bindings owned by the destroyed instance. A mod that overridesOnBuiltand attaches a different modifier still creates a binding owned by the same instance; ordinary destruction cleanup removes it without any inheritedOnDestroyedremove site.
- Resolved 2026-04-26. The committed pattern is owner/target cleanup:
-
DRIFT, 02-templates.md:291-302 — Formula hash is
Formula_{TemplateName}{ChannelName}- Formula ids are derived from template + channel name. A mod that overrides
a template's dynamic channel re-uses the same formula id (per doc 06).
But a mod that adds a new template reusing a base-declared channel
(e.g., mod adds
template<Building> MegaFarmwith a dynamicFoodOutput) emitsH.Formula_MegaFarmFoodOutput. If the mod and base both contribute toFoodOutput, registration order matters and the spec is silent. - Mod scenario: Base's Farm and mod's MegaFarm both contribute
dynamic
FoodOutput. Both formulas register; both hooks intoRegisterFormulaContribution(Formula_*, H.FoodOutput). Under Model (a) these should be modifier attachments, not direct contributions — but the doc's cross-scope-bug flagging says this is already wrong. The mod case magnifies the drift. - Fix shape: Doc 06 should mention that once Model (a) is enforced
via
SECS0201, every cross-scope template body becomes a modifier attachment; formula ids becomeFormula_{ModifierName}{ChannelName}by extension. Clarify that mod-added templates follow the same naming.
- Formula ids are derived from template + channel name. A mod that overrides
a template's dynamic channel re-uses the same formula id (per doc 06).
But a mod that adds a new template reusing a base-declared channel
(e.g., mod adds
-
BLOCKING, 02-templates.md:488-495 — Template-method dictionary-init syntax
- Dictionary init for
TemplateEntry.Methodsvaries between Buildings (brace-brace) and Features (indexer). Compiler must pick one (FUTURE_WORK § 1 row 19). Under mod overrides, the merger emits the final template body; the chosen init form must be consistent. If the compiler outputs brace-brace for base and indexer for the override-merged version, diffs between--order [A,B]and--order [B,A]become noisy without semantic difference. - Mod scenario: Re-merge on load-order change produces a different init form purely because of authoring-side formatting. Phase 3's "interactive re-merge under 1 second" (doc 06) is still valid, but the output's diff-cleanliness is broken.
- Fix shape: Canonicalise the init form. The compiler must emit one form regardless of which source the final slot came from.
- Dictionary init for
-
RESOLVED 2026-04-26, 02-templates.md:416 —
CanBuildpermissive fallback on mod walks- Historical finding: Farm's hand-written body returned
truewhen the settlement walk failed. - Current state: source and generated queries require the declared
location -> settlementwalk and returnfalseif the runtime relationship is absent. Undeclared walks areSECS0111. - Historical mod scenario: Base Farm
CanBuildwalks to Settlement and returnstrueif walk fails. Mod addswalks_to Settlementonscope Location. The walk now succeeds,Stage >= 1check runs, and Farms that were previously buildable become unbuildable on a fresh map. - Resolution: doc 02 now states the failure-mode convention. Doc 01 owns the scope-edge contract; doc 06 owns the additive mod merge rule.
- Historical finding: Farm's hand-written body returned
-
BLOCKING, 02-templates.md:503-571 — Bare templates under mods
- "The compiler emits one Bare template per scope that participates in
runtime spawn". A mod that adds a new scope would also need a Bare
template. Doc does not say whether the mod declares it explicitly,
whether the compiler auto-emits one, whether a mod can
override BareSettlementto change defaults (BareLocation hasSlots = 4). - Mod scenario: Mod adds
scope OrbitalStationwithwalks_to province. Compiler must emitBareOrbitalStation. Does the compiler auto-generate, or does the mod author need to declare an explicit Bare template? Today the Valenar MainCharacter .secs declarestemplate<Character> BareCharacterexplicitly (line 565-566 flags this as a legacy artifact that the compiler will subsume). - Fix shape: State the Bare-template rule as a universal
invariant — "for every scope declared (base or mod), the compiler
emits one Bare template automatically; mods may
inject template BareX { ... }to change defaults" (using theinjectoperation from 06 § 3.2). Cross-reference from doc 06.
- "The compiler emits one Bare template per scope that participates in
runtime spawn". A mod that adds a new scope would also need a Bare
template. Doc does not say whether the mod declares it explicitly,
whether the compiler auto-emits one, whether a mod can
-
DRIFT, 02-templates.md:785-789 —
name = "X"string pool under modsname = "Hero"on a template is template metadata, not localizable. Two mods that both setname = "Champion"on different templates cause no clash becauseTemplateEntry.Nameis stored per-template. But if a mod override setsname = "Zero"on base'sMainCharacter(a pattern overriding this metadata is explicitly in doc 06's slot list — line 104 "template method override" — butname =isn't a method, it's a metadata field), is that an override-able slot?- Mod scenario: Mod wants to rename MainCharacter from "Hero" to
"Zero". Inject shape per new operation model:
inject template MainCharacter { name = "Zero"; }. - Fix shape: Explicitly list
template_name_metadataas an injectable per-field slot in 06 § 8.2 (it appears in the slot table asname = "Farm";with merge rule "per-field replacement"). Specify what happens if a mod sets an empty string. Theinjectoperation replaces only the name slot, leaving all other template slots intact.
-
DRIFT, 02-templates.md:642-693 —
reads { }auto-extraction under mod overrides- Auto-extraction walks the formula body. When a mod overrides a dynamic channel formula (doc 06:233-287), the override's body produces a different auto-extracted read set. Does the compiler re-run auto-extraction on the merged body? The doc implies yes (since the formula is emitted from the merged AST) but doesn't say so.
- Mod scenario: Base:
Formula_FarmFoodreads{ Fertility }. Mod's override body addsresolve(Blight). Merged formula must register withreadsChannels: [ Fertility, Blight ]. But theregistry.RegisterFormulacall lives in the centralSecsModule.cswhich was written for the base; the merger must rewrite that registration line too. - Fix shape: Doc 06 should explicitly say: "For a dynamic-channel
override, the merger re-runs auto-extraction on the merged formula
body and rewrites the
readsChannels:argument on the correspondingRegisterFormulacall inSecsModule.cs." Otherwise the cycle detector operates on stale dependency metadata.
-
BLOCKING, 02-templates.md:694-727 —
upgradeblock and mod overrideupgradeis NOT YET IMPLEMENTED. Once it lands, the override story is non-trivial: can a mod override the upgrade target (upgrade TerracedFarmbecomesupgrade MegaFarm) or the query binding independently? Resource values should remain templatefieldslots rather than an action-style templatecostblock.- Mod scenario: Mod wants to change Farm's upgrade path from IrrigatedFarm to AutomatedFarm. Current design needs field overrides on the target template plus a transition query override; exact syntax is deferred.
- Fix shape: When
upgradelands, add it to doc 06's override slot table with slot granularity specified (whole block vs per-field).
-
DRIFT, 02-templates.md:1080-1085 —
TemplateEntryinit-field mod rewriting- Canonical init-field order is specified. A mod override that changes
GetFieldoutput doesn't touch the init field order. A mod that injects a new lifecycle method (e.g.,inject template Farm { method void OnRaided() { … } }) must add[H.OnRaided] = OnRaided,to theMethodsdictionary init. If the contract doesn't declareOnRaided, this is aSECS0XXXerror (which doesn't exist). The doc is silent. - Mod scenario: Mod uses
inject template Farm { method void OnRaided() { ... } }— a method the base contract'smethodslist does not include. Compiler behaviour unspecified. - Fix shape: Spec the rule: mod inject bodies may only add methods
that the template's contract already lists in its
methodsdeclaration. If not, emit a new diagnostic (reserve SECS0209 or similar) — or state that contract-method extension is the only way to introduce new hooks. See 06 § 8.7 (contracts are not patchable by third-party mods).
- Canonical init-field order is specified. A mod override that changes
Doc 03 — Channels and Modifiers
-
BLOCKING, 03-channels-and-modifiers.md:62-103 — Modifier declarations: duplicate across mods
- Modifier declarations produce
ModifierDeclarationentries indexed byModifierId. Doc 06 specifies effect-bundle overrides are full-replacement. But two mods adding the same modifier name (withoutoverride) should clash. Doc 03 says nothing; doc 06 mentions SECS0602 briefly but doesn't explicitly name the modifier-declaration case. - Mod scenario: Mod A adds
modifier Blessed { … }. Mod B addsmodifier Blessed { … }, different effects. Without explicitoverride, both should error with SECS0602. Today the spec is only clear that the diagnostic exists for templates. - Fix shape: Explicitly list modifier declarations under SECS0602's scope — every declaration type (template, modifier, system, event, on_action, action, channel, scope, contract, formula, trigger) is SECS0602's domain. Doc 06's diagnostic spec should enumerate.
- Modifier declarations produce
-
DRIFT, 03-channels-and-modifiers.md:105-169 — EffectMode enum is fixed
EffectModeis Additive / Multiplicative / HardOverride. The enum is closed. Mods cannot add a new effect mode. Doc 03 doesn't say this; a mod author reading the doc might reasonably think effect-mode extensibility is possible because modifier declarations feel extensible.- Mod scenario: Mod wants a "clamp to N" effect (
Morale <= 50to cap morale). Deferred already (doc 03 line 172). Mod author attempts to add it via a new EffectMode entry. - Fix shape: State in doc 03: "EffectMode is a closed enum. Mods cannot extend it. New effect modes require an engine + compiler release."
-
BLOCKING, 03-channels-and-modifiers.md:270-384 — Stacking policy under mod-added attachers
- Stackable modifiers are keyed by
(owner, target, modifierId). Ten base Houses stack ten bindings. A mod adds 5 MegaHouses that each attachHouseMoralePresenceas well. Undermax_stacks = 100this is fine, but if both mods think they own the cap, clashes can occur. Also: if a mod overridesHouseMoralePresenceto havemax_stacks = 5, existing base bindings at count 10 become invalid. - Mod scenario: Override reduces
max_stacks. Live bindings exceed the new cap. Behaviour unspecified — do the excess bindings stay, get truncated, get rejected? - Fix shape: Specify in doc 06 the rule for overrides that
change
max_stacks,stacking, orreapplyon a modifier with live bindings. Options: (a) re-validate at startup only (since the compiler outputs a single DLL, "live bindings" only exist at runtime and the saved-game migration needs a story); (b) truncate; (c) reject at compile time.
- Stackable modifiers are keyed by
-
DRIFT, SUPERSEDED, 03-channels-and-modifiers.md:546-594 — Historical tag model replaced by registry-backed tags
- Historical finding: This audit originally assumed tags were
FNV-1a hashes of lowercase names, mods could introduce new tags
freely at use sites, and the missing decision was only who emitted
registry.RegisterTag(H.Tag_X, "X"). - Superseded by: the registry-backed tag model in
02-templates.md § Tags,08-collections-and-propagation.md § Part 3 (Tags), andSECS-Compiler-Plan.mdPhase 2 (the tag-binding bullet: bind plain C#TagIdvalues, reject top-leveltag <Name>;, emit noTagDeclaration[], register the collected catalog, and report undeclared symbolic references asSECS0212). - Current rule: A mod may add tags only by declaring
TagIdconstants with canonical id strings.tags = ...,has_tag ..., activity tag selectors, and bulk modifier tag helpers bind against the merged declared catalog. Generated output emitsRegisterTagcalls for that catalog; generatedH.Tag_*values are mirrors ofTagId.Value, not independent lowercase-name hashes.
- Historical finding: This audit originally assumed tags were
FNV-1a hashes of lowercase names, mods could introduce new tags
freely at use sites, and the missing decision was only who emitted
-
BLOCKING, 03-channels-and-modifiers.md:625-702 — Template-scoped modifiers mod override
- Template-scoped modifier names are
Template.Modifier. Doc 06:408-470 covers overriding them. But doc 03 says the lowering places them inDeclarations.Modifiers[]same as global modifiers. Under Phase 3 override, if a mod overridesoverride Barracks.VeteranGarrison { Garrison += 20; }, the merger rewrites the per-slotModifierDeclaration. But doc 03 is silent on the cross-file / cross-mod storage model: what if a mod declares its own template-scoped modifier with the same short name as another mod's template's modifier?hash("ModA.Foo.Modifier")vshash("ModB.Foo.Modifier")— but template names are already mod-qualified by FNV (sohash("ModA.Foo")≠hash("ModB.Foo")). Actually, there is no explicit mod-namespacing on template names in the spec; two mods each declaretemplate<Building> Fooand both arehash("Foo")under FNV. SECS0602 fires. OK — but the doc doesn't walk through this scenario for template-scoped modifiers. - Mod scenario: Mod A declares
template<Building> Watchtower { modifier Alertness { … } }. Mod B declares the same.H.Watchtower.Alertnesscollides under the "full identifier" hashing. Does SECS0602 or SECS0601 fire? - Fix shape: Doc 06 and doc 03 must align. Propose doc 03 point to doc 06's explicit spec: "Two declarations of the same template name (even from different mods) are SECS0602; this extends to all nested slots including template-scoped modifiers."
- Template-scoped modifier names are
-
BLOCKING, 03-channels-and-modifiers.md:873-941 — reads { } D-Hybrid under mod formulas
- D-Hybrid dynamic
resolve(<expr>)requires explicitreads { }. A mod that adds a new dynamic formula on an existing channel (e.g., adds a new modifier with dynamic effect) needs to declarereadsper its own formula. Fine. But when a mod overrides an existing dynamic formula with a new body, the override's auto-extracted read set is what matters (see finding 25). The registration-time cycle detector then operates on the merged set. Doc 03 mentions the cycle detector but doesn't walk the mod case. - Mod scenario: Base dynamic formula reads
{ A }. Mod override reads{ A, B }. The dependency graph gets a new edge B → formula. If B's contribution creates a cycle via the new edge, the mod introduces a startup-fatal cycle that the base would not have. - Fix shape: Doc 06 already references
ValidateDependenciesimplicitly but never spells out that mod overrides can introduce new graph edges. Add: "ValidateDependenciesruns on the merged declaration set after Phase 3. A mod override that introduces a new channel dependency may cause a previously-valid base to fail cycle detection; this is reported to the player via the merger's conflict report (structured as a cycle error, not a conflict)."
- D-Hybrid dynamic
-
DRIFT, 03-channels-and-modifiers.md:947-959 — SecsFormulaReadSetViolation across mods
- The debug-build runtime check catches formulas that call
resolve(X)outside their declared read set. Under mods, a formula override with a stalereads { }block will trigger this at runtime — but only in debug builds. Release builds silently corrupt the dirty-tracking graph. The spec doesn't mention this. - Mod scenario: Mod overrides
Formula_FarmFoodbody to addresolve(Irrigation)but doesn't updatereads { }. Compiler auto-extraction catches the static literal (good), but if the new call is dynamic (resolve(Irrigation)inside a runtime switch),SECS0302fires. If auto-extraction catches it, the runtime check is moot for this case. Still, a malformed mod could slip through. - Fix shape: Add a release-mode opt-in: "
--strict-readsetsCLI flag, analogous to--strict-conflicts(doc 06:560), runs the read-set validation in release builds to catch mod drift."
- The debug-build runtime check catches formulas that call
-
BLOCKING, 03-channels-and-modifiers.md:994-1015 — 6-phase pipeline ordering under mod HardOverrides
- Phase 4 HardOverride: "last-applied wins per modifier-binding-registration
order". When two mods each add a
HardOverrideon the same channel, the winner is the mod whose binding was registered last. Which mod that is depends on the engine's order of applyingAddModifiercommands during tick processing, not on compile-time load order. The doc says "existing effect-pile iteration order" but doesn't qualify how that relates to mod attribution. - Mod scenario: Mod A's
StarvationCapHardOverridesFoodCapacity = 10. Mod B'sAbundanceCapHardOverridesFoodCapacity = 100. Both bindings are active on the same settlement. Runtime order depends on which system ranAddModifierfirst, which depends on system phase + system frequency + entity iteration order. Deterministic but not predictable to a mod author. - Fix shape: Commit to one of: (a) keep current order and document "HardOverride tie-breaker is binding-registration-chronological, which is system-phase-dependent; authors who care about tie-breaking should not depend on HardOverride stacking"; (b) add a priority field on modifiers (deferred per doc 03:173).
- Phase 4 HardOverride: "last-applied wins per modifier-binding-registration
order". When two mods each add a
-
DRIFT, 03-channels-and-modifiers.md:621-623 —
translationlocalization under modstranslation = "..."inline on modifier declaration. Replaced at runtime by localization provider via id lookup. If a mod declares a new modifier, does the mod ship its own YAML localization file alongside? If a mod overrides an existing modifier's translation only (a per-field slot per doc 06:342-344), does the YAML need updating? No spec for mod YAML files.- Mod scenario: Mod adds
modifier PiousBlessing { translation = "Pious Blessing"; … }. Mod ships a YAMLmods/ModX/Localization/en.yaml. How does the runtime find it? - Fix shape: Spec mod-localization directory convention in either doc 03 or a new doc. Decide: does each mod contribute its own YAML files that the localization provider merges at runtime? Is load order applied to YAML keys too?
Doc 04 — Behavior
-
PARTIALLY RESOLVED (2026-05-04), 04-behavior.md:104 — Mods can introduce new phases (underspecified)
- Historical finding: the
const uint PhaseMyThing = ...approach left the scheduler-slotting mechanism unspecified, and mod access to top-level phase declarations was ambiguous. - Architecture update (06 § 9.1, § 9.2): Top-level phase declarations
are no longer SECS surface. They are plain C#
PhaseDeclaration.Create(...)calls in a static class. There is nophase X { ... }SECS keyword. Third-party data-only mods should not add arbitrary new phases because scheduler ordering is host-owned; the permitted mod operation isinject system ExistingSystem { phase = Phases.SomeExistingPhase; }to reassign a system to an existing phase. Thesystem_phaseslot is still overridable viainject system X { phase = Y; }. - Remaining gap: The
mod.json phase_ordermechanism for host-capable expansions adding new phases (option b of original fix shape) is still deferred — 06 § 9.2 notes it as a future feature. This finding remains open for the phase-extension story for host-capable source sets only. - Fix shape (updated): State in doc 04 that mods use
inject system X { phase = Y; }to reassign systems to existing phases. For host-capable expansions that genuinely need new phases, defer to themod.json phase_ordermechanism when it is implemented (06 § 9.2).
- Historical finding: the
-
DRIFT, 04-behavior.md:209-213 — SystemSource mod dispatch
SystemSource.Secsis mod-overridable;SystemSource.Hostis not. Mod-declared systems areSystemSource.Secsby default. But whether a mod can declare aSystemSource.Hostsystem (e.g., to inject into a host-defined phase) is not specified. Likely no, but the spec is silent.- Mod scenario: Mod wants a system that runs uninterruptible by
other mods' overrides. Is
SystemSource.Hostavailable? Or is override-protected status only achievable through host code? - Fix shape: State: "Mods declare only
SystemSource.Secssystems. Host systems are defined in host code and are not expressible in.secsregardless of source set."
-
BLOCKING, 04-behavior.md:344-411 — Event trigger type: mods adding new on_actions
- Events with
trigger on_my_new_actiondepend onon_my_new_actionbeing declared somewhere. If the on_action is base-declared, OK. If mod-declared, the event-subscribing mod must load after the on-action-declaring mod. Doc 04 doesn't state this dependency rule. - Mod scenario: Mod A declares
on_action on_celebration. Mod B declaresevent CelebrationBonus { trigger on_celebration; ... }. Mod B must load after Mod A.mod.json load_after: [ModA]mechanism is mentioned in doc 06:500 but its interaction with event-trigger resolution is not. - Fix shape: Add to doc 04: "An event's
trigger <name>must resolve to an on-action in the merged declaration set. Unresolved triggers produce a new diagnostic (propose SECS0402):event '<EventName>' subscribes to on-action '<name>' which is not declared in base or any loaded mod."
- Events with
-
RESOLVED 2026-04-30, 04-behavior.md / 06-overrides-and-modding.md — Event condition under mod condition overrides
Conditionlowers to abooloverride. Can mods override just the condition without touching the rest of an event? Doc 06 lists event as a "system, event, class, or any slot within them" override target (line 69), but the per-slot granularity for events isn't spelled out in doc 04 either. Specifically: condition-only override, or full event body replacement?- Mod scenario: Mod wants DemonRaid to fire at Defense < 50
instead of < 30, without changing anything else. Does it write
override DemonRaid { query bool Condition() { return @Settlement.resolve(Defense) < 50; } }? Or the entire event body? - Fix shape landed: Enumerate per-slot override granularity for events
in doc 06:
trigger,frequency,chance,condition,Execute,options[], each option's body. Define what's replaceable and what's whole-event.
-
RESOLVED, 04-behavior.md:470-586 / 06-overrides-and-modding.md:146-147 — Player-choice event option override
- Event options are no longer represented as nested
optionblocks. Current event source registers normal handler methods fromBuildOptions(EventOptions options)withoptions.Add("Name", Handler). - Mod scenario: Base PlagueSpreads has Quarantine and Ignore
handlers. A mod adds a third option by declaring
void Prayer() { ... }and registeringoptions.Add("Prayer", Prayer);fromBuildOptions. - Fix shape landed:
event_build_options_bodyis a full method-body replacement slot; each registered option handler is an independently addressable slot keyed by the explicit option name. New option names are additive. Base-option removal remains deferred and is tracked in04-behavior.md, not by inventing nested option-block syntax.
- Event options are no longer represented as nested
-
DRIFT, 04-behavior.md:543 — Option descriptions localization under mods
- "Description strings for the options do not currently come from
.secssource … hand-written in Generated/". Mod-introduced options inherit this gap — mods must hand-write descriptions in their Generated/ stand-in, which breaks the "mods ship only.secs, compile once, load DLL" model. - Mod scenario: Same as finding 41. Mod author's new Prayer option needs a description. Where does it come from?
- Fix shape: Resolve alongside the localization story (finding
36). Either inline strings in
.secs(tied to localization keys) or out-of-band YAML files.
- "Description strings for the options do not currently come from
-
BLOCKING, 04-behavior.md:567-576 — EventEntry array index identity
EventEntry[] Eventsis a flat array inSecsModule.cs. Under Phase 3 merge, mod events are added to this array. Array position has no semantic meaning (engine indexes byEventId), but theSecsModule.csfile's order of initialization depends on the emit order, which under mods depends on mod load order. Minor diff noise but could affect bootstrap order if any event's side effect depended on registration order.- Mod scenario: Same as finding 21 (template dictionary init). If mod-added events register before base events in the runtime ordering, and any base system fires at tick 0 expecting only base events to be registered, mod side-effects can leak in earlier than expected.
- Fix shape: Spec the array ordering rule: "events are sorted
by
EventIdhash in the finalSecsModule.csoutput, so mod load order does not affect array position. Registration order is therefore hash-deterministic." Or commit to load-order-based appending and spec the consequences.
-
DRIFT, 04-behavior.md:580-581 — No EventSource discriminant
- "There is no
SystemSource-analogEventSourcefield onEventEntry. Host-registered events are not distinguished from SECS-generated ones in the current engine." Matters for mod override dispatch: the engine cannot tell whether a given event isoverride-able or host-protected. - Mod scenario: Mod tries to
override HostOnlyHeartbeat— an event the host registered for its own game logic. Today the registry silently replaces. - Fix shape: Add
EventSourcefield toEventEntry. Same semantics asSystemSource. FUTURE_WORK § 5.18 tracks this.
- "There is no
-
BLOCKING, 04-behavior.md:686-707 — On-action selection mode under mod events
mode all | first_valid | weighted. Consequence: if mod B adds an event subscribing to a base on-action withmode first_validandpriority 999, mod B's event fires first, blocking every base and mod A event.priorityon events is "optional default 0" (line 558); this behaviour is deterministic but gives mods effectively-unbounded priority authority over the selection.- Mod scenario: Mod B's event with
priority 999captureson_building_complete, preventing base celebration from firing. Is this desired behaviour? Under what cases should the engine warn? - Fix shape: Spec the priority convention: "Priority values 0-99 are reserved for base content. Mods should use values 100-999. Higher values reserved for platform/critical systems." Or add a warning when mod event priority exceeds a threshold.
-
BLOCKING, 04-behavior.md:713-738 —
provides scope:Xmod extension- An on-action declares
provides scope:X, scope:Y. A mod's event subscribing to the on-action can reference those scopes. But can a modoverride on_actionto add a new provided scope? If yes, base-authored events that don't provide the new scope at fire time silently omit it; mod-authored events that rely on it getEntityHandle.Null. If the fire site is in a system the mod doesn't own, this silently breaks. - Mod scenario: Mod overrides
on_building_completetoprovides scope:Location, scope:Building, scope:newThing. Mod events rely onscope:newThing. BaseConstructionSystemfireson_building_completeand saves onlyscope:Location,scope:Building— neverscope:newThing. Mod events readEntityHandle.Null. - Fix shape: Make
providesan override-protected slot (not extensible by mods), or require every firing site to be audited whenprovideschanges. The latter is a compile-time check that should exist anyway (FUTURE_WORK § 4 entry onfire on_actioncall-site provides-coverage check).
- An on-action declares
-
DRIFT, 04-behavior.md:931-932 — Chained and fallback on-actions unspecified
- "Chained-on-action syntax in .secs:
chain_to: [other]? Achainkeyword? Not decided. Fallback syntax:fallback: other_on_action? Not decided." Under mods, the question is: can a mod extend a base on-action's chain list? Override it entirely? No syntax means no spec, no diagnostic. - Mod scenario: Mod wants
on_building_completeto chain into a newon_mod_celebration. Syntax:override on_building_complete { chain: [on_mod_celebration]; }? Additive, or replace? - Fix shape: When chain / fallback syntax is committed, spec mod-extension behaviour in parallel.
- "Chained-on-action syntax in .secs:
-
RESOLVED, 04-behavior.md actions / 06-overrides-and-modding.md action slot table — Action mod override scope
- Old audit wording talked about
allow,cost,effect,ai_will_do, andpotentialblocks. That prototype surface is superseded by class-based actions with explicit query/method members. - Current mod scenario: A mod changes
RecruitGarrisoncost logic by replacingquery bool CanStart()and/ormethod void OnStart(ActionRun run), not by editing acost { }block. - Fix shape landed: Doc 06 enumerates action slots:
actor,target,Cooldown,AiTickFrequency,IsVisible,IsTargetValid,CanStart,GetDurationTicks, lifecycle methods, andGetAiWeight.
- Old audit wording talked about
-
RESOLVED, 04-behavior.md actions / 06-overrides-and-modding.md action_is_target_valid — Targeted action candidate validation override
- Old candidate-filter block terminology is superseded by
bool IsTargetValid()on targeted actions. - Current mod scenario: A mod narrows a targeted action by overriding
IsTargetValid; visibility and final start checks remain independent slots (IsVisible,CanStart). - Fix shape landed:
action_is_target_validis a full method-body replacement slot in doc 06. No nested or attribute-based syntax is introduced.
- Old candidate-filter block terminology is superseded by
Doc 05 — Expressions
-
DRIFT, 05-expressions.md:68-103 —
<scope>sigil is mod-declared- For every
scope Name { ... }a mod adds, the compiler generatesname. Good. But when a mod override of a template uses a sigil from another mod, the override site must have both mods loaded. Doc 05 doesn't say this explicitly. - Mod scenario: Mod A adds
scope OrbitalStation. Mod B overridesFarm.OnBuiltto callorbital_station.add_modifier TractorBeam. User loads mod B without mod A. Compile fails — but with what diagnostic? - Fix shape: Add to doc 05: "Sigil resolution runs on the
merged scope set. An
namereference to a scope not in the merged set emits SECS05XX:sigil <name> refers to an undeclared scope." Recommended:SECS0501.
- For every
-
DRIFT, 05-expressions.md:261-356 —
add_modifiercross-mod identityadd_modifier FooBarresolvesFooBarto a modifier id. First checks template-local scoped modifiers, then global. When the modifier is declared by a different mod than the template, the resolution must still work because identities are global FNV hashes. But the error message for "modifier not found" needs to be mod-aware.- Mod scenario: Mod B's template calls
add_modifier ModAFlame. If mod A isn't loaded, compile fails. Diagnostic must mention the missing dependency mod, not just "unknown modifier". - Fix shape: Spec the error form:
SECS0XX: unknown modifier '<name>' — declared in mod '<source>' which is not in the load order(if the compiler can trace provenance) orSECS0XX: unknown modifier '<name>'(if not).
-
BLOCKING, 05-expressions.md:512-554 —
foreach entity in Contractunder mod templates- Iteration over a contract's entities yields every template
registered under that contract. A mod-added template implementing
SettlementContractgets iterated by base systems. If the system's body assumes template-specific state (e.g., checks a template-private channel that only base templates declare), the mod's settlement silently fails the check. Doc 05 doesn't discuss this. - Mod scenario: Mod adds
template<Settlement> FloatingCity. BaseTaxCollectionSystemiterates Settlement contracts, including FloatingCity. The system readsPopulation(host-owned on settlement scope) — works because field is shared. But if FloatingCity has different economics, the system can't differentiate without explicit per-template branching. - Fix shape: Document in doc 05 or doc 06 the invariant: "Systems iterate every template registered under the contract, including mod-added templates. Per-template behavioural differentiation is not supported at the iterator level; use contract-method dispatch or template-specific channels."
- Iteration over a contract's entities yields every template
registered under that contract. A mod-added template implementing
-
DRIFT, 05-expressions.md:624-823 — Query keywords under mod scope-graph extensions
foreach child in parent.Collectionreadshost.GetChildren. A mod that adds a new collection to an existing scope (if allowed — see finding 5) introduces a new children list that the host must support. Same forany / every / count / random / first.- Mod scenario: Mod adds
collection Shrinesonscope settlement.any shrine in settlement.Shrines where …must work. But the host'sGetChildrenimplementation must recogniseH.Shrines. Who wires that? - Fix shape: Same as finding 5 — the scope-extension story must include host-bridge responsibilities. Host bridges may need to be made generic over declared collections, driven by the declaration tables.
Doc 06 — Overrides and Modding
-
BLOCKING, 06-overrides-and-modding.md:94-116 — "What's NOT overridable" is incomplete
- Lists channel declarations and contract definitions. Missing:
EffectMode enum (finding 29), SecsScalarType enum (finding 9), phase
names (finding 37 — under review), event options (finding 41),
provides scope:X(finding 46), method-name-to-slot mapping on contract, tag names, Bare template synthesis rule. - Fix shape: Expand the list to enumerate every closed surface. One line per item. Cross-reference the enforcing diagnostic.
- Lists channel declarations and contract definitions. Missing:
EffectMode enum (finding 29), SecsScalarType enum (finding 9), phase
names (finding 37 — under review), event options (finding 41),
-
RESOLVED (2026-05-04), 06-overrides-and-modding.md:117-152 — FNV-1a-32 vs 64 mod-scale
- Historical finding: doc 06 had resolved the hash-width question in favour of 32-bit to match the Generated/ stand-in, which was inadequate for mod-ecosystem scale.
- Resolution: The new
06-overrides-and-modding.md§ 4.2 commits to FNV-1a-64 with canonical identity strings (valenar:template/farm,mod:phase/x) as hash inputs. § 4.1 introduces typed ID record structs wrappingulong. The old 32-bit resolution is superseded. See finding 12.
-
PARTIALLY RESOLVED (2026-05-04), 06-overrides-and-modding.md:508-564 — Conflict detection is slot-complete?
- "When two or more mods write to the same SECS slot …". The JSON
example shows
template_fieldandmodifier_effect_bundleslot types. What are ALL the slot types the merger produces? Enumeration absent. - Architecture update: The old
override Farm { reads { Fertility, Drought } }syntax is superseded; the new operation form isinject template Farm { channel FoodOutput { ... } }(06 § 3.2). The oldoverride Farm { Methods[...] }form is superseded byinject template Farm { query bool CanBuild() { ... } }(06 § 3.2, § 8.2). - Partial resolution:
06-overrides-and-modding.md§ 8.1–8.8 provides the slot schema for system, template, modifier, event, action, scope, contract, and type declarations. The full enumerated slot table now exists in 06 § 8. The conflict-detection gap (are ALL slots covered in the conflict report JSON?) remains open for slots not yet covered by examples in 06 § 11 (notablyon_action_*,scope_*,contract_*slots). - Remaining fix shape: Verify the merger's conflict report covers every
slot kind in 06 § 8, including
system_phase,system_frequency,system_execute_body, event slots, action slots, scope slots, and contract slots. Thetemplate_*andmodifier_*slots are covered by examples; other declaration kinds need explicit coverage confirmed. - Note on
system_phase/system_frequencysyntax: The slot syntax changed fromphase Y;/frequency Y;tophase = Y;/frequency = Y;per 06 § 6.3 and § 8.1. References to the old semicolon-declaration form in this audit are retained as audit history.
- "When two or more mods write to the same SECS slot …". The JSON
example shows
-
RESOLVED (2026-05-04), 06-overrides-and-modding.md:566-595 — Merge-pass pipeline: no add-extension case
- Historical finding: the pipeline step 3 language assumed
OverrideDeclarationSyntaxand did not spell out additive registration. - Resolution:
06-overrides-and-modding.md§ 3 replaces the singleoverridekeyword with explicit operations. A plain declaration iscreate(step 4 in 06 § 16);inject/replace/try injectetc. are the mod-patching operations. The 7-step pipeline in 06 § 16 now explicitly handles all operations at step 4: "For every source set in resolved order: normal declaration → create; inject → write listed slots; replace → replace whole declaration; try inject → inject if target exists; etc." Additive mod declarations (new templates, new modifiers) are plaincreateoperations that fill empty slots and are covered by 06 § 10.3. Duplicate plain declarations withoutinject/replaceemitSECS0602per 06 § 12.
- Historical finding: the pipeline step 3 language assumed
-
PARTIALLY RESOLVED (2026-05-04), 06-overrides-and-modding.md:591-594 — Hash collision under cross-mod additions
- SECS0601 hash collision case. Covers distinct identifier
strings hashing to the same value. But two mods both declaring
the same identifier is covered by SECS0602. What if two mods both
use
injectorreplaceon the same target? Both target the same slot; last-writer-wins with a conflict report entry. - Architecture update: The old
overridekeyword is superseded byinject/replace/try inject/try replace/inject_or_create/replace_or_create(06 § 3). The case table below is updated accordingly. - Fix shape (updated): Make the cases explicit using new operation terms:
| Case | Mod A decl | Mod B decl | Result |
| same name, base has it, both
inject/replace| legal | legal | conflict report, later wins | | same name, no base, both plaincreate| legal | SECS0602 | error: duplicate | | same name, no base, bothinject/replace| SECS0603 | SECS0603 | error: no target to patch | | different names, same FNV-1a-64 hash | SECS0601 | SECS0601 | error: rename one | TheSECS0603diagnostic already exists in 06 § 12 forinject/replaceon missing targets.
- SECS0601 hash collision case. Covers distinct identifier
strings hashing to the same value. But two mods both declaring
the same identifier is covered by SECS0602. What if two mods both
use
-
DRIFT, 06-overrides-and-modding.md:499-504 — Load order resolution
- "Load order itself is resolved by the launcher ahead of time from
(a) the user's drag-to-reorder list and (b)
mod.jsonload_after/load_beforehints". Cyclic dependencies are possible (ModA load_after ModB,ModB load_after ModA). No resolution policy. - Mod scenario: Two mods declare mutual
load_after. Launcher cannot compute a linear order. Merger either aborts, breaks the cycle arbitrarily, or fails. - Fix shape: Spec: "If
load_after/load_beforehints form a cycle, the launcher emits amod.jsonvalidation error before invoking the merger. The merger assumes a valid linear order."
- "Load order itself is resolved by the launcher ahead of time from
(a) the user's drag-to-reorder list and (b)
-
DRIFT, 06-overrides-and-modding.md:595 — Base as load order 0 semantics
- "Special case: base is implicitly a 'mod' at load order 0." Clean design. But what about the case where a mod shadows the base by loading at order 0? Explicitly disallowed? Or is it allowed and produces override-like behaviour?
- Fix shape: State: "Load order 0 is reserved for the base game. Mods load at order ≥ 1. A mod that claims order 0 is a manifest error."
FUTURE_WORK
-
DRIFT, FUTURE_WORK.md:5-10 — "Not a plan" metadata and mod-item admission
- How this doc is maintained. Every future-work item is derived from a design-doc open question or audit finding. No item explicitly references the mod-coverage audit class. The FUTURE_WORK file's § 5 design topics section does not list mod-coverage items as a class.
- Fix shape: Add a new section § 7 or extend § 5 with a "mod-coverage" topic cluster: findings from this audit (or future audits) land there with the same citation / blocks / notes schema.
-
RESOLVED, FUTURE_WORK.md:105-126 — 3.2 Modifier lifecycle leaks and mod inheritance
- Resolved 2026-04-26 by owner/target lifecycle cleanup. The old
12-template table is historical;
remove_modifieris no longer a prerequisite for ordinary destruction cleanup.
- Resolved 2026-04-26 by owner/target lifecycle cleanup. The old
12-template table is historical;
-
RESOLVED, FUTURE_WORK.md:372-397 — 5.1 Modifier lifecycle pattern commitment
- Resolved 2026-04-26. The chosen pattern is engine cleanup keyed by binding owner/target, which is the mod-friendly option because partial overrides of attach sites do not need to maintain a second teardown site.
-
DRIFT, FUTURE_WORK.md:425-440 — 5.4 System declaration defaults and mod audit
- "Should
frequencybe required on every system?" / "auto-distribute foreach?". Under mod-added systems, answers matter: a careless mod-added system withoutfrequencycould flood the tick loop. The design topic should flag this. - Fix shape: Add to the topic: "Mods load untrusted system
code. Defaults that fail-safe under mod use (required
frequency, auto-distribute on) should weight higher than defaults that fail-noisy only for base."
- "Should
Cross-cutting patterns
Three themes recur across the 64 findings, each affecting 10+ items:
Pattern A — "Rule is base-scoped, not mod-scoped" (29 findings: 1, 2, 8, 10, 11, 13, 14, 18, 19, 21, 22, 24, 25, 27, 28, 30, 31, 32, 33, 34, 36, 38, 40, 42, 44, 50, 51, 52, 53). The largest class. Every rule written for a single source set needs re-qualification under the base+mod union. The spec's authors reasoned through rules against "base content", not "base + N mods". Every diagnostic, every invariant, every uniqueness rule, every ordering rule has to be re-read with the mod question. The fix is uniformly a one-paragraph "under mods" clarification, not a structural change.
Pattern B — "Closed enum / fixed surface, not stated" (7 findings: 9, 29, 37, 38, 41, 46, 54). EffectMode, SecsScalarType, phase names, SystemSource, event options, provides scope:X, Bare template synthesis — all are closed or semi-closed. Some are now documented (event options, Bare templates); the remaining rows still need explicit closed-surface declarations so mods do not get silent shadowing, compile errors without useful codes, or runtime errors.
Pattern C — "Identifier hash space is sized for base, not ecosystem" (5 findings: 12, 15, 16, 55, plus the registry-collision case 17). RESOLVED (2026-05-04): findings 12, 15, and 55 are resolved by 06-overrides-and-modding.md § 4.1–4.2, which commits to FNV-1a-64 hashing on canonical identity strings and typed ID record structs wrapping ulong. Finding 16 (hand-picked hash sentinels) remains open. Finding 17 (runtime registry silent-overwrite) remains open pending the compile-time-only modding commitment.
Minor secondary patterns:
- Override slot enumeration is incomplete (findings 27, 32, 46, 56). Doc 06 now covers template metadata, event condition/options, and class-based activity slots. Remaining gaps are narrower: mod-added contract methods, template-scoped modifiers, on-action
provides, and conflict-detection slot coverage. - Mod-manifest / load-order interactions are hand-waved (findings 17, 18, 31, 39, 59, 60).
mod.json, dependency resolution, cycle detection, load-order 0 shadow rules — these are mentioned in passing but not spelled out.
Items that are correctly mod-aware
Proof that the doc set isn't uniformly weak — several places explicitly think through mods:
- Doc 06 § 3 (operation model) — Explicit
inject/replace/try inject/try replace/inject_or_create/replace_or_createoperations replace the singleoverridekeyword. Each operation has a clear error code for misuse (SECS0602 for duplicate create, SECS0603 for inject/replace on missing target). - Doc 06 § 4.1 (typed IDs) — Every declaration kind has a typed record struct (
TemplateId,ModifierId,ChannelId, etc.) wrappingulong, preventing accidental ID mixing across kinds. - Doc 06 § 4.2 (canonical identity) — Canonical identity strings (
valenar:template/farm) are hashed via FNV-1a-64, preventing mod collision on common names. - Doc 06 § 8.1–8.8 (slot schema) — Full slot schema for all declaration kinds with slot names, syntax, and merge rules enumerated.
- Doc 06:117-152 — Identity scheme (FNV-1a) is well-thought-through for mod reuse: "a mod's override of
Marketmust map to the base'sMarketslot regardless of file path". - Doc 06:472-504 — Load order / last-writer-wins is clearly stated with examples showing mod A + mod B both overriding the same slot.
- Doc 06:508-564 — Conflict report JSON shape is mod-first: every field (modId, loadOrder, sourcePath, sourceLine, winner) is mod-aware by construction.
- Doc 06:566-595 — Merge-pass pipeline is cleanly parameterized over mods (step 1 parses all source sets independently; step 6 re-merges on reorder cheaply).
- Doc 03:625-702 — Template-scoped modifier identity
hash("Template.Modifier")correctly prevents cross-mod name clashes at the template level. - Doc 01:765-774 —
SECS0107andSECS0108spec case-collision and cross-assembly redeclaration with mod scenarios explicitly walked through. - Doc 04:195-213 —
SystemSource.SecsvsSystemSource.Hostcorrectly distinguishes mod-overridable from host-protected systems. - Doc 04:344-411 — Event trigger types separate pulse (scheduler-driven) from on-action (mod-extensible).
- ADR csharp-fork-decision.md — Roslyn-fork rationale explicitly ties the decision to mod-merge capability ("AST-level mod merging" at line 74, 395-414).
- Glossary entry 00:83 correctly describes override as mod-level; the gap is that it's the only mod concept in the glossary (see finding 1).
The positive coverage is strongest in doc 06 (unsurprising — that's its job) and weakest in docs 01, 02, 04 (which were written as lowering contracts for base content and only glanced at mods at the margin).
Recommended follow-up commits
Ordered by blast radius — smallest first, so each commit can land independently:
-
Close the glossary gap (finding 1). One entry in doc 00. Cite the five mod capabilities. 5 minutes.
-
Add "closed surface" declarations (findings 9, 29, 37, 38, 46). One sentence each in the relevant doc stating "mods cannot extend this surface". Touches docs 01, 03, 04. 30 minutes.
-
Expand
SECS0XXdiagnostic specs with mod cases (findings 2, 10, 13, 14, 50, 51). Add a "Fires across source sets?" column or note per diagnostic. Docs 01, 02, 03, 04, 05. 1 hour. -
Finish override slot granularity for remaining declaration kinds (findings 27, 32, 46, 56). Doc 06 already covers template metadata, event condition/options, and class-based activity slots.
-
RESOLVED (2026-05-04) — FNV-1a-64 for identity space (findings 12, 15, 55). The design commitment is in 06-overrides-and-modding.md § 4.1–4.2. Remaining action: update Generated/ stand-ins where 32-bit hashes persist as historical artifacts. Finding 16 (hand-picked sentinels) is still open.
-
Commit
walks_tostorage design + scope / contract extension rules (findings 4, 5, 6, 7, 8). Affects 01 and 06 substantially. Half a day writing, longer with stakeholder review. -
Resolved 2026-04-26: modifier lifecycle pattern (findings 19, 22, 62, 63 + FUTURE_WORK 5.1). Owner/target cleanup is the committed pattern; no open action remains for ordinary template-owned modifier lifetime.
-
Commit
on_actionscope lifecycle rules under mods (findings 45, 46, 47). Affects doc 04 and doc 06. 2-3 hours. -
Commit mod.json / load-order / dependency resolution story (findings 17, 18, 31, 39, 59, 60). New document or large section in doc 06. Half a day.
-
Commit remaining event modding details (findings 42, 44). Event option slots are now method-based and documented; option descriptions/localization and
EventSourceremain. -
Commit scope-method and collection mod-extension rules (findings 7, 53, FUTURE_WORK 5.14). One section in doc 01 + one in doc 06. Three hours.
-
Commit
provides scope:Xextensibility and mod-cross-firing validation (finding 46). New compile-time check; one paragraph spec + one implementation. Half a day. -
Commit tag / localization / description mod stories (findings 31, 36, 42). Touches docs 03, 04 and a new mod-localization doc or section. Half a day.
-
Commit HardOverride tie-breaker policy (finding 35). One paragraph. 15 minutes.
-
Commit phase-extension story for host-capable expansions (finding 37). The new 06 § 9.1–9.2 settles that top-level phase declarations are plain C#, not SECS surface. The remaining open piece is the
mod.json phase_ordermechanism for host-capable expansions. Either reject or spec it. Two hours. -
Add mod-coverage items to FUTURE_WORK (finding 61). Mechanical. 30 minutes.
-
Cross-reference the audit findings into each doc's open-questions section (all findings). Mechanical cleanup. Hour.
Commits 1-4 are pure documentation polish; 5-8 require design decisions but are scoped; 9-17 depend on commits earlier in the list or on separately-tracked work (compiler Phase 3 implementation). The hash-width decision (finding 12) was resolved by 06-overrides-and-modding.md § 4 (2026-05-04); the remaining blocker for mod-ecosystem concerns is the walks_to / scope-extension story (findings 4, 5) and the mod-manifest dependency resolution story (finding 9).