Skip to main content

Mod-coverage audit — design docs

Historical note (2026-04-25): findings about FNV-1a-32 / uint identifier surfaces were implemented by the FNV-1a-64 ulong migration 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: ParentScopeIds replaced the old single-parent shape, location walks_to settlement is declared, and template build costs are field values 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_to edges, 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 OnDestroyed removal 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.md and the method-level action slot rows in 06-overrides-and-modding.md. The old allow / effect / ai_will_do block 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-level phase, tag, and tick_rate declarations are no longer SECS surface — they are plain C# (06 § 9). The system-body slot syntax changed from phase Y; / frequency Y; to assignment form phase = Y; / frequency = Y; (06 § 6 and § 8.1). Older wording here treated a prev_tick helper as a plain C# ctx.PrevTick call (06 § 9.5). Superseded live contract: previous-tick access uses track_prev = true plus host/runtime ReadPrevTick* bridge reads. Typed ID record structs (PhaseId, TagId, ChannelId, TemplateId, ContractId, ScopeId, SystemId, EventId, ActionId, OnActionId, TickRateId, ModifierId) replace raw ulong (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 old override keyword 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

  1. 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 no override. 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 that override uses." Cite doc 06.
  2. 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 declares Gold and a mod declares gold?
    • Mod scenario: Mod author names a modifier Fortified in PascalCase, not knowing base already declared fortified. Both hash to the same H.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.
  3. 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

  1. BLOCKING, 01-world-shape.md:117-120walks_to design decision unresolved under mods

    • The doc offers two designs for walks_to storage: design (1) "single parent
      • transitive walks" vs design (2) "multi-edge declaration". The choice is explicitly undecided. A mod that adds walks_to Settlement; on scope location has radically different semantics under the two designs. Mod-ready spec cannot defer this.
    • Mod scenario: Base declares scope Location { walks_to Province; }. Mod A declares (via override? via extension? — also ambiguous) an additional walks_to Settlement;. Under design (1) this edge may have to be computed transitively; under design (2) it is a new row in a ParentScopeIds array. 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_to edges on existing scopes and they are merged additively. Third-party data-only mods cannot add walks_to edges because these require host support (SECS0609). The inject scope Location { walks_to Settlement; } form is reserved for host-capable expansions only.
    • Fix shape (updated): Commit to multi-edge ParentScopeIds storage (design 2) as the 06 § 8.6 intent implies. Specify that inject scope X { walks_to Y; } is the syntax for host-capable expansions, and that third-party mods attempting it receive SECS0609.
  2. BLOCKING, 01-world-shape.md:82-84, 150-153 — Scope-extension semantics unspecified

    • The doc documents scope declarations and collection declarations but nowhere says whether a mod can extend an existing base scope. Doc 06 § 8.6 covers scope slots (additive walks_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 scope walks_to additions are additive (not override). Third-party mods cannot add new scope fields, collections, or walks_to edges because these require host support. The old override scope Settlement { int Piety; } syntax is superseded; the new form for host-capable expansions would be inject scope Settlement { int Piety; } (pending full scope-inject spec in doc 01).
    • Mod scenario: Mod adds a new channel Piety whose source is settlement.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.
  3. RESOLVED 2026-04-26, 01-world-shape.md:111-120 — single-parent scope storage was not mod-composable

    • Historical finding: ScopeDeclaration had one parent slot, so two mods adding different walk edges could not both be represented.
    • Current state: the live contract uses ScopeDeclaration.ParentScopeIds and the docs commit to additive walks_to edges merged into that array. location walks_to Settlement is now declared instead of being a host-only fallback.
  4. 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.
  5. 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 override root_scope or method lists. But mods adding new methods to existing contracts (e.g., adding OnRaided to Building) 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 OnEnchanted for 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.
  6. BLOCKING, 01-world-shape.md:362-405SecsScalarType enum 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 decimal to SecsScalarType; 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. A channel <non-listed-type> declaration in a mod emits a binder diagnostic (proposed SECS01XX)."
  7. BLOCKING, 01-world-shape.md:456-476kind = 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 no kind =. SECS0101 fires. Good — but the doc doesn't say so explicitly, and the reverse case (for example, a legacy channel-replacement attempt targeting Population) isn't covered either (doc 06 says channel declarations are not overridable, so this case should be SECS0602 or a new code).
    • Fix shape: Clarify that SECS0101 fires 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").
  8. DRIFT, 01-world-shape.md:437-438Internal = false on user channels is base-only

    • ChannelDeclaration.Internal flag is implied to be compiler-internal. Doc mentions "an Internal ChannelKind for purely compiler-internal channels remains a potential future addition". Whether mods' declared channels always have Internal = false is not stated — and whether a mod could declare an Internal = true channel 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. The Internal slot is reserved for compiler-synthesised channels and is not reachable via .secs surface in either source set."
  9. 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 form valenar:template/farm as 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.) wrapping ulong, eliminating raw uint from 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.
  10. 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 declares Gold and base already has gold, 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."
  11. 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 Fortified and mod B also declares Fortified (neither with override), SECS0108 fires on both? Or one? What if mod A declares and mod B uses override? Case table is incomplete.
    • Mod scenario: Two mods independently introduce a modifier Fortified { … }. Neither uses override. 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 override on a name in another mod (not base) → well-defined; last mod in load order wins. Spec all three.
  12. RESOLVED (2026-05-04), 01-world-shape.md:835-836uint vs ulong for 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 a readonly record struct wrapping ulong. Raw ulong is no longer the public ID surface. The typed wrappers prevent accidental mixing of ChannelId and TagId even though both wrap ulong.
    • Remaining action: Update Generated/ stand-ins where uint hashes are still present as historical artifacts.
  13. DRIFT, 01-world-shape.md:820-821 — Hand-picked hash sentinels block mod detection

    • "Some identifiers in Hashes.cs use 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-picked BuildOrder sentinel. 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.
  14. 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 explicit override marker 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.
  15. 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 SourceScopeId referenced in a channel declaration must correspond to an existing ScopeDeclaration" — when a mod declares a channel whose source = ... 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.json per doc 06) declare its dependencies, and does the merger validate them before merging?

Doc 02 — Templates

  1. RESOLVED, 02-templates.md:501 — Modifier auto-detach under mod overrides

    • Resolved 2026-04-26. The committed pattern is owner/target cleanup: owner is lifetime source, target is effect anchor, and template destruction removes bindings owned by the destroyed instance. A mod that overrides OnBuilt and attaches a different modifier still creates a binding owned by the same instance; ordinary destruction cleanup removes it without any inherited OnDestroyed remove site.
  2. 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> MegaFarm with a dynamic FoodOutput) emits H.Formula_MegaFarmFoodOutput. If the mod and base both contribute to FoodOutput, 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 into RegisterFormulaContribution(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 become Formula_{ModifierName}{ChannelName} by extension. Clarify that mod-added templates follow the same naming.
  3. BLOCKING, 02-templates.md:488-495 — Template-method dictionary-init syntax

    • Dictionary init for TemplateEntry.Methods varies 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.
  4. RESOLVED 2026-04-26, 02-templates.md:416CanBuild permissive fallback on mod walks

    • Historical finding: Farm's hand-written body returned true when the settlement walk failed.
    • Current state: source and generated queries require the declared location -> settlement walk and return false if the runtime relationship is absent. Undeclared walks are SECS0111.
    • Historical mod scenario: Base Farm CanBuild walks to Settlement and returns true if walk fails. Mod adds walks_to Settlement on scope Location. The walk now succeeds, Stage >= 1 check 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.
  5. 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 BareSettlement to change defaults (BareLocation has Slots = 4).
    • Mod scenario: Mod adds scope OrbitalStation with walks_to province. Compiler must emit BareOrbitalStation. Does the compiler auto-generate, or does the mod author need to declare an explicit Bare template? Today the Valenar MainCharacter .secs declares template<Character> BareCharacter explicitly (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 the inject operation from 06 § 3.2). Cross-reference from doc 06.
  6. DRIFT, 02-templates.md:785-789name = "X" string pool under mods

    • name = "Hero" on a template is template metadata, not localizable. Two mods that both set name = "Champion" on different templates cause no clash because TemplateEntry.Name is stored per-template. But if a mod override sets name = "Zero" on base's MainCharacter (a pattern overriding this metadata is explicitly in doc 06's slot list — line 104 "template method override" — but name = 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_metadata as an injectable per-field slot in 06 § 8.2 (it appears in the slot table as name = "Farm"; with merge rule "per-field replacement"). Specify what happens if a mod sets an empty string. The inject operation replaces only the name slot, leaving all other template slots intact.
  7. DRIFT, 02-templates.md:642-693reads { } 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_FarmFood reads { Fertility }. Mod's override body adds resolve(Blight). Merged formula must register with readsChannels: [ Fertility, Blight ]. But the registry.RegisterFormula call lives in the central SecsModule.cs which 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 corresponding RegisterFormula call in SecsModule.cs." Otherwise the cycle detector operates on stale dependency metadata.
  8. BLOCKING, 02-templates.md:694-727upgrade block and mod override

    • upgrade is NOT YET IMPLEMENTED. Once it lands, the override story is non-trivial: can a mod override the upgrade target (upgrade TerracedFarm becomes upgrade MegaFarm) or the query binding independently? Resource values should remain template field slots rather than an action-style template cost block.
    • 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 upgrade lands, add it to doc 06's override slot table with slot granularity specified (whole block vs per-field).
  9. DRIFT, 02-templates.md:1080-1085TemplateEntry init-field mod rewriting

    • Canonical init-field order is specified. A mod override that changes GetField output 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 the Methods dictionary init. If the contract doesn't declare OnRaided, this is a SECS0XXX error (which doesn't exist). The doc is silent.
    • Mod scenario: Mod uses inject template Farm { method void OnRaided() { ... } } — a method the base contract's methods list 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 methods declaration. 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).

Doc 03 — Channels and Modifiers

  1. BLOCKING, 03-channels-and-modifiers.md:62-103 — Modifier declarations: duplicate across mods

    • Modifier declarations produce ModifierDeclaration entries indexed by ModifierId. Doc 06 specifies effect-bundle overrides are full-replacement. But two mods adding the same modifier name (without override) 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 adds modifier Blessed { … }, different effects. Without explicit override, 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.
  2. DRIFT, 03-channels-and-modifiers.md:105-169 — EffectMode enum is fixed

    • EffectMode is 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 <= 50 to 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."
  3. 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 attach HouseMoralePresence as well. Under max_stacks = 100 this is fine, but if both mods think they own the cap, clashes can occur. Also: if a mod overrides HouseMoralePresence to have max_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, or reapply on 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.
  4. 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), and SECS-Compiler-Plan.md Phase 2 (the tag-binding bullet: bind plain C# TagId values, reject top-level tag <Name>;, emit no TagDeclaration[], register the collected catalog, and report undeclared symbolic references as SECS0212).
    • Current rule: A mod may add tags only by declaring TagId constants with canonical id strings. tags = ..., has_tag ..., activity tag selectors, and bulk modifier tag helpers bind against the merged declared catalog. Generated output emits RegisterTag calls for that catalog; generated H.Tag_* values are mirrors of TagId.Value, not independent lowercase-name hashes.
  5. 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 in Declarations.Modifiers[] same as global modifiers. Under Phase 3 override, if a mod overrides override Barracks.VeteranGarrison { Garrison += 20; }, the merger rewrites the per-slot ModifierDeclaration. 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") vs hash("ModB.Foo.Modifier") — but template names are already mod-qualified by FNV (so hash("ModA.Foo")hash("ModB.Foo")). Actually, there is no explicit mod-namespacing on template names in the spec; two mods each declare template<Building> Foo and both are hash("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.Alertness collides 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."
  6. BLOCKING, 03-channels-and-modifiers.md:873-941 — reads { } D-Hybrid under mod formulas

    • D-Hybrid dynamic resolve(<expr>) requires explicit reads { }. A mod that adds a new dynamic formula on an existing channel (e.g., adds a new modifier with dynamic effect) needs to declare reads per 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 ValidateDependencies implicitly but never spells out that mod overrides can introduce new graph edges. Add: "ValidateDependencies runs 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)."
  7. 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 stale reads { } 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_FarmFood body to add resolve(Irrigation) but doesn't update reads { }. Compiler auto-extraction catches the static literal (good), but if the new call is dynamic (resolve(Irrigation) inside a runtime switch), SECS0302 fires. 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-readsets CLI flag, analogous to --strict-conflicts (doc 06:560), runs the read-set validation in release builds to catch mod drift."
  8. 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 HardOverride on the same channel, the winner is the mod whose binding was registered last. Which mod that is depends on the engine's order of applying AddModifier commands 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 StarvationCap HardOverrides FoodCapacity = 10. Mod B's AbundanceCap HardOverrides FoodCapacity = 100. Both bindings are active on the same settlement. Runtime order depends on which system ran AddModifier first, 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).
  9. DRIFT, 03-channels-and-modifiers.md:621-623translation localization under mods

    • translation = "..." 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 YAML mods/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

  1. 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 no phase X { ... } SECS keyword. Third-party data-only mods should not add arbitrary new phases because scheduler ordering is host-owned; the permitted mod operation is inject system ExistingSystem { phase = Phases.SomeExistingPhase; } to reassign a system to an existing phase. The system_phase slot is still overridable via inject system X { phase = Y; }.
    • Remaining gap: The mod.json phase_order mechanism 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 the mod.json phase_order mechanism when it is implemented (06 § 9.2).
  2. DRIFT, 04-behavior.md:209-213 — SystemSource mod dispatch

    • SystemSource.Secs is mod-overridable; SystemSource.Host is not. Mod-declared systems are SystemSource.Secs by default. But whether a mod can declare a SystemSource.Host system (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.Host available? Or is override-protected status only achievable through host code?
    • Fix shape: State: "Mods declare only SystemSource.Secs systems. Host systems are defined in host code and are not expressible in .secs regardless of source set."
  3. BLOCKING, 04-behavior.md:344-411 — Event trigger type: mods adding new on_actions

    • Events with trigger on_my_new_action depend on on_my_new_action being 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 declares event 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."
  4. RESOLVED 2026-04-30, 04-behavior.md / 06-overrides-and-modding.md — Event condition under mod condition overrides

    • Condition lowers to a bool override. 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.
  5. 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 option blocks. Current event source registers normal handler methods from BuildOptions(EventOptions options) with options.Add("Name", Handler).
    • Mod scenario: Base PlagueSpreads has Quarantine and Ignore handlers. A mod adds a third option by declaring void Prayer() { ... } and registering options.Add("Prayer", Prayer); from BuildOptions.
    • Fix shape landed: event_build_options_body is 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 in 04-behavior.md, not by inventing nested option-block syntax.
  6. DRIFT, 04-behavior.md:543 — Option descriptions localization under mods

    • "Description strings for the options do not currently come from .secs source … 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.
  7. BLOCKING, 04-behavior.md:567-576 — EventEntry array index identity

    • EventEntry[] Events is a flat array in SecsModule.cs. Under Phase 3 merge, mod events are added to this array. Array position has no semantic meaning (engine indexes by EventId), but the SecsModule.cs file'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 EventId hash in the final SecsModule.cs output, 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.
  8. DRIFT, 04-behavior.md:580-581 — No EventSource discriminant

    • "There is no SystemSource-analog EventSource field on EventEntry. 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 is override-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 EventSource field to EventEntry. Same semantics as SystemSource. FUTURE_WORK § 5.18 tracks this.
  9. 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 with mode first_valid and priority 999, mod B's event fires first, blocking every base and mod A event. priority on 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 999 captures on_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.
  10. BLOCKING, 04-behavior.md:713-738provides scope:X mod 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 mod override on_action to 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 get EntityHandle.Null. If the fire site is in a system the mod doesn't own, this silently breaks.
    • Mod scenario: Mod overrides on_building_complete to provides scope:Location, scope:Building, scope:newThing. Mod events rely on scope:newThing. Base ConstructionSystem fires on_building_complete and saves only scope:Location, scope:Building — never scope:newThing. Mod events read EntityHandle.Null.
    • Fix shape: Make provides an override-protected slot (not extensible by mods), or require every firing site to be audited when provides changes. The latter is a compile-time check that should exist anyway (FUTURE_WORK § 4 entry on fire on_action call-site provides-coverage check).
  11. DRIFT, 04-behavior.md:931-932 — Chained and fallback on-actions unspecified

    • "Chained-on-action syntax in .secs: chain_to: [other]? A chain keyword? 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_complete to chain into a new on_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.
  12. 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, and potential blocks. That prototype surface is superseded by class-based actions with explicit query/method members.
    • Current mod scenario: A mod changes RecruitGarrison cost logic by replacing query bool CanStart() and/or method void OnStart(ActionRun run), not by editing a cost { } block.
    • Fix shape landed: Doc 06 enumerates action slots: actor, target, Cooldown, AiTickFrequency, IsVisible, IsTargetValid, CanStart, GetDurationTicks, lifecycle methods, and GetAiWeight.
  13. 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_valid is a full method-body replacement slot in doc 06. No nested or attribute-based syntax is introduced.

Doc 05 — Expressions

  1. DRIFT, 05-expressions.md:68-103<scope> sigil is mod-declared

    • For every scope Name { ... } a mod adds, the compiler generates name. 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 overrides Farm.OnBuilt to call orbital_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 name reference to a scope not in the merged set emits SECS05XX: sigil <name> refers to an undeclared scope." Recommended: SECS0501.
  2. DRIFT, 05-expressions.md:261-356add_modifier cross-mod identity

    • add_modifier FooBar resolves FooBar to 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) or SECS0XX: unknown modifier '<name>' (if not).
  3. BLOCKING, 05-expressions.md:512-554foreach entity in Contract under mod templates

    • Iteration over a contract's entities yields every template registered under that contract. A mod-added template implementing SettlementContract gets 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. Base TaxCollectionSystem iterates Settlement contracts, including FloatingCity. The system reads Population (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."
  4. DRIFT, 05-expressions.md:624-823 — Query keywords under mod scope-graph extensions

    • foreach child in parent.Collection reads host.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 for any / every / count / random / first.
    • Mod scenario: Mod adds collection Shrines on scope settlement. any shrine in settlement.Shrines where … must work. But the host's GetChildren implementation must recognise H.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

  1. 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.
  2. 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 wrapping ulong. The old 32-bit resolution is superseded. See finding 12.
  3. 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_field and modifier_effect_bundle slot 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 is inject template Farm { channel FoodOutput { ... } } (06 § 3.2). The old override Farm { Methods[...] } form is superseded by inject 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 (notably on_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. The template_* and modifier_* slots are covered by examples; other declaration kinds need explicit coverage confirmed.
    • Note on system_phase / system_frequency syntax: The slot syntax changed from phase Y; / frequency Y; to phase = 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.
  4. 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 OverrideDeclarationSyntax and did not spell out additive registration.
    • Resolution: 06-overrides-and-modding.md § 3 replaces the single override keyword with explicit operations. A plain declaration is create (step 4 in 06 § 16); inject / replace / try inject etc. 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 plain create operations that fill empty slots and are covered by 06 § 10.3. Duplicate plain declarations without inject/replace emit SECS0602 per 06 § 12.
  5. 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 inject or replace on the same target? Both target the same slot; last-writer-wins with a conflict report entry.
    • Architecture update: The old override keyword is superseded by inject / 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 plain create | legal | SECS0602 | error: duplicate | | same name, no base, both inject/replace | SECS0603 | SECS0603 | error: no target to patch | | different names, same FNV-1a-64 hash | SECS0601 | SECS0601 | error: rename one | The SECS0603 diagnostic already exists in 06 § 12 for inject/replace on missing targets.
  6. 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.json load_after/load_before hints". 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_before hints form a cycle, the launcher emits a mod.json validation error before invoking the merger. The merger assumes a valid linear order."
  7. 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

  1. 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.
  2. 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_modifier is no longer a prerequisite for ordinary destruction cleanup.
  3. 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.
  4. DRIFT, FUTURE_WORK.md:425-440 — 5.4 System declaration defaults and mod audit

    • "Should frequency be required on every system?" / "auto-distribute foreach?". Under mod-added systems, answers matter: a careless mod-added system without frequency could 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."

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_create operations replace the single override keyword. 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.) wrapping ulong, 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 Market must map to the base's Market slot 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-774SECS0107 and SECS0108 spec case-collision and cross-assembly redeclaration with mod scenarios explicitly walked through.
  • Doc 04:195-213SystemSource.Secs vs SystemSource.Host correctly 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).

Ordered by blast radius — smallest first, so each commit can land independently:

  1. Close the glossary gap (finding 1). One entry in doc 00. Cite the five mod capabilities. 5 minutes.

  2. 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.

  3. Expand SECS0XX diagnostic 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.

  4. 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.

  5. 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.

  6. Commit walks_to storage design + scope / contract extension rules (findings 4, 5, 6, 7, 8). Affects 01 and 06 substantially. Half a day writing, longer with stakeholder review.

  7. 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.

  8. Commit on_action scope lifecycle rules under mods (findings 45, 46, 47). Affects doc 04 and doc 06. 2-3 hours.

  9. 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.

  10. Commit remaining event modding details (findings 42, 44). Event option slots are now method-based and documented; option descriptions/localization and EventSource remain.

  11. 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.

  12. Commit provides scope:X extensibility and mod-cross-firing validation (finding 46). New compile-time check; one paragraph spec + one implementation. Half a day.

  13. 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.

  14. Commit HardOverride tie-breaker policy (finding 35). One paragraph. 15 minutes.

  15. 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_order mechanism for host-capable expansions. Either reject or spec it. Two hours.

  16. Add mod-coverage items to FUTURE_WORK (finding 61). Mechanical. 30 minutes.

  17. 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).