Skip to main content

10 — Host <-> SECS execution boundary

This doc classifies every concept in the SECS world into one of three ownership buckets — Host-owned, SECS-owned, or Shared/bridged — and records the runtime entry points by which the two sides call into each other. It is a cross-cutting reference that complements rather than replaces docs 01-09:

  • 01-world-shape.md defines what scopes, fields, and contracts are.
  • 04-behavior.md defines what activities, policies, and events do.
  • 05-expressions.md defines what queries, scope methods, and types look like at the call site.
  • 06-overrides-and-modding.md defines what mods may and may not add.
  • This doc names who owns each piece of state and code, which assembly each type lives in, and which interfaces gate the Host <-> SECS handoff.

Boundary discipline matters because SECS targets netstandard2.1 for Unity consumers; the engine assembly compiles into a host-game DLL alongside hand-written Generated/*.cs and the host's own runtime, and the lines between those layers must stay sharp.

1. Purpose and scope

This doc answers four questions:

  1. What does the host own? (data, rendering, networking, save-file, platform services)
  2. What does SECS own? (declared content, generated behavior surfaces, channel resolution, command pipeline, mod merge)
  3. What is shared at the bridge? (typed ids, contexts, command buffers, the host-bridge interfaces themselves)
  4. Where do the assembly lines fall today and where do they need to move?

Out of scope for this doc:

  • Per-call-site invocation legality (which surfaces may start an activity, fire an event, enqueue a command). That detail lives in the invocation matrix below.
  • Read-only vs command-producing hardening beyond the committed boundary in section 13.
  • Save payload wire format. Section 20.2 separates today's partial restore primitives from a future unified payload candidate; the host still owns bytes and container format.

2. The execution model in two paragraphs

A SECS host instantiates one SecsRegistry, calls every generated SecsModule.Initialize(registry) once at boot, calls registry.FinalizeModRegistration() after all mods are loaded, and then runs a tick loop. On each tick, the host advances the world (input, networking, AI scheduling) and at chosen phase boundaries calls into SECS to resolve channels, run systems, dispatch events, evaluate policies, and progress activities. SECS commands flow back to the host through ISecsHostCommands and ISecsHostWrites; SECS reads of host-owned data flow forward through ISecsHostReads.

In implementation terms: the host references both SECS.Abstractions (the public bridge contract) and SECS.Engine (the runtime that actually executes the contract). SECS-authored content (Content/*.secs) lowers to plain C# in the Generated/ tree, which compiles into the host DLL. Sub-agents, modders, and the future SECS compiler must respect this two-layer split: anything that crosses the line between SECS.Engine and host code must be expressed as a method on one of the ISecsHost* interfaces or as a registered runtime callback.

3. Three-bucket classification

Every concept below is assigned exactly one bucket. Items marked DRIFT are committed but the assembly placement does not match the "shared" intent — the table notes whether that placement is intentional.

3.1 Host-owned

ItemEvidenceNotes
Entity / world physical storageexamples/valenar/Host/GameWorld.csThe host owns every Dictionary<long, T> and array of game data; SECS engine never holds entity state physically.
Scope field storage01-world-shape.md:36, 01-world-shape.md:101, 05-expressions.md:184Scope fields are host-owned data. The engine reads them via ISecsHostReads.ReadInt/Long/Float/Double/Bool/String/Enum/... and writes the resolved-channel dirty set back via ISecsHostWrites.
Scope graph relationship storageISecsHostReads.WalkScope (src/SECS.Abstractions/Interfaces/ISecsHostReads.cs)The host stores parent / child / sibling relationships; engine asks for them by walking declared walks_to paths.
Scoped collection backing containers08-collections-and-propagation.md:41 ("host-side iteration")Aggregate channels and policy selectors enumerate via the bridge; engine never owns the backing list.
Host bridge implementationexamples/valenar/Host/Bridge/HostBridge.cs:10 (class HostBridge : ISecsHostReads, ISecsHostWrites, ISecsHostCommands)The class that implements the interfaces lives in host code.
Rendering / UI / inputexamples/valenar/Server/, examples/valenar/Client/Implicit by structure. No SECS type imports rendering or input. (Not committed as an explicit rule — repo structure alone establishes it.)
Networking / server authorityexamples/valenar/Server/ (SignalR hub)Implicit by structure; no SECS type references server APIs.
Save-file container(not committed in design docs)The host owns the save file format and serialization driver; SECS contributes the current partial runtime payload objects and may later contribute a unified payload. See § 20.2.
Platform services(not committed in design docs)OS / file / network / clock are host-owned by convention.
Host-only systems04-behavior.md:66, 04-behavior.md:221Systems registered with SystemSource.Host are excluded from the SECS merge pass. The host runs them itself.
Host boot / new-game orchestrationexamples/valenar/Host/GameRuntime.cs:77 (GameRuntime.Create())The host code orchestrates engine boot; the engine exposes SecsRegistry.Register* and FinalizeModRegistration as the entry points.

3.2 SECS-owned

ItemEvidenceNotes
Declared contentexamples/valenar/Content/**/*.secs, docs/design/README.md:29Designer-facing source files. Today they are authored by hand in plain text mirroring committed SECS keywords; the future SECS compiler will parse them.
Generated behavior surfacesexamples/valenar/Generated/**/*.cs, docs/design/README.md:30Hand-written stand-ins for compiler output. Either carry // Source: <Content/.secs> provenance or the canonical no-source marker. The generated-code provenance rules are backed by a test guard.
Template / channel / modifier / formula declarations00-overview.md glossary, SecsRegistry runtime registrationEngine owns the declaration metadata at runtime; host registers it at boot.
Scope declarations as schemas01-world-shape.md, ScopeDeclaration in SecsRegistryThe schema (field names, walks, methods) is SECS-owned; the field data is host-owned.
Contract declarations01-world-shape.md:397 (contract Character, contract Settlement, etc.)Engine owns contract metadata; host implements the contract methods through scope-method bindings.
Systems / events / on_actions / activities / policies04-behavior.md, 09-ai-policies-and-activities.mdAll five are SECS declaration keywords with engine-owned executors / dispatchers.
Channel / modifier resolutionsrc/SECS.Engine/Resolution/ChannelResolution.csThe 6-phase resolution pipeline is entirely engine-owned. Host never participates in channel math.
Event dispatchsrc/SECS.Engine/Events/EventDispatcher.csEngine owns the dispatcher and the queue.
Activity executionsrc/SECS.Engine/Activities/ActivityExecutor.csEngine owns lane scheduling, lifecycle progression, and ActivityRunStore.
Policy evaluationsrc/SECS.Engine/Policies/PolicyExecutor.csEngine owns rule iteration, candidate building, and decision selection.
Command buffer model (semantics)src/SECS.Abstractions/Commands/CommandBuffer.cs (the type itself is in Abstractions; the model is engine-owned)The type is bridged; the rules about when commands flush, ordering, idempotence belong to the engine.
Compile-time semantic mod mergePhase 3 compiler-owned; activity/policy runtime finalization exists in ModRegistryThe future compiler performs the full source-set-aware merge. The runtime-owned executable subset is startup finalization for activity and policy mods through SecsRegistry.FinalizeModRegistration(). See docs/adr/ad-0001-runtime-mod-finalization-boundary.md.
Declaration metadatasrc/SECS.Engine/SecsRegistry.csThe registry owns every declaration entry at runtime.
Preview / effect planssrc/SECS.Engine/Activities/EffectPlan.cs, EffectPlanner.csThe engine owns the preview/predict surface used by AI and UI.
ModRegistry runtime accumulatorsrc/SECS.Engine/Modding/ModRegistry.csEngine-owned runtime object that holds and merges activity/policy mod entries before finalize.
ContractLifecycleIds engine vocabularysrc/SECS.Abstractions/Contracts/ContractLifecycleIds.csThe engine defines Activation and Deactivation as the canonical lifecycle slot names; host implementations bind by these constants.
RegistryDiagnostics static-analysis surfacesrc/SECS.Engine/Diagnostics/RegistryDiagnostics.csWarn-only validation surface that runs against the populated registry.

3.3 Shared / bridged

ItemAssemblyEvidenceDrift?
EntityHandleSECS.Abstractionssrc/SECS.Abstractions/EntityHandle.cs:3None
ScopeFrameSECS.Abstractionssrc/SECS.Abstractions/Scopes/ScopeFrame.cs:6None
TickContextSECS.Enginesrc/SECS.Engine/Pipeline/TickContext.cs:3BRIDGED engine context type; physical placement in SECS.Engine is intentional.
ActivityRunContextSECS.Enginesrc/SECS.Engine/Activities/ActivityRunContext.cs:6BRIDGED engine context type; physical placement in SECS.Engine is intentional.
EventContextSECS.Engineinside src/SECS.Engine/Events/SecsEvent.cs:34BRIDGED engine context type; physical placement in SECS.Engine is intentional.
PolicyRunContextSECS.Enginesrc/SECS.Engine/Policies/PolicyRunContext.csBRIDGED engine context type; physical placement in SECS.Engine is intentional.
CommandBuffer / command contextsSECS.Abstractionssrc/SECS.Abstractions/Commands/CommandBuffer.csNone
ISecsHostReadsSECS.Abstractionssrc/SECS.Abstractions/Interfaces/ISecsHostReads.csNone
ISecsHostCommandsSECS.Abstractionssrc/SECS.Abstractions/Interfaces/ISecsHostCommands.csNone
ISecsHostWritesSECS.Abstractionssrc/SECS.Abstractions/Interfaces/ISecsHostWrites.csNone
IEntityIdGeneratorSECS.Abstractionssrc/SECS.Abstractions/Interfaces/IEntityIdGenerator.csNone
IEntityCreatorSECS.Abstractionssrc/SECS.Abstractions/Interfaces/IEntityCreator.csNone
Registry / executor / dispatcher APIsSECS.EngineSecsRegistry, ActivityExecutor, EventDispatcher, PolicyExecutor, PolicyDispatcherSECS public engine API.
Save payload contributed by SECS runtimeSECS.Engine (objects); host (container)ScopeSlotStore.Snapshot/Restore, ActivityRunStore.Restore / ActivityRun.ToStateSECS owns the current partial runtime payload objects; host owns the serialized container. PrevTickSnapshotStore is warmed runtime state, not durable payload. See § 20.2.
Typed ids and SecsTypeRef metadataSECS.Abstractionssrc/SECS.Abstractions/Templates/TemplateId.cs, SecsTypeMetadata.cs, etc.None
ActivityArgsBlob / IActivityArgs<TSelf>SECS.Engine (runtime) and SECS.Abstractions (interface)src/SECS.Engine/Activities/ActivityArgsBlob.cs, IActivityArgs.csRuntime payload in SECS.Engine, interface contract in SECS.Abstractions; split is intentional.
ScopeSlotStoreSECS.Engine (the store); host (the seeding code)src/SECS.Engine/Slots/ScopeSlotStore.csEngine-owned store seeded directly by generated or host-side activation code.

4. Assembly boundary

SECS ships as two assemblies plus generated host code:

  • SECS.Abstractions (netstandard2.1) — public bridge contract. Holds EntityHandle, ScopeFrame, all ISecsHost* interfaces, typed ids (TemplateId, ChannelId, ActivityId, EventId, etc.), SecsTypeRef declaration metadata, CommandBuffer, ContractLifecycleIds. A Unity consumer that wants to declare scopes and run them only needs to reference this assembly plus the engine.
  • SECS.Engine (netstandard2.1) — runtime. Holds SecsRegistry, ChannelResolution, ActivityExecutor, EventDispatcher, PolicyExecutor, ModRegistry, ScopeSlotStore, all the runtime context types (TickContext, EventContext, ActivityRunContext, PolicyRunContext), and RegistryDiagnostics.
  • Generated host code — compiles into the host DLL. References both assemblies. Ownership of the generated tree is SECS-owned (it is what the future compiler will emit) but the build artifact lives in the host DLL.
  • Host code — the game implementation. References both SECS assemblies plus its own world-storage code. Implements ISecsHostReads / Writes / Commands and registers itself with the engine at boot.

The runtime context types currently in SECS.Engine (TickContext, EventContext, ActivityRunContext, PolicyRunContext) are logically bridge types — host code receives them through generated callbacks — but physically live in the engine assembly. That placement is intentional.

5. Host bridge implementation contract

The host must implement:

  • ISecsHostReads — every read primitive the engine uses to access scope fields, walk scope graphs, enumerate scoped collections, and call read-only scope methods. See src/SECS.Abstractions/Interfaces/ISecsHostReads.cs.
  • ISecsHostWrites — every write primitive the engine uses to push resolved-channel dirty values back to host storage. See src/SECS.Abstractions/Interfaces/ISecsHostWrites.cs.
  • ISecsHostCommands — every command primitive the engine uses to call void scope methods that produce host-side mutation. See src/SECS.Abstractions/Interfaces/ISecsHostCommands.cs.
  • IEntityIdGenerator — the host's id generator for runtime entity creation requests.
  • IEntityCreator — the host's entity-creation entry point that the engine calls when a create_entity command resolves.

The host typically implements all five interfaces on a single HostBridge class for simplicity; the engine never asks more from the host than the union of these interfaces, and the host never calls the engine outside SecsRegistry.Register*, FinalizeModRegistration, executor entry points (Tick, StartActivity, Fire, EvaluatePolicy), and reading registry diagnostic output.

ISecsHostReads is read-only: every method returns a value and is guaranteed not to mutate host state. ISecsHostCommands and ISecsHostWrites are command-producing: the engine calls them only in command-producing contexts (system bodies, event bodies, activity lifecycle hooks, command-method scope methods, modifier dirty-sync). Read-only contexts (formulas, trigger conditions, queries, activity CanStart, Preview, policy rule bodies, UI dry-run) must not reach ISecsHostCommands / ISecsHostWrites. Remaining structural hardening is backlog work, not a change to the live rule.

6. Host-capable vs data-only mod boundary

docs/design/01-world-shape.md:253-270 establishes a hard rule:

  • Host-capable source sets (the base game, official expansions shipping with the host build) may add new scopes, scope fields, walks_to declarations, scoped collections, and scope methods. These additions require host-side bridge code, so they cannot ship as data-only content.
  • Third-party data-only mods may NOT add any of those. They may add templates, modifiers, systems, events, activities, policies, and content for existing scopes.

This boundary is enforced at compile time by SECS diagnostics (SECS0604, SECS0609). There is no runtime SourceSet enum or HostCapable / DataOnly flag in the engine — the distinction is purely compiler / doc-level. SECS0609 is compiler-phase-only; no runtime enum wiring is applicable because runtime registration sees the already-lowered generated declarations, not source-set rights.

7. Valenar-illustrative labeling convention

This doc set uses Valenar (examples/valenar/) as the primary example game. Valenar concept names — Settlement, Location, Character, Farm, KnownSpells, ScoutNearbyLead, Goblin, Lead, Wood, DrainStamina, AdvanceLead — are illustrative only. A different game implementing SECS would have entirely different scopes, contracts, channels, activities, policies, and host-side scope methods.

When a design doc gives a Valenar example for a generic SECS concept, it should:

  1. Cite the source .secs or Generated/*.cs file once near the first occurrence.
  2. Include a one-line "(Valenar example; another game would have different X)" caveat at the section head OR at the first use of each Valenar-specific name.
  3. Avoid building section headings or sub-doc structure around Valenar-specific names; use generic SECS vocabulary in headings and demote Valenar names into the example body.

This convention is normative for new docs and recommended for existing docs. Older docs may still carry unlabeled Valenar examples; remaining cleanup is tracked in docs/design/FUTURE_WORK.md rather than in this live boundary doc.

8. Cross-references

  • 01-world-shape.md — defines scopes, fields, contracts, scope methods.
  • 04-behavior.md — defines activities, policies, events, on_actions, systems.
  • 05-expressions.md — defines queries, scope methods at the call site.
  • 06-overrides-and-modding.md — defines what mods may add (host-capable vs data-only).
  • 09-ai-policies-and-activities.md — defines AI policy lowering and the candidate-builder runtime shape.
  • docs/adr/ad-0001-runtime-mod-finalization-boundary.md — records the runtime/compiler boundary for mod finalization.
  • SECS-Compiler-Plan.md — defines what the compiler must lower and when.

9. Boundary highlights

  • Runtime context placement: TickContext, EventContext, ActivityRunContext, and PolicyRunContext stay in SECS.Engine. See § 22.1.
  • Slot seeding ownership: ScopeSlotStore is engine-owned and seeded directly by generated or host-side activation code. See § 17.5.
  • Save payload protocol: SECS provides structured runtime payload objects while the host owns serialization format. See § 20.2.
  • Phase and cadence declarations stay plain C# helper values, not SECS keywords. See § 21.4.
  • SecsRegistry and the executor / dispatcher family are SECS.Engine public API, not bridge interfaces. See § 22.2.
  • Remaining Valenar-label cleanup is backlog only and is tracked in docs/design/FUTURE_WORK.md.

10. Reading guide

Sections 11-20 classify executable surfaces, read-only versus command-producing behavior, and save/load boundaries. Later sections capture API-shape and assembly-placement decisions that constrain the bridge.

11. Invocation matrix

This section classifies every callable / executable / lifecycle surface SECS exposes today (or ships a hand-written stand-in for) by who may call it, what context it requires, what it may do, and how it fails. Surfaces marked NOT-COMMITTED are listed in section 12; detailed backlog lives in docs/design/FUTURE_WORK.md.

The column model is fixed:

  • Caller — host / SECS / both / via-bridge-only.
  • Context requiredTickContext / ActivityRunContext / ActivityContext / EventContext / PolicyRunContext / TemplateCommandContext / ScopeFrame + ISecsHostReads / CommandBuffer / ScopeSlotStore / SecsRegistry / none.
  • Read-only? — yes / no / depends-on-caller-context.
  • Command-producing? — yes / no / via-context-Commands.
  • May start activities? — yes / no.
  • May fire events / on_actions? — yes / no.
  • May create / destroy entities? — yes / no.
  • Random? — yes / no / via-context-Random / accessible-but-inappropriate.
  • Callable in Preview? — yes / no.
  • Callable in save / load? — yes / no / N/A.
  • Result type — typed / void / ActivityRequest[] / RuleDecision / EffectPlan / etc.
  • Failure mode — throws / fails-validation / silent / undefined.
  • Saved? — yes-what / no.
  • Status — COMMITTED / PARTIAL / NOT-COMMITTED / DEPRECATED.

Surfaces in this section are ordered by group (A through G), and within each group by the runtime flow used in this doc. Each surface is a paragraph block — a flat-table form would wrap badly in this column model.

11.1 Group A — System / event / on_action / template

A1 — system Execute — COMMITTED (src/SECS.Engine/Pipeline/ITickSystem.cs:49). Caller: SECS-registered; dispatched by SystemStep.Execute(TickContext) inside the tick pipeline; host triggers the pipeline. Context: TickContext. Read-only? No — has full Commands and FlushCommands(). Command-producing? Yes — SystemStep.cs:47 flushes after the body. May start activities? Yes (via ctx and the executor). May fire events / on_actions? Yes (ctx.Events.FireOnAction). May create / destroy entities? Yes (create via ctx.CreateEntity; destroy via host-exposed TemplateActivator.DestroyWithChildren). Random? Yes (ctx.Random). Callable in Preview? No (no preview gating for ITickSystem). Callable in save / load? No (pipeline not driven during restore). Result: void. Failure mode: exception propagates; SystemStep.cs:50 discards the buffered commands on throw and re-throws. Saved? No — system has no saved state; activity runs started from within are saved through ActivityRunStore.

A2 — pulse event Condition — COMMITTED (src/SECS.Engine/Events/SecsEvent.cs:16). Caller: EventDispatcher.EvaluateCondition per entity per pulse cycle. Context: EventContext (wraps TickContext). Read-only? Yes by enforcement — EventDispatcher.cs:350 calls ctx.DiscardCommands() in finally. Command-producing? No (any commands enqueued are discarded). May start activities? No. May fire events / on_actions? No (no flush). May create / destroy entities? No. Random? Accessible via context.Tick.Random but semantically inappropriate for a pure predicate. Callable in Preview? No (no defined preview mode for pulse events). Callable in save / load? No. Result: bool. Failure mode: exception propagates after discard; the event is skipped for that entity. Saved? No.

A3 — pulse event Execute — COMMITTED (src/SECS.Engine/Events/SecsEvent.cs:19). Caller: EventDispatcher.InvokeEventBody from ExecuteOrQueueChoice when no BuildOptions is present. Context: EventContext. Read-only? No. Command-producing? Yes — EventDispatcher.cs:373 flushes after the body. May start activities? Yes. May fire events / on_actions? Yes (recursive FireOnAction allowed). May create / destroy entities? Yes. Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: void. Failure mode: exception propagates; commands discarded (EventDispatcher.cs:376). Saved? No — deferred choice queue is in-memory; not persisted.

A4 — event BuildOptions — COMMITTED (src/SECS.Engine/Events/SecsEvent.cs:26). Caller: EventDispatcher.InvokeBuildOptions from ExecuteOrQueueChoice. Context: EventContext plus the EventOptions builder. Read-only? Yes by enforcement — EventDispatcher.cs:362 calls ctx.DiscardCommands() in finally. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible but semantically inappropriate. Callable in Preview? No. Callable in save / load? No. Result: void (mutates the EventOptions builder). Failure mode: exception propagates; commands discarded. Saved? No.

A5 — event option handler — COMMITTED (src/SECS.Engine/Events/SecsEvent.cs:97EventOption.Execute). Caller: EventDispatcher.ResolveChoice invoked by host when the player picks an option. Context: EventContext (restored from PendingChoice.SavedScopes). Read-only? No. Command-producing? Yes — InvokeEventBody wraps the option handler (EventDispatcher.cs:322). May start activities? Yes. May fire events / on_actions? Yes. May create / destroy entities? Yes. Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: void. Failure mode: exception propagates; commands discarded. Saved? PendingChoice queue is in-memory and not serialized; pending choices are lost across save / load.

A6 — on_action declaration — COMMITTED (src/SECS.Engine/Events/OnActionDeclaration.cs, registered via SecsRegistry.RegisterOnAction). Caller: registration-time only; host registers at boot via SecsModule.Initialize(registry). Context: SecsRegistry (boot-time). Read-only? N/A — metadata object. Command-producing? N/A. May start activities? N/A. May fire events / on_actions? N/A. May create / destroy entities? N/A. Random? N/A. Callable in Preview? N/A. Callable in save / load? N/A. Result: void. Failure mode: InvalidOperationException on duplicate id or missing registry attachment. Saved? No — declaration is static.

A7 — on_action fire — COMMITTED (src/SECS.Engine/Events/EventDispatcher.cs:166FireOnAction). Caller: SECS or host; ctx.Events.FireOnAction(onActionId, target, ctx) is callable from any context that holds a TickContext. Context: TickContext (implicit on ctx.Events). Read-only? No. Command-producing? Yes — FireOnAction flushes the buffer before dispatch (EventDispatcher.cs:168). May start activities? Yes (via subscribed events). May fire events / on_actions? Yes (subscriber code or host code may call nested dispatch explicitly; reserved fallback / chaining metadata is ignored). May create / destroy entities? Yes. Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: void. Failure mode: InvalidOperationException on missing required saved scope (ValidateProvidedScopes). Saved? No.

A8 — template activation — COMMITTED (src/SECS.Engine/TemplateActivator.cs:40Activate). Caller: host-initiated at boot or runtime; Activate(templateId, owner, scopeTarget). Context: TemplateCommandContext for lifecycle methods; ScopeFrame + CommandBuffer for the activation delegate itself. Read-only? No — runs RegisterChannelSource / AddModifier commands and invokes the contract's activation lifecycle (e.g. OnBuilt). Command-producing? Yes (own internal CommandBuffer). May start activities? No during activation proper; lifecycle method may. May fire events / on_actions? No (no TickContext available during activation; TemplateCommandContext lacks event dispatch). May create / destroy entities? Yes — TemplateCommandContext.CreateEntity delegates to TemplateCommandEntityCreator. Random? No (no Random on TemplateCommandContext). Callable in Preview? No. Callable in save / load? Not as a durable restore primitive — Activate / Create fire activation and lifecycle behavior and would treat an already-saved entity as newly created. Current partial restore uses raw ActivityRunStore.Restore + ScopeSlotStore.Restore for the stores that support it; a no-lifecycle template rehydrate path remains future work. Result: ActivationSummary. Failure mode: InvalidOperationException on missing lifecycle method. Saved? Channel sources and modifier bindings registered during activation are not persisted as an "activation record" today; future save/load must either persist the resulting runtime records or add a restore-only rehydrate path that rebuilds template runtime state without normal lifecycle side effects.

A9 — template deactivation — COMMITTED (src/SECS.Engine/TemplateActivator.cs:401Destroy, DestroyWithChildren). Caller: host — Activator.Destroy(templateId, owner, scopeTarget) or Activator.DestroyWithChildren(owner). Context: TemplateCommandContext for the deactivation lifecycle; no TickContext. Read-only? No — removes channel sources and bindings; calls the deactivation lifecycle. Command-producing? Yes (internal buffer; RemoveAllChannelSources, RemoveAllBindings). May start activities? No. May fire events / on_actions? No. May create / destroy entities? Yes (recursive DestroyWithChildren). Random? No. Callable in Preview? No. Callable in save / load? No. Result: void. Failure mode: InvalidOperationException on missing instance. Saved? No.

A10 — template query — COMMITTED (src/SECS.Abstractions/Delegates.cs:58TemplateQueryDelegate, called via ISecsHostReads.CallScopeQuery). Caller: host or SECS — host.CallScopeQuery<TArgs, TResult>(target, scopeId, methodId, args) (ISecsHostReads.cs:34); formula bodies and event conditions can call through context.Tick.Host.CallScopeQuery. Context: ScopeFrame + ISecsHostReads (no full TickContext). Read-only? Yes — ISecsHostReads guarantees no host mutation per § 5. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No (no Random in the delegate signature). Callable in Preview? Yes — safe to call from Preview, Condition, CanStart, etc. Callable in save / load? No (no active host state to query). Result: generic TResult. Failure mode: InvalidOperationException if the method is not found or the argument type does not match (SecsRegistry.cs:2449). Saved? No.

A11 — template command method — COMMITTED (src/SECS.Abstractions/Delegates.cs:44TemplateCommandDelegate, invoked via ITemplateCommandContext.CallScopeCommand or TickContext.CallScopeCommand). Caller: SECS or host — invoked from system / event / activity bodies via ctx.CallScopeCommand(target, scopeId, methodId, args). Context: ITemplateCommandContext (its own CommandBuffer). Read-only? No. Command-producing? Yes — writes to the CommandBuffer cmds passed in via ISecsHostCommands. May start activities? Yes (host bridge may start activities inside). May fire events / on_actions? Yes (host bridge may fire events inside). May create / destroy entities? Yes. Random? Depends on the host implementation. Callable in Preview? No — TickContext.CallScopeCommand throws if HostCommands is null (TickContext.cs:216). Callable in save / load? No. Result: void. Failure mode: InvalidOperationException if HostCommands is null; the host bridge throws on unknown methods. Saved? No (effects flow through the CommandBuffer to the host).

11.2 Group B — Activity

B1 — activity IsVisible — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:80). Caller: ActivityExecutor.IsVisible(activityId, context) (ActivityExecutor.cs:88). Context: ActivityContext. Read-only? Yes by convention — no command flush in the executor's IsVisible path. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible but inappropriate. Callable in Preview? Yes — safe outside tick execution. Callable in save / load? No. Result: bool. Failure mode: exception propagates; the executor returns false for an unknown activityId. Saved? No.

B2 — activity IsTargetValid — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:83). Same shape as IsVisible. Caller: ActivityExecutor.IsTargetValid(activityId, context) (ActivityExecutor.cs:97). Context: ActivityContext. Read-only? Yes (same enforcement pattern as IsVisible — no flush in path). Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible but inappropriate. Callable in Preview? Yes. Callable in save / load? No. Result: bool. Failure mode: returns false for unknown activityId. Saved? No.

B3 — activity CanStart — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:86). Caller: inner check inside ActivityExecutor.CanStartAt (ActivityExecutor.cs:129); also reachable through the host-facing CanStart API. Context: ActivityContext. Read-only? Yes — no flush in the CanStart path; § 5 names it as a read-only context. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible but inappropriate. Callable in Preview? Yes — CanStart is part of the non-starting preview path. Callable in save / load? No. Result: bool. Failure mode: exception propagates; the activity is not started. Saved? No.

B4 — activity GetDurationTicks — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:89). Caller: ActivityExecutor.CanStartAt (ActivityExecutor.cs:157) and StartAt (ActivityExecutor.cs:222). Context: ActivityContext. Read-only? Yes — pure computation; no command flush in the path. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible but inappropriate. Callable in Preview? Yes. Callable in save / load? No. Result: int. Failure mode: InvalidOperationException on a negative return (ActivityExecutor.cs:226). Saved? No on its own; the duration is stored on ActivityRun and persisted via ActivityRunState.

B5 — activity Preview — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:92). Caller: ActivityExecutor.Preview(...) (ActivityExecutor.cs:353); also called at start time by PreviewDriftRecorder. Context: ActivityContext. Read-only? Yes by convention and § 5; no command flush in the executor's preview path. Command-producing? No (commands must not be flushed). May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible via context.Tick.Random — acceptable for probabilistic previews, but should be deterministic. Callable in Preview? Yes — this IS the preview surface. Callable in save / load? No. Result: EffectPlan. Failure mode: InvalidOperationException on unknown activityId; exception in body propagates. Saved? No.

B6 — activity OnStart — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:95). Caller: ActivityExecutor.InvokeLifecycle(runContext, activity.OnStart) inside StartAt (ActivityExecutor.cs:259). Context: ActivityRunContext. Read-only? No. Command-producing? Yes — InvokeLifecycle flushes after the body (ActivityExecutor.cs:587). May start activities? Yes. May fire events / on_actions? Yes. May create / destroy entities? Yes (full TickContext available through context.Tick). Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: void. Failure mode: exception propagates; commands discarded; an OnStart failure leaves the run in the store and the caller checks the terminal state. Saved? No on its own — the run is saved, not the OnStart invocation.

B7 — activity OnUpdate — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:99). Same shape as OnStart. Caller: ActivityExecutor.TickAtInvokeLifecycle(context, run.Activity.OnUpdate) (ActivityExecutor.cs:325). Context: ActivityRunContext. Read-only? No. Command-producing? Yes. May start activities / fire events / create / destroy entities? Yes. Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: void. Failure mode: exception propagates; commands discarded. Saved? No.

B8 — activity OnComplete — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:103). Same shape as OnStart. Caller: InvokeTerminalLifecycle case Completed (ActivityExecutor.cs:562). Context: ActivityRunContext. Result: void.

B9 — activity OnStop — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:107). Same shape as OnComplete. Caller: InvokeTerminalLifecycle case Stopped.

B10 — activity OnCancel — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:111). Same shape as OnComplete. Caller: InvokeTerminalLifecycle case Canceled.

B11 — activity OnFail — COMMITTED (src/SECS.Engine/Activities/SecsActivity.cs:116). Same shape as OnComplete. Caller: InvokeTerminalLifecycle case Failed.

B12 — activity start (host-initiated) — COMMITTED (src/SECS.Engine/Activities/ActivityExecutor.cs:161Start(ActivityId, ActivityContext)). Caller: host or SECS via ActivityExecutor.Start(request, tick) or Start(activityId, context). Context: ActivityContext externally; ActivityRunContext for lifecycle invocations. Read-only? No — deducts costs, applies cooldown, fires OnStart. Command-producing? Yes (cost deduction calls FlushCommands; OnStart fires commands). May start activities? Yes (recursive starts on different lanes). May fire events / on_actions? Yes (through the OnStart body). May create / destroy entities? Yes (through the OnStart body). Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: ActivityRun? (null when CanStart fails). Failure mode: returns null on validation failure; throws InvalidOperationException on args schema mismatch or negative duration. Saved? ActivityRun.ToState()ActivityRunState, including request origin; serialized by host to persist across saves; ActivityExecutor.SeedNextRunId restores the id counter.

B13 — activity preview (host-initiated) — COMMITTED (src/SECS.Engine/Activities/ActivityExecutor.cs:353Preview(...)). Caller: host or SECS (e.g. policy scoring). Context: TickContext plus ActivityId plus EntityHandle actor / target. Read-only? Yes. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible. Callable in Preview? Yes — this is the preview entry point. Callable in save / load? No. Result: EffectPlan. Failure mode: InvalidOperationException for unknown activity. Saved? No.

B14 — activity cancel (host-initiated) — COMMITTED (src/SECS.Engine/Activities/ActivityExecutor.cs:342Cancel(ActivityRun, TickContext)). Caller: host or PolicyDispatcher.ProcessCancelChild (PolicyDispatcher.cs:168). Context: TickContext. Read-only? No — transitions the run, fires OnCancel. Command-producing? Yes (through OnCancel). May start activities / fire events / create / destroy entities? Yes (through OnCancel). Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: bool (false if the run is not in a cancelable state). Failure mode: returns false silently. Saved? After cancel, the run is removed from the store and not persisted.

B15 — activity lifecycle transition — COMMITTED (src/SECS.Engine/Activities/ActivityRun.csTryTransition). Caller: internal to the executor and via ActivityRunContext.Complete/Stop/Cancel/Fail (ActivityRunContext.cs:58-75). Context: ActivityRunContext. Read-only? No — mutates run status. Command-producing? No (the transition itself); the terminal lifecycle hook is command-producing. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No. Callable in Preview? No. Callable in save / load? No. Result: void (via the context methods). Failure mode: silent if the run is already terminal — TryTransition returns false and the call is ignored. Saved? The new status is captured in ActivityRunState.

11.3 Group C — Policy

C1 — policy rule Decision — COMMITTED (src/SECS.Engine/Policies/SecsPolicy.cs:26EvaluateRule(RuleId, PolicyRunContext)). Caller: PolicyExecutor.EvaluateAllRulespolicy.EvaluateRule(rule.Id, context) (PolicyExecutor.cs:111). Context: PolicyRunContext (Actor, Domain, Tick). Read-only? AMBIGUOUS — see C13-18 below. Command-producing? AMBIGUOUS (C13-18). May start activities? No (rule bodies return RuleDecision; the dispatcher starts activities). May fire events / on_actions? AMBIGUOUS (C13-18). May create / destroy entities? AMBIGUOUS (C13-18). Random? context.Tick.Random is accessible; whether it is allowed is AMBIGUOUS (C13-18). Callable in Preview? No — PolicyRunContext only carries Actor, Domain, Tick. Callable in save / load? No. Result: RuleDecision (one of Continue, Complete, Fail, Wait, Call, CallBest, CancelChild). Failure mode: exception propagates from the EvaluateAllRules iterator (uncaught) up to the dispatcher. Saved? No (the decision is ephemeral).

C2 — policy Call — COMMITTED (src/SECS.Engine/Policies/RuleDecision.cs:35RuleDecision.Call). Caller: PolicyDispatcher.ProcessCall triggered when EvaluateRule returns RuleDecision.Call (PolicyDispatcher.cs:137). Context: implicit TickContext on PolicyDispatcher.Tick(policy, actor, tick). Read-only? No — ProcessCall calls ActivityExecutor.Start. Command-producing? Yes (activity start triggers costs / OnStart). May start activities? Yes. May fire events / on_actions? Yes (through the started activity). May create / destroy entities? Yes (through the started activity). Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: PolicyDispatchResult.Fail if the start returns null; otherwise iteration continues. Failure mode: returns PolicyDispatchResult.Fail (no exception on failure to start). Saved? The activity run is saved with ActivityRequestOrigin.Policy; the Call decision itself is not.

C3 — policy CallBest — COMMITTED (src/SECS.Engine/Policies/RuleDecision.cs:38RuleDecision.CallBest). Caller: PolicyDispatcher.ProcessCallBestPolicyExecutor.CallBestActivityExecutor.Start. Context: PolicyRunContext inside CallBest. Read-only? No. Command-producing? Yes. May start activities? Yes. May fire events / on_actions? Yes. May create / destroy entities? Yes. Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: PolicyDispatchResult.NoCandidate, StartRefused, or ActivityStarted. Failure mode: returns an activity-level failure code (no exception). Saved? The activity run is saved with ActivityRequestOrigin.Policy; the CallBest decision itself is not.

C4 — policy CancelChild — COMMITTED (src/SECS.Engine/Policies/RuleDecision.cs:41RuleDecision.CancelChild). Caller: PolicyDispatcher.ProcessCancelChild (PolicyDispatcher.cs:163). Context: TickContext (held by the dispatcher). Read-only? No. Command-producing? Yes (through ActivityExecutor.CancelOnCancel). May start activities / fire events / create / destroy entities? Yes (through the OnCancel body). Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: PolicyDispatchAction.Skipped or ActivityCancelled. Failure mode: silent — Skipped when no child is tracked. Saved? No.

C5 — policy dispatcher tick — COMMITTED (src/SECS.Engine/Policies/PolicyDispatcher.cs:59Tick(policy, actor, tick)). Caller: host or SECS system body — host drives the dispatcher per actor per tick. Context: TickContext. Read-only? No. Command-producing? Yes (via the activities it starts or cancels). May start activities / fire events / create / destroy entities? Yes. Random? Yes. Callable in Preview? No. Callable in save / load? No. Result: PolicyDispatchResult. Failure mode: throws InvalidOperationException if policy or tick is null; throws if a rule slot is not seeded (RequireSlot). Saved? The activeChildren dictionary is in-memory and NOT persisted — child tracking is lost on save / load.

C6 — candidate-builder delegate — COMMITTED (src/SECS.Engine/Policies/CandidateBuilder.csCandidateBuilderDelegate). Surface form is a delegate registered by the host; there is no committed source-form keyword for declaring one. Caller: PolicyExecutor.BuildFromCollectionCandidateBuilderDelegate(actor, slot, tick) (PolicyExecutor.cs:199). Context: (EntityHandle actor, SlotKindId slot, TickContext tick). Read-only? Yes by convention — should only read slot contents and return an ActivityRequest list; no flush in the path. Command-producing? No (no flush in BuildFromCollection). May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? Accessible via tick; inappropriate for deterministic candidate building. Callable in Preview? Yes — called during policy scoring, which is a read-only phase. Callable in save / load? No. Result: IReadOnlyList<ActivityRequest>. Failure mode: InvalidOperationException if the builder returns null (treated as empty); InvalidOperationException if Builder.IsZero (PolicyExecutor.cs:186). Saved? No. A builder may return request origin when it knows it, but PolicyDispatcher.ProcessCallBest normalizes the selected live start to ActivityRequestOrigin.Policy.

11.4 Group D — Contract / scope

D1 — contract query call — COMMITTED. Same surface as A10 (TemplateQueryDelegate / ISecsHostReads.CallScopeQuery); read-only, Preview-safe, no command capability. See A10 for full classification.

D2 — contract method call — COMMITTED. Same surface as A11 (TemplateCommandDelegate / ISecsHostCommands.CallScopeCommand); command-producing, requires ISecsHostCommands. See A11 for full classification.

D3 — scope field read — COMMITTED (src/SECS.Abstractions/Interfaces/ISecsHostReads.cs:27-31ReadInt/Long/Float/Double/Bool). Caller: any code holding ISecsHostReads (formulas, triggers, queries, system bodies, event bodies, activity bodies). Context: ISecsHostReads. Read-only? Yes. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? N/A. Callable in Preview? Yes. Callable in save / load? No. Result: typed scalar. Failure mode: implementation-defined; typically returns 0 for an unknown entity or field. Saved? No (host owns the data).

D4 — scope query method — COMMITTED. Same surface as A10 / D1. Read-only, Preview-safe.

D5 — scope command method — COMMITTED. Same surface as A11 / D2. Command-producing.

D6 — formula body — COMMITTED (src/SECS.Abstractions/Delegates.cs:9FormulaIntDelegate and siblings). Caller: ChannelResolution during channel resolution; also PolicyExecutor.ResolveNeedTarget (PolicyExecutor.cs:364). Context: (EntityHandle, EntityHandle, EntityHandle, ISecsHostReads). Read-only? Yes — the delegate signature has no command capability. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No (no Random in the signature). Callable in Preview? Yes — formulas evaluate lazily, including during preview channel reads. Callable in save / load? No. Result: typed scalar (int, long, float, double, bool). Failure mode: exception propagates from ChannelResolution. Saved? No.

D7 — trigger body — COMMITTED (src/SECS.Abstractions/Delegates.cs:3TriggerDelegate). Same shape as the formula body but returns bool. Used by modifier condition evaluation and AddModifierWhen. Read-only? Yes. Command-producing? No. Random? No. Callable in Preview? Yes. Result: bool.

D8 — modifier-effect formula — COMMITTED. The modifier-effect "magnitude formula" is the same delegate type as D6: modifier effects carry a formulaId resolved via SecsRegistry.GetFormulaInt/Float/Long/Double/Bool. Same constraints as D6.

11.5 Group E — Channel / template field / collection

E1 — template_field base read — COMMITTED (src/SECS.Engine/Instances/TemplateEntry.cs:23-27GetFieldInt/Long/Float/Double/Bool; also ISecsHostReads.ResolveTemplateValueInt etc.). Caller: host or engine channel resolution (TemplateValueResolver). Context: TemplateId + fieldId + contextTargets (no TickContext for the resolver itself). Read-only? Yes. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No. Callable in Preview? Yes. Callable in save / load? No. Result: typed scalar. Failure mode: returns default (0, false) for unknown field. Saved? No — template fields are static declarations.

E2 — effective template value read — COMMITTED (src/SECS.Engine/Resolution/TemplateValueResolver.cs). Same constraints as E1. The effective template value is the modifier-resolved value from ISecsHostReads.ResolveTemplateValueInt.

E3 — collection iteration — COMMITTED (src/SECS.Abstractions/Interfaces/ISecsHostReads.cs:25WalkChildren; ISecsHostReads.cs:10GetChildren). Caller: ChannelResolution for aggregate channels, PolicyExecutor for collection-driven candidate sources, system bodies, event bodies. Context: ISecsHostReads. Read-only? Yes. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No. Callable in Preview? Yes. Callable in save / load? No. Result: IEnumerable<EntityHandle> or ReadOnlySpan<EntityHandle>. Failure mode: returns empty for unknown collection. Saved? No (host owns the backing data).

E4 — slot read — COMMITTED (src/SECS.Engine/Slots/ScopeSlotStore.cs:31Read<T>(scope, kind)). Caller: PolicyExecutor.RequireSlot and host code; engine reads, host seeds. Context: ScopeSlotStore (accessed via tick.SlotStore). Read-only? Yes (read path). Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No. Callable in Preview? Yes. Callable in save / load? No. Result: IReadOnlyList<T>. Failure mode: InvalidOperationException if the slot is not initialized (ScopeSlotStore.cs:37). Saved? Yes — ScopeSlotStore.Snapshot() serializes all slots; Restore() rehydrates.

E5 — slot write — COMMITTED (src/SECS.Engine/Slots/ScopeSlotStore.csInitialize, Append, Insert, Replace, RemoveAt). Caller: host at activation time (e.g. CharacterSlots.SeedDefaults) and at runtime for player-edited policy rules. Context: ScopeSlotStore directly. Read-only? No. Command-producing? No — direct store mutation, not via CommandBuffer. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No. Callable in Preview? No (slot writes during preview would corrupt state). Callable in save / load? No (initialization must happen before policy evaluation; Restore is the load path). Result: void. Failure mode: InvalidOperationException on uninitialized slot for write ops; InvalidOperationException on duplicate Initialize. Saved? Yes — slot contents are captured in Snapshot(). Saved currently from SECS source? No — there is no committed source-form syntax for slot writes; host code owns slot seeding. Direct-call pattern endorsed; see § 17.5.

11.6 Group F — Commands

F1 — create_entity — COMMITTED (src/SECS.Engine/Pipeline/TickContext.cs:229CreateEntity(templateId, ...); also TemplateCommandContext.cs:66). Caller: SECS system / event / activity bodies via ctx.CreateEntity(templateId) or ctx.CreateEntity(templateId, scopeTarget); host can also call. Context: TickContext or TemplateCommandContext. Read-only? No — creates the entity and runs activation. Command-producing? Yes (activation registers channel sources / bindings via an internal CommandBuffer). May start activities? No (activation does not start activities directly; lifecycle hooks may). May fire events / on_actions? No (no TickContext exposure inside activation). May create / destroy entities? Yes (creates; does not destroy here). Random? Accessible via ctx.Random on TickContext. Callable in Preview? No — CreateEntity throws if IdGenerator is null (TickContext.cs:259). Callable in save / load? No. Result: EntityHandle. Failure mode: InvalidOperationException on null IdGenerator. Saved? The new entity must be persisted by the host; activated channel sources / bindings are tracked by the engine.

F2 — destroy-entity surface — PARTIAL (src/SECS.Engine/TemplateActivator.cs:401Destroy; TemplateActivator.cs:386DestroyWithChildren). Caller: host or system bodies via an exposed TemplateActivator reference. Context: host holds TemplateActivator directly; no TickContext wrapper. Read-only? No. Command-producing? Yes (removes channel sources / bindings internally). May start activities? No. May fire events / on_actions? No. May create / destroy entities? Yes (destroys; DestroyWithChildren recurses). Random? No. Callable in Preview? No. Callable in save / load? No. Result: void. Failure mode: InvalidOperationException on missing instance. Saved? Destruction must be mirrored in host save data; the engine removes from InstanceStore. Status: PARTIAL — there is no committed ctx.DestroyEntity(handle) lowering on TickContext; the only committed surface is the host-direct TemplateActivator API. See § 12.1.

F3 — add_modifier — COMMITTED (src/SECS.Abstractions/Commands/CommandBufferExtensions.cs:107AddModifier; also TickContext.AddModifierWhen). Caller: any code with CommandBuffer access. Context: CommandBuffer. Read-only? No. Command-producing? Yes — this IS the command. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No (a modifier is registered via command processing; it does not start activities). Random? N/A. Callable in Preview? No (would mutate state). Callable in save / load? No. Result: void. Failure mode: InvalidOperationException at flush time if the modifier is not registered. Saved? Modifier bindings are volatile and currently NOT persisted by the engine — host must serialize ModifierBindingStore contents (not in ScopeSlotStore or ActivityRunState).

F4 — remove_modifier — COMMITTED (src/SECS.Abstractions/Commands/CommandBufferExtensions.cs:132RemoveModifier). Same constraints as F3. Saved? See F3 — modifier state is transient.

F5 — increment / set scope-field command — COMMITTED (src/SECS.Abstractions/Commands/CommandBufferExtensions.cs:171IncrementScopeField; also SetScopeField). Caller: any code with CommandBuffer. Context: CommandBuffer. Read-only? No. Command-producing? Yes. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? N/A. Callable in Preview? No (would mutate the host field value). Callable in save / load? No. Result: void. Failure mode: at flush, ISecsHostCommands.CallScopeCommand dispatches to host; errors are host-implementation-defined. Saved? Host owns the scope field; SECS does not persist field values.

F6 — fire-event surface — NOT-COMMITTED. There is no ctx.FireEvent(eventId) API on TickContext. Pulse events are fired by EventDispatcher.TickPulse from EventPulseStep.Execute in the pipeline; SECS source fires events through on_actions only. The committed scripted-fire path is FireOnAction (A7). See § 12.2.

F7 — on_action fire (from a body) — COMMITTED. Same surface as A7 (ctx.Events.FireOnAction).

F8 — save_scope_as / get-saved-scope — COMMITTED (src/SECS.Engine/Pipeline/TickContext.cs:62SaveScope; TickContext.cs:69GetSavedScope). Caller: SECS system / event / activity bodies; host bridge code. save_scope_as lowers to ctx.SaveScope(hash, entity). Context: TickContext. Read-only? SaveScope is a write; GetSavedScope is a read. Command-producing? No (in-memory transient dictionary). May start activities / fire events / create / destroy entities? No. Random? N/A. Callable in Preview? No (saved scopes are dispatch-frame specific; preview does not run through dispatch). Callable in save / load? No — saved scopes are cleared between ticks via frame push / pop. Result: void / EntityHandle. Failure mode: GetSavedScope returns EntityHandle.Null if not found (no exception). Saved? No — saved scopes are transient within a dispatch frame; PendingChoice captures a snapshot for deferred player choices (EventDispatcher.cs:298) but that snapshot is also not persisted .

11.7 Group G — Registry / save

G1 — registry validate — COMMITTED (src/SECS.Engine/SecsRegistry.cs:2945Validate()). Caller: host at boot, after all Register* calls. Context: SecsRegistry (self). Read-only? Yes — analysis only, no mutations. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No. Callable in Preview? N/A. Callable in save / load? No. Result: RegistryDiagnostics (list of RegistryDiagnostic with codes and severities). Failure mode: never throws — returns diagnostics that include error-level items. Saved? No.

G2 — partial runtime restore primitives — PARTIAL. Two store-level restore surfaces exist today; this is not a unified SECS save/load boundary:

  • Surface A: ScopeSlotStore.Snapshot() / ScopeSlotStore.Restore(snapshots) (ScopeSlotStore.cs:89, 110).
  • Surface B: ActivityExecutor.SeedNextRunId(nextValue) before ActivityRunStore.Restore(activity, state, durationTicks) (ActivityRunStore.cs:190, ActivityExecutor.cs:57).
  • Re-derived runtime state: PrevTickSnapshotStore is warmed from live channel values after restore / first tick. Its Snapshot* methods are per-tick capture APIs for previous-value reads, not durable save payload.

Caller: host save / load driver. Context: host-driven; no TickContext. Read-only? Save reads existing state; restore writes to fresh stores. Command-producing? No. May start activities? No. May fire events / on_actions? No. May create / destroy entities? No. Random? No. Callable in Preview? N/A. Callable in save / load? Yes — this is the current partial restore surface. Result: void (restore) / IReadOnlyList<SlotSnapshot> (slot snapshot) / ActivityRunState[] (activity restore — host materializes). PrevTickSnapshotStore has no durable restore result because it is re-derived runtime state. Failure mode: InvalidOperationException on restore to a non-empty store, type mismatch, unknown slot kind, or schema violation. Saved? Yes — these are partial runtime save/load primitives only for ScopeSlotStore and activity runs. A unified SecsSavePayload / SaveSnapshot entry point remains future work.

12. Uncommitted invocation surfaces

12.1 TickContext destruction helpers

TickContext exposes creation helpers but not DestroyEntity or DestroyWithChildren. The committed destruction path remains host-direct TemplateActivator use.

12.2 Direct pulse-event fire by id

There is no committed ctx.FireEvent(eventId) API. Pulse events fire through the pipeline, and scripted dispatch goes through on_action.

12.3 Structural hardening for read-only contexts

The live semantic rule is in § 13, but some read-only contexts still need stronger structural guards against command access. That hardening work is backlog, not current contract.

13. Read-only vs command-producing taxonomy

This section defines the read-only vs command-producing boundary for callable SECS surfaces.

Resolution: rule bodies are read-only.

Policy rule bodies (the body of EvaluateRule(...)) may call only:

  • ISecsHostReads methods (read scope fields, walk graph, enumerate collections, call read-only scope methods).
  • Channel resolution (ctx.Tick.ChannelResolver.ResolveIntFast, etc.).
  • RuleDecision constructors (RuleDecision.Continue, Call(...), CallBest(...), Complete(), Fail(...), Wait(...), CancelChild(...)).

Policy rule bodies may NOT call:

  • ctx.Tick.Commands.* (any command-buffer method).
  • ctx.Tick.FlushCommands().
  • ctx.Tick.CreateEntity(...) or any entity-creation surface.
  • ISecsHostCommands methods (no void scope methods, no host-side side effects).
  • ISecsHostWrites methods (the dirty-sync writer; that is the engine's job at end-of-tick).

This matches what generated policies already do today. The SECS-Compiler-Plan.md:143 mention of "policy rule bodies" in the flush-contract list was a drafting error and has been removed in this wave.

13.1 Read-only surfaces

The following surfaces are read-only by declaration kind. They receive contexts or signatures that structurally exclude the command buffer:

SurfaceContext / signatureStructural read-only enforcement
formula body(EntityHandle, EntityHandle, EntityHandle, ISecsHostReads) -> TNo TickContext; only ISecsHostReads admitted.
trigger bodysame as formulaSame.
modifier effect formula (math, not the apply step)ISecsHostReads onlySame.
template query(ScopeFrame, ISecsHostReads, in TArgs) -> TResultNo TickContext.
contract querysame as template querySame.
scope query method (non-void)ISecsHostReads.CallScopeMethodThe void / non-void split is the discriminator.
event ConditionEventContext (carries TickContext)Normative read-only; future compiler/analyzer handoff currently reserved as SECS0803.
activity IsVisibleActivityContext (no FlushCommands())Weak structural barrier; future compiler/analyzer handoff currently reserved as SECS0803.
activity IsTargetValidActivityContextSame.
activity CanStartActivityContextSame.
activity GetDurationTicksActivityContextSame.
activity PreviewActivityContext (calls EffectPlanner)Same.
policy rule Decision (C13-18)PolicyRunContext (no FlushCommands())Weak structural barrier; future compiler/analyzer handoff currently reserved as SECS0803.
policy candidate scoring / previewPolicyExecutor.ScoreCandidate reads EffectPlan dataRead-only by construction.
UI / tooling dry-runfuture read-only contextNot yet implemented; reserved for future implementation.

13.2 Command-producing surfaces

The following surfaces are command-producing. They receive contexts that expose Commands and (for the lifecycle methods) FlushCommands():

SurfaceContextNotes
system ExecuteTickContextCanonical host of ctx.Commands.* + ctx.FlushCommands().
pulse event ExecuteEventContextDistinct from Condition which is read-only.
event option handlerEventContextGenerated examples call ctx.Commands.AddModifier, FlushCommands.
on_action firing siteTickContext (system context) or EventContext (event context)The fire is a command; subscribers run with their own contexts.
activity OnStart / OnUpdate / OnComplete / OnStop / OnCancel / OnFailActivityRunContextHas public CommandProcessSummary FlushCommands() shorthand.
void scope methodITemplateCommandContext (template) or TickContext.CallScopeCommandRoutes through ISecsHostCommands.CallScopeCommand.
template command method (method void)ITemplateCommandContextSame flush contract as system/event/activity bodies.
entity creation / destruction (create_entity, destroy_entity)TickContext.CreateEntity, etc.Requires IdGenerator != null; legal only from command-producing contexts.
modifier attach / remove (apply step)ctx.Commands.AddModifier / RemoveModifierDistinct from modifier effect math (read-only).
scope field increment / write commandsctx.Commands.IncrementScopeFieldCanonical mutation surface.

13.3 Diagnostics that enforce the taxonomy

These compiler diagnostics are reserved by this doc; their concrete implementation lands as part of the future SECS compiler (Roslyn fork) or as Roslyn analyzers running against the generated tree:

  • SECS0803 — future compiler/analyzer handoff for "command call in read-only body". Would fire when a read-only surface listed in § 13.1 lowers to CommandBuffer.*, ctx.Commands.*, ctx.FlushCommands(), ctx.CreateEntity(...), ctx.CallScopeCommand(...), or any host void-method call through ISecsHostCommands / ISecsHostWrites. Catches the "read-only body emitting a command" mistake at compile time. Earlier drafts used SECS0801; the doc now uses SECS0803 to avoid colliding with the runtime ModDiagnosticCode.InjectClosedSlot = 801 (shipped public API, immovable) and with the propagates_to where diagnostic now assigned to SECS0802. See § 21.5 for the collision history.
  • SECS0804 — future compiler/analyzer handoff for "host command without command context". Would fire when generated code reaches ISecsHostCommands without being inside a command-producing surface listed in § 13.2. Earlier drafts used SECS0802; the doc now uses SECS0804 because propagates_to where landed on SECS0802. See § 21.5.

13.4 Structural vs normative enforcement

The taxonomy mixes two kinds of enforcement:

  • Structural — the type signature of the surface excludes the command surface entirely. Examples: Formula*Delegate, TriggerDelegate, template query (only ISecsHostReads parameter). Any code that tries to enqueue commands fails to compile in C#.
  • Normative — the type signature transitively reaches the command surface (via EventContext.Tick.Commands, ActivityContext.Tick.Commands, PolicyRunContext.Tick.Commands), but the discipline forbids using it. Generated code today honors the discipline; a future compiler/analyzer pass would enforce it under the reserved SECS0803/SECS0804 ids.

A future packaging follow-up may eliminate the normative gap by introducing read-only-only context types (EventQueryContext, ActivityQueryContext, PolicyQueryContext) that physically lack Tick.Commands. That is a structural improvement, not a semantic change; the design intent is identical to today's normative discipline.

13.5 Cross-references

  • docs/design/04-behavior.md — system / event / activity / policy declarations.
  • docs/design/05-expressions.md — query / scope-method / formula signatures.
  • docs/design/09-ai-policies-and-activities.md — policy semantics and lowering surface.
  • SECS-Compiler-Plan.md — flush contract for generated command-producing bodies.
  • docs/design/10-host-secs-execution-boundary.md § 11 — invocation matrix per surface.

14. Host-callable SECS API

This section documents the public surfaces host code calls to drive SECS. Every entry point listed here lives in SECS.Engine or SECS.Abstractions and accepts typed arguments (typed ids, ScopeFrame, EntityHandle, TickContext, etc.). Host code must not reach past these surfaces into Generated/*.cs private members; the only exception is H.* hash constants and compiler-emitted typed-args record types, which are part of the SECS contract by design.

14.1 Boot sequence

The host wires SECS once at startup, in this order:

  1. var registry = new SecsRegistry();
  2. Generated.<GameName>.SecsModule.Initialize(registry); — generated entry point that calls every Register* API on the registry.
  3. registry.RegisterCandidateBuilder(...) for each candidate builder the host owns (currently runtime-only; see § 13.1 and § 17.5).
  4. registry.RegisterActivityMod(...) / registry.RegisterPolicyMod(...) for each loaded mod.
  5. var modResult = registry.FinalizeModRegistration(); — produces the merged read-only view.
  6. var diagnostics = registry.Validate(); — static analysis of the populated registry; host should warn or fail on diagnostics.HasErrors.
  7. registry.ValidateDependencies(); — formula cycle detection.

After step 5, the merged view is frozen for the rest of the process.

14.2 Tick loop

SurfaceSignatureNotes
TickPipeline.Executevoid Execute(TickContext)Single canonical tick entry. Runs all registered ITickStep instances in order.
GameRuntime.Update (host helper)void Update()Host-side after-mutation re-sync: BindingUpdater.UpdateAll() + ChannelResolver.SyncDirty(dirtySet, Bridge). Must be called after any direct world mutation that the engine has not seen.

14.3 Entity lifecycle

SurfaceSignatureNotes
TickContext.CreateEntityEntityHandle CreateEntity(TemplateId, ...) and overloads with initial fields / scope targetLowering of create_entity. Requires TickContext.IdGenerator != null.
TemplateActivator.CreateEntityHandle Create(TemplateId, long entityId, EntityHandle scopeTarget, TemplateCreationInitializer? initialize = null, IReadOnlyDictionary<ulong, ITemplateValue>? initialData = null)Host-controlled-id path (e.g. id=1 settlement, id=2 config).
TemplateActivator.CreateEntityEntityHandle CreateEntity(TemplateId, EntityHandle scopeTarget = default, IReadOnlyDictionary<ulong, ITemplateValue>? initialData = null)Generated-id path; routes through registered IEntityIdGenerator.
TemplateActivator.Destroy / DestroyWithChildrenvoid Destroy(EntityHandle) / void DestroyWithChildren(EntityHandle)Throws if entity not tracked in InstanceStore.
TemplateActivator.TransitionActivationSummary Transition(EntityHandle, TemplateId)Template upgrade along a declared transition edge. Paired with CanTransition and GetAvailableTransitions.

14.4 Template queries and commands

SurfaceSignatureNotes
SecsRegistry.EvaluateTemplateQuery<TResult>overloads accepting EntityHandle or ScopeFrame and optional in TArgsReturns the typed result. Throws on missing template / method / type mismatch.
TemplateActivator.CallMethodvoid CallMethod(TemplateId, ulong methodId, EntityHandle owner, EntityHandle scopeTarget) and typed-args overloadLowers entity.method(args). Throws if method is a query, not a command.

14.5 Activity lifecycle

SurfaceSignatureNotes
ActivityExecutor.StartActivityRun? Start(ActivityId, ActivityContext) and Start(ActivityRequest, TickContext)Returns null on CanStart == false; throws on args schema mismatch.
ActivityExecutor.StartAtOverloads for ActivityRequest + explicit clock, and ActivityId + context + explicit clock / args / originTime-banked / queued starts preserve explicit clock ticks. Request-based overloads preserve ActivityRequestOrigin; low-level id/context overloads use Unknown unless the origin overload is called.
ActivityExecutor.PreviewEffectPlan Preview(ActivityId, EntityHandle actor, EntityHandle target, TickContext, ActivityArgsBlob = default)Throws InvalidOperationException if the activity is not registered. Read-only — see § 13.1.
ActivityExecutor.Cancel / Stop / Failbool Cancel(ActivityRun, TickContext) (etc.)Returns false on already-terminal runs; transitions on success.
ActivityExecutor.Tick / TickAtvoid Tick(TickContext) / void TickAt(TickContext, int clockTick)Drives lifecycle for all live runs. Called by the pipeline step or host-controlled scheduler.
ActivityExecutor.ActiveRunsIReadOnlyList<ActivityRun>Snapshot in insertion order.
ActivityRunStore.GetByActor / TryGet / TryGetActiverun inspection by actor / id / laneAll read-only; safe in any context.
ActivityRunStore.RestoreActivityRun Restore(SecsActivity, ActivityRunState, int durationTicks)Save/load path. Validates ActivityId, SchemaId, Lane (SECS0810 / 0811 / 0812).

14.6 Policy dispatch

SurfaceSignatureNotes
PolicyDispatcher.TickPolicyDispatchResult Tick(SecsPolicy, EntityHandle actor, TickContext)Throws if slots not seeded; returns Continue / Complete / Fail. Slots seed at entity activation via the direct-call pattern in § 17.5.
PolicyExecutor.CallBestActivityCandidate? CallBest(SecsPolicy, SelectorId, PolicyRunContext)Returns the best-scored candidate or null.
PolicyExecutor.EvaluateAllRulesIEnumerable<(RuleId, RuleDecision)> EvaluateAllRules(SecsPolicy, EntityHandle, TickContext)Used for dry-run and tooling. Read-only — see § 13.1.

There is no single EvaluatePolicy(PolicyId, EntityHandle, TickContext) convenience entry today. Hosts must look up the SecsPolicy from registry.GetPolicy(policyId) and call PolicyDispatcher.Tick. A future ergonomics follow-up may add the one-call form; this is recorded as a known gap.

14.7 on_action dispatch

SurfaceSignatureNotes
TickContext.SaveScopevoid SaveScope(ulong nameHash, EntityHandle) and string overloadRequired pre-fire for every scope listed in OnActionDeclaration.ProvidedScopes.
EventDispatcher.FireOnActionvoid FireOnAction(ulong onActionId, EntityHandle target, TickContext)Throws if a ProvidedScopes entry is not pre-saved. Silent on no subscribed events.

on_action is metadata-only at the source level; firing is a host-side or system-side command. See § 11 and docs/design/04-behavior.md.

14.8 Player choice resolution

SurfaceSignatureNotes
EventDispatcher.PendingChoicesIReadOnlyList<PendingChoice>Snapshot of unresolved player choices.
EventDispatcher.ResolveChoicevoid ResolveChoice(int choiceIndex, int optionIndex, TickContext)Silent no-op on invalid indices.

14.9 Validation surfaces

SurfaceSignatureNotes
SecsRegistry.ValidateRegistryDiagnostics Validate()Warn-only static analysis. Never throws. Host should call at boot and react to HasErrors / HasWarnings.
SecsRegistry.ValidateDependenciesIReadOnlyList<string>Formula cycle detection.

14.10 Bridged read / write / command surfaces (host implements)

These are the inverse direction — SECS calls host code. Listed for completeness; full contract in § 5.

  • ISecsHostReads — read scope fields, walk graph, enumerate collections, call read-only scope methods.
  • ISecsHostWrites — push resolved-channel dirty values back to host storage.
  • ISecsHostCommands — call void scope methods that produce host-side mutation.
  • IEntityIdGenerator / IEntityCreator — host id generation and creation hooks.

14.11 Open API gaps

These are surfaces present-but-fragile or absent-but-needed. Keep them as backlog inventory, not as live contract changes.

  • AddResource bypass. GameRuntime.AddResource() calls template.Activate(scope, Bridge, buffer) directly on the internal TemplateEntry, bypassing TemplateActivator.Create's InstanceStore.Add and lifecycle validation. Future refactor: route through Activator.Create or extract an Activator.ActivateInPlace(templateId, entity, scopeTarget) for the shared-entity case.
  • registry.Validate() not called at boot. GameRuntime.Create() never invokes registry.Validate() or ValidateDependencies() after registration. Static-analysis errors are silently missed. A future host-hardening follow-up should add a startup validation call and warn or fail on HasErrors.
  • No EvaluatePolicy(PolicyId, ...) convenience entry. Host must manually look up the SecsPolicy and call PolicyDispatcher.Tick. A future follow-up may add a one-call wrapper.
  • Valenar.Generated.Templates.Skills direct sub-namespace use. examples/valenar/Host/ReadModels/CharacterSheetReadModel.cs imports a generated sub-namespace directly. This couples the host read-model to the compiler-emitted directory hierarchy. Future doc-hygiene cleanup should replace it with hash-constant access.
  • EffectPlanSnapshot.cs reflection on Valenar.Generated.H. Debugging-only reverse-name table built via reflection. Not load-bearing for correctness, but couples to a Generated type name.

These gaps do not change the committed boundary. They are tracked here so future ergonomics or host-hardening work has a stable punch list.

14.12 Cross-references

  • § 5 — Host bridge implementation contract (the inverse direction).
  • § 11 — Invocation matrix per surface.
  • § 13 — Read-only vs command-producing taxonomy.
  • docs/design/04-behavior.md — system / event / activity / policy / on_action declarations.

15. Host bridge per-method semantics

These are the concrete per-method semantics for the bridge surfaces named abstractly in § 5. For source-set restrictions see § 6. For invocation context constraints see § 13.

15.1 ISecsHostReads (14 methods)

The read bridge. Every method is read-only — no command buffer, no engine state mutation. Implementations may throw InvalidOperationException on unrecognized scope/field/method ids; the Valenar bridge does, which catches bridge-gap bugs at test time.

MethodLowering targetFailureSource-set
WalkScope(source, scopeId)walks_to edges; formula scope-walk; Base source resolutionreturns EntityHandle.Null on missdata-only mod cannot add walks (SECS0609)
GetChildren(parent, collectionId)system/event batch iteration of ScopedList/ScopedDictionaryempty span on missdata-only mod cannot add collections (SECS0609)
LookupTemplate(templateId)read-only template metadata viewreturns null silentlymods may add templates; lookup works post-register
GetChildByTemplate(parent, collectionId, templateId)keyed-by-template child lookupreturns EntityHandle.Null on misssame as GetChildren
WalkChildren(parent, collectionId)aggregate channel iteration; BuildFromCollectionempty enumerable on misssame as GetChildren
ReadInt/Long/Float/Double/Boolscope field read in formulas, triggers, system/event/activity bodies; Base channel sourcehost-defined (Valenar throws on unknown pair)mods cannot add scope fields (SECS0609)
ReadEntityEntityHandle-typed scope fieldhost-defined (Valenar throws — no entity-typed fields used)same
CallScopeQuery<TArgs, TResult>non-void scope method callhost-defined (Valenar throws on unknown method)mods cannot add scope methods (SECS0609)
ResolveChannelInt/Long/Float/Double/Boolnested channel reads in formulas (resolved value, not raw field)propagates from ChannelResolutionmods may declare and resolve channels
ResolveTemplateValueInt/Long/Float/Double/Booleffective template value with modifier effects appliedreturns type default on unknownmods may add template fields
ReadPrevTickInt/Long/Float/Double/BoolPrevious-tick bridge reads; requires ChannelDeclaration.TrackPrev = truethrows on missing required snapshot or non-tracked channelmods may declare track_prev channels

Extension WalkScopePath(source, ReadOnlySpan<ulong> path) (in SecsHostExtensions) chains WalkScope calls; null on first null hop. Not part of the interface.

Three observations:

  • ReadDouble, ReadBool, ReadEntity exist in the interface but the Valenar bridge currently maps no field to any of them. Unreachable at runtime if any double/bool/entity-typed scope field is declared without bridge wiring. Bridge implementers must wire each typed Read* they expect to be called.
  • HostBridge.AttachResolver is lazy-injected after construction (works around the bridge ↔ ChannelResolver constructor cycle). If ResolveChannel* is called before AttachResolver, the bridge returns zero silently — incorrect rather than an error. Boot order matters; see § 14.1.
  • The ReadInt-throw-on-miss pattern is by convention, not by interface contract. Prev-tick bridge reads are stricter in the live contract here: missing required snapshots should throw so bridge/setup gaps fail loudly instead of silently manufacturing zeros.

15.2 ISecsHostWrites (5 methods)

The dirty-sync write bridge. Called only from command-producing contexts during ChannelResolution.SyncDirty and from CommandProcessing applying SetScopeField/IncrementScopeField.

MethodLowering targetFailureSource-set
WriteInt/Long/Float/Double/Booldirty-sync flush of resolved channel values; command-applied field mutationhost-defined (Valenar throws on unknown pair)data-only mod-contributed channels have no host backing and are never written

WriteDouble is part of the interface but no SyncDirty path in Valenar invokes it today. Similar to the Read* observation above.

15.3 ISecsHostCommands (1 method)

The command bridge for void scope methods. Called only from command-producing contexts.

MethodLowering targetFailureSource-set
CallScopeCommand<TArgs>(target, scopeId, methodId, in args, cmds)void scope method calls in system/event/activity bodies; TickContext.CallScopeCommand; TemplateCommandContext.CallScopeCommandhost-defined (Valenar throws on unknown method)mods cannot add or patch void scope methods (SECS0609)

The CommandBuffer cmds parameter is the same buffer the engine will flush. Host implementations enqueue further field-mutation commands onto it rather than mutating host state directly when transactional safety matters.

15.4 IEntityIdGenerator (1 method)

MethodLowering targetFailureSource-set
NextEntityId() : longTickContext.CreateEntity; TemplateActivator.CreateEntity (internal)undefined; host must guarantee uniquenessN/A (host service, not in any mod)

Valenar implementation: GameWorld itself implements this and is passed as the id generator. Counter starts above 100 to avoid collision with host-controlled boot ids (id=1 settlement, id=2 config).

15.5 IEntityCreator (4 methods)

MethodLowering targetFailureSource-set
CreateEntity(id, contractId, scopeTarget, templateId)TemplateActivator.Activate; TickContext.CreateEntity; TemplateCommandContext.CreateEntityhost-defined (Valenar EntityFactory throws on unknown contractId)N/A (host service)
SetEntityName(entity, name)TemplateActivator.Activate when TemplateEntry.Name != null; SwapTemplateundefined; Valenar guards against overwriting richer host names with placeholder template namesN/A
SetEntityTemplate(entity, templateId)TemplateActivator.SwapTemplate (template swap at runtime)undefined by interface; host must update any channel-source references keyed on the template idN/A
DestroyEntity(entity)TemplateActivator.Destroy and DestroyWithChildren (after engine has removed channel sources and bindings)host-definedN/A

There is no committed ctx.DestroyEntity(handle) surface on TickContext (F2 in § 11 is PARTIAL). The only committed destruction path is host-direct TemplateActivator.Destroy / DestroyWithChildren.

15.6 Bridge wiring and validation

The host wires the five interfaces in GameRuntime.Create (or its game-specific equivalent):

  1. HostBridge bridge = new HostBridge(world) implements ISecsHostReads, ISecsHostWrites, ISecsHostCommands.
  2. bridge.AttachRegistry(registry) enables LookupTemplate.
  3. bridge.AttachResolver(channelResolver) enables ResolveChannel* and ResolveTemplateValue*.
  4. bridge.AttachTemplateValues(templateValues) enables template-value resolver delegation.
  5. bridge.AttachPrevTickStore(prevTickStore) enables ReadPrevTick*.
  6. EntityFactory entityFactory = new EntityFactory(world) implements IEntityCreator.
  7. world (GameWorld) implements IEntityIdGenerator directly.
  8. TemplateActivator(...) constructor wires reads, commands, creator, idgen.
  9. TickContext { Host = bridge, Commands = cmd, IdGenerator = world, ... } named-property init.

There is no compile-time or runtime "all required bridge methods implemented" check beyond C# interface satisfaction. Missing (scopeId, fieldId) coverage is a runtime InvalidOperationException at the first call site. The RegistryDiagnostics surface (§ 14.9) validates declaration metadata but does not probe the bridge.

15.7 Source-set restriction enforcement (cross-reference to § 6)

DiagnosticStatusNotes
SECS0604Implementedsrc/SECS.Engine/Modding/ModDiagnosticCode.cs — operation-kind/declaration-kind mismatch in mod registry.
SECS0609Compiler-phase-onlyCited in docs/design/01-world-shape.md:253-272 and § 6 above as the data-only-mod-declares-host-surface diagnostic. No ModDiagnosticCode / runtime enum entry is applicable: ModRegistry handles activity/policy patches after lowering and never sees source-set declarations for scopes, fields, collections, scope methods, walks_to, existing contract method/lifecycle surfaces, or on-action provides entries.

Today, no runtime enforcement of the source-set boundary exists, by design. The future SECS compiler (Roslyn fork) is the intended enforcement point. A data-only mod that somehow introduced a new scope field would generate a ReadInt call with an unrecognized field hash, which would throw InvalidOperationException in Valenar's bridge — incidental coverage, not a designed guard.

15.8 Cross-references

  • § 5 — abstract host bridge contract.
  • § 6 — host-capable vs data-only mod boundary.
  • § 11 — invocation matrix per surface.
  • § 13 — read-only vs command-producing taxonomy.
  • § 14 — host-callable SECS API (the inverse direction).
  • docs/design/01-world-shape.md:253-272 — source-set restrictions.

16. Universal ActivityRequest model

This section formalizes the activity request / candidate / run / save model used by the current runtime and hand-written stand-ins.

16.1 ActivityRequest — the single handshake type

public readonly record struct ActivityRequest(
ActivityId ActivityId,
EntityHandle Actor,
EntityHandle Target,
ActivityArgsBlob Args,
ActivityRequestOrigin Origin)

5 fields. Three shorter convenience overloads exist for legacy / best-effort call sites (no-target, no-args, 4-arg form); all delegate to the 5-field constructor and default the missing fields to EntityHandle.Null / ActivityArgsBlob.Empty / ActivityRequestOrigin.Unknown. Live player, policy, system, event, and mod producers should use the five-field form or a producer-specific wrapper that supplies origin. Source: src/SECS.Engine/Activities/ActivityRequest.cs.

16.2 ActivityRequestOrigin

public enum ActivityRequestOrigin
{
Unknown = 0,
Player,
Policy,
System,
Event,
Mod,
}

6 values. No expansion to Host / Activity / TemplateMethod / Debug / Test is committed; any such addition requires an explicit ADR. Source: src/SECS.Engine/Activities/ActivityRequestOrigin.cs.

Producer contract. Unknown is a legacy / best-effort value for direct executor call sites that do not own a declared producer boundary. Live public producers stamp their boundary explicitly: Valenar's player queue starts through ActivityRequestOrigin.Player; PolicyDispatcher.ProcessCall creates policy-origin requests; and PolicyDispatcher.ProcessCallBest normalizes the selected candidate request to ActivityRequestOrigin.Policy before start. This is runtime/save provenance, not a client-visible field by default.

16.3 ActivityCandidate — the policy decision shape

public sealed record ActivityCandidate(
ActivityRequest Request,
EffectPlan? Preview,
float PolicyScore,
PredictionConfidence ScoreConfidence,
CandidateStatus Status)

CandidateStatus lifecycle: Available → Filtered | Selected → Started | Rejected. ActivityCandidate.Empty(request) produces a zero-score, null-preview Available candidate. Used by PolicyExecutor.CallBest and BuildCandidates. Source: src/SECS.Engine/Activities/ActivityCandidate.cs, CandidateStatus.cs.

16.4 ActivityRun and ActivityRunState — runtime + persist identity

ActivityRun is the live runtime object. Its constructor is internal — only ActivityExecutor.StartAt (live start) and ActivityRun.FromState (save/load restore) can produce instances.

ActivityRunState is the 11-field persist record:

public sealed record ActivityRunState(
ActivityRunId RunId,
ActivityId ActivityId,
EntityHandle Actor,
EntityHandle Target,
ActivityLaneId Lane,
ActivityRunStatus Status,
ActivityArgsBlob Args,
ActivityRequestOrigin Origin,
ActivityRunId ParentRunId,
ActivityRunId ChildRunId,
int TicksElapsed)

ToState() projects exactly these 11 fields. FromState() round-trips them, including Origin. Restore never fabricates provenance from the host/load call site; a restored run keeps the origin saved by the original live start.

ActivityRunStatus: 5-value enum — Running, Completed, Stopped, Canceled, Failed.

ActivityRunStore.Restore enforces three hard preconditions before reconstruction:

  • SECS0810ActivityId mismatch (the recorded id does not match the activity registered today).
  • SECS0811ExpectedArgsSchemaId mismatch (the args blob's schema does not match what the activity declares).
  • SECS0812 — zero Lane (lane id must be non-zero).

These are stable id-based checks; restore never indexes by array position.

16.5 ActivityLaneId, ActivityLanePolicy, and concurrency slot identity

ActivityLaneId(ulong Value). None = 0. FNV-1a-64 of a wire id string via ActivityLaneDeclaration.Create(string).

ActivityLanes ships 5 built-in declarations: Primary, CombatEncounter, CombatActivity, Movement, Interaction. Hosts may declare additional lanes through their content; the engine treats each ActivityLaneId as opaque.

ActivityLanePolicy: 3-value enum.

  • Reject — implemented. The default SecsActivity.LaneOccupiedPolicy. Executor enforces at both CanStartAt and StartAt.
  • Queue — RESERVED. Throws NotImplementedException. No silent fallback.
  • Preempt — RESERVED. Throws NotImplementedException. No silent fallback.

The reserved policies are committed enum values; their semantics are deferred until queue / preempt mechanics are designed.

16.6 ActivityArgsBlob and IActivityArgs

ActivityArgsBlob(ulong SchemaId, ReadOnlyMemory<byte> Payload). Empty = (0, empty). Decode<T>() verifies SchemaId against the target type before decoding. Encode<T>(T) static factory builds the blob from a T : IActivityArgs<TSelf>.

IActivityArgs<TSelf> (CRTP):

  • ulong SchemaId { get; }
  • ReadOnlyMemory<byte> Encode()
  • static abstract TSelf DecodeFrom(ActivityArgsBlob blob)

Concrete example: CastSpellArgs(TemplateId SpellDefinitionId). SchemaId = FnvHash.Compute("CastSpellArgs(TemplateId)"). Payload is 8-byte little-endian TemplateId.Value.

SecsActivity.ExpectedArgsSchemaId (virtual, defaults 0UL) binds an activity to its args record's SchemaId. Activities decoding typed args MUST override this. Mismatch is enforced at:

  • ActivityExecutor.StartAtInvalidOperationException at start boundary.
  • ActivityRunStore.RestoreSECS0811 at save/load boundary.

16.7 One core start path

There is exactly one implementation of activity start: ActivityExecutor.StartAt(ActivityId activityId, ActivityContext context, int clockTick, ActivityArgsBlob args, ActivityRequestOrigin origin) (src/SECS.Engine/Activities/ActivityExecutor.cs).

The public start surface funnels into this implementation:

Public methodDelegates to
Start(ActivityId, ActivityContext)StartAt(id, ctx, tick.TickNumber)
Start(ActivityRequest, TickContext)StartAt(req, tick, tick.TickNumber)
StartAt(ActivityRequest, TickContext, int)builds ActivityContext, calls StartAt(req.ActivityId, ctx, clockTick, req.Args, req.Origin)
StartAt(ActivityId, ActivityContext, int)StartAt(id, ctx, clockTick, ActivityArgsBlob.Empty)
StartAt(ActivityId, ActivityContext, int, ActivityArgsBlob)StartAt(id, ctx, clockTick, args, ActivityRequestOrigin.Unknown)
StartAt(ActivityId, ActivityContext, int, ActivityArgsBlob, ActivityRequestOrigin)the core implementation
Execute(ActivityId, ActivityContext)wraps Start and returns bool
ExecuteAt(ActivityId, ActivityContext, int)wraps StartAt and returns bool

The pipeline executed by the core implementation:

  1. CanStartAt (read-only validation; see § 13).
  2. ExpectedArgsSchemaId validation against args.SchemaId.
  3. GetDurationTicks (read-only).
  4. DeductCosts (command-producing).
  5. ApplyCooldown (command-producing).
  6. new ActivityRun (internal constructor invocation, including origin).
  7. runStore.Add(run).
  8. OnStart(run, runContext) (command-producing).
  9. Terminal-lifecycle handling for instant activities (OnComplete/OnFail/OnStop/OnCancel) and runStore.Remove if instant.

There is no second start path. The host UI, player queue, policy dispatcher, event handlers, system bodies, mods, debug tools, and tests all funnel through the same six public entry methods, all delegating to the same private StartAt.

16.8 No separate AI / player / host hidden semantics

The orchestration prompt's Discipline 8 ("one core executor path") is satisfied:

  • ActivityRun constructor is internal and is invoked from exactly one place: the core StartAt. The only other producer of ActivityRun instances is FromState (save/load restore).
  • runStore.Add is called from exactly two places: the core StartAt (live start) and ActivityRunStore.Restore (save/load).
  • The host's PlayerActivityQueue does not own or sequence runs separately; it builds an ActivityRequest with ActivityRequestOrigin.Player, calls executor.StartAt(request, tick, activityClockTick), and reads back run.RunId for queue tracking.
  • The policy PolicyDispatcher.ProcessCall / ProcessCallBest calls executor.Start(request, tick) with ActivityRequestOrigin.Policy — same path.
  • Tests use the same public methods as host code.

If a future requirement demands per-origin start hooks (e.g. analytics, audit log, pre-start cancellation policy), they must be added inside the core StartAt and may branch on request.Origin. Adding a parallel start method that bypasses the core pipeline would be an architectural failure caught by this section's contract.

16.9 Cross-references

  • § 5 — host bridge contract (the read/write/command surfaces the activity body uses).
  • § 11 — invocation matrix per surface. Activity surfaces appear under Group B.
  • § 13 — read-only vs command-producing taxonomy. Each lifecycle method is classified there.
  • § 14 — host-callable SECS API. § 14.5 lists the public start/preview/cancel surface.
  • src/SECS.Engine/Activities/ActivityExecutor.cs — the single implementation.
  • src/SECS.Engine/Activities/ActivityRequest.cs, ActivityRequestOrigin.cs, ActivityCandidate.cs, ActivityRun.cs, ActivityRunState.cs, ActivityLaneId.cs, ActivityLanePolicy.cs, ActivityArgsBlob.cs, IActivityArgs.cs — the model types.

17. Policy / slot / candidate-builder

Documents the policy-as-request-producer model, the no-fallback slot invariant, the CandidateBuilder runtime-only surface, and the content-family parameterization that prevents content explosion.

17.1 Policy as request producer

SecsPolicy.EvaluateRule(PolicyRunContext) → RuleDecision is read-only (see § 13.1). The dispatcher is the only site that translates a decision into a side effect.

PolicyDispatcher.Tick(SecsPolicy, EntityHandle actor, TickContext):

  1. Calls policyExecutor.EvaluateAllRules(policy, actor, tick) — read-only iteration over rule slots.
  2. Translates each RuleDecision into one of: Continue (advance), Complete (stop, success), Fail (stop, failure), Wait (keep child, advance), Call(activityId, target, args)ActivityExecutor.Start, CallBest(selector)policyExecutor.CallBestActivityExecutor.Start, or CancelChildActivityExecutor.Cancel.

The policy itself never starts or cancels an activity. Policy is one of the six request producers (see § 16.2 — ActivityRequestOrigin.Policy). Source: src/SECS.Engine/Policies/PolicyDispatcher.cs:59, PolicyExecutor.cs:91.

17.2 No-fallback slot model

ScopeSlotStore.Read<T>(actor, slot) throws InvalidOperationException on missing slot or type mismatch. There is no ResolveSlotOrDefault method in the engine — grep confirms zero hits. PolicyExecutor.RequireSlot<T> calls store.Contains first; if not seeded it throws with an actionable message pointing at the seed helper.

SecsPolicy.Rules / Needs / Selectors properties are compile-time seed sources only — used as the initial argument to store.Initialize<T>(actor, kind, policy.Rules) inside the generated <ScopeName>Slots.SeedDefaults helper. They are not fallback read paths at evaluation time.

If a policy's slots are not seeded for an actor when PolicyDispatcher.Tick runs, the throw is loud and immediate. Sources: ScopeSlotStore.cs:31-45, PolicyExecutor.cs:117-140.

17.3 CandidateBuilder runtime-only surface

public delegate IReadOnlyList<ActivityRequest> CandidateBuilderDelegate(
EntityHandle actor, SlotKindId slot, TickContext tick);

Registered at boot via registry.RegisterCandidateBuilder(builderId, delegate). Looked up at evaluation time by policyExecutor.CallBest when SelectorSource is FromCollection(SlotKindId Slot, CandidateBuilderId Builder).

There is no committed .secs source syntax for declaring this bridge. Today it is a C# runtime/lowering surface implemented in hand-written stand-ins under Generated/Policies/, carrying the canonical no-source marker.

Unregistered or zero CandidateBuilderId throws at evaluation time. Source: src/SECS.Engine/Policies/PolicyExecutor.cs:186-196.

17.4 Content-family parameterization

The spell family is the canonical example:

  • Activity_CastSpell is one of seven allowlisted generic mechanic activities (see docs/design/behavior-vocabulary.md § Group D). It is registered exactly once.
  • CharacterSlots.KnownSpells is a SlotKindId slot whose elements are TemplateId values — one per known SpellDefinition template. The slot stores typed data ids, not activity ids.
  • KnownSpellsCandidateBuilder.Build (Generated/Policies/KnownSpellsCandidateBuilder.cs) reads tick.SlotStore.Read<TemplateId>(actor, slot) and produces one ActivityRequest(H.Activity_CastSpell, actor, EntityHandle.Null, encodedArgs, ActivityRequestOrigin.Policy) per entry, where encodedArgs = ActivityArgsBlob.Encode(new CastSpellArgs(spellId)).

Adding a new spell:

  1. Add a SpellDefinition template in Content/spells/.
  2. Append its TemplateId to the actor's KnownSpells slot at activation.

No new SecsActivity declaration is required. The pattern generalizes: RecipeDefinition + CraftRecipe, Building template + BuildStructure, ItemDefinition + UseItem, SkillDefinition + TrainSkill, WorkOrderDefinition + PerformWorkOrder, RitualDefinition + PerformRitual.

The bad pattern this prevents: KnownSpells storing ActivityId values and one Activity_FrostBolt / Activity_FireBolt / etc. per spell. That would be content explosion ("200 spells = 200 activities"), explicitly banned in docs/design/behavior-vocabulary.md § Group D.

17.5 Slot seeding ownership

Verdict: endorse the direct-call pattern. No ISecsHostSlotSeeder interface is needed.

Slot seeding is a generated-system or generated-code responsibility within the tick pipeline, not a host bridge concern. The canonical call site is a SECS ISystem body (typically a world-initialization or actor-activation system) that calls the generated static helper:

// In Generated/Systems/.../SomeActorActivationSystem.cs:
CharacterSlots.SeedDefaults(ctx.SlotStore, character, survivalPolicy);

CharacterSlots.SeedDefaults is itself a hand-written stand-in (carrying a real // Source: Content/characters/slots.secs provenance) that the future SECS compiler would emit from a template<RuleSlot> / template<NeedSlot> / template<KnownSpells> block in .secs source. The helper performs four tick.SlotStore.Initialize<T>(actor, slot, policy.X) calls — one per architectural slot kind.

Why direct-call wins:

  1. The caller is generated SECS code, not raw host code. The valenar production call site (Generated/Systems/World/MapGenerationSystem.cs:152) is compiler-emitted system code. Adding a host bridge interface would route a generated-to-engine call through host indirection for no benefit.
  2. The engine never drives seeding. The seeding direction is always generated/system code → engine slot store. An ISecsHostSlotSeeder interface only makes sense when the engine needs to call out to host code; here it is a passive receiver of typed slot inits.
  3. A lifecycle phase would violate game-agnosticism. "Seed policies X and Y for any Character" is a game-specific decision; the engine is game-agnostic. The decision belongs in <ScopeName>Slots.SeedDefaults, not in an engine lifecycle hook.
  4. No-fallback invariant fully satisfied. If seeding is skipped, RequireSlot throws immediately at PolicyDispatcher.Tick with an actionable error message naming the missing slot and the seed helper. Tests catch this loudly.

ScopeSlotStore itself lives in SECS.Engine. Even if future packaging work adds narrower query-only interfaces elsewhere, the seeding call sites remain the same.

17.6 Cross-references

  • § 11 E4 / E5 — slot read / slot write surfaces in the invocation matrix.
  • § 13.1 — policy rule Decision listed as read-only.
  • § 14.6 — PolicyDispatcher.Tick host-callable surface.
  • § 16 — universal ActivityRequest model and the one core start path.
  • src/SECS.Engine/Policies/PolicyDispatcher.cs, PolicyExecutor.cs, CandidateBuilder.cs, SelectorSource.cs, Slots/ScopeSlotStore.cs.
  • examples/valenar/Generated/Slots/CharacterSlots.csSeedDefaults reference implementation.
  • examples/valenar/Generated/Policies/KnownSpellsCandidateBuilder.cs — content-family parameterization reference.

18. Template / contract / entity creation boundary

Formalizes the lifecycle ordering for entity creation, activation, and lifecycle methods. Cross-references § 11 A8-A11, § 14.3, § 14.4, § 17.5.

18.1 Entity creation paths

Four creation paths exist, all converging on TemplateActivator.CreateCore:

  1. TemplateActivator.Create(templateId, entityId, scopeTarget, ...) — host-controlled id. Used at boot for fixed-id entities (e.g. settlement id=1, config id=2).
  2. TemplateActivator.CreateEntity(templateId, scopeTarget, ...) — generated-id path via IEntityIdGenerator.
  3. TickContext.CreateEntity(templateId, ...) — lowering of SECS create_entity. Delegates to path 2. Requires IdGenerator != null.
  4. ITemplateCommandContext.CreateEntity(templateId, ...) — inside lifecycle / command method bodies. Flushes pending commands first, then delegates to path 2 to avoid mid-activation command-buffer entanglement.

18.2 Activation phase ordering

CreateCore runs these phases atomically from the caller's perspective:

  1. ApplyInitialState — only SetScopeField commands. Validates only owner or root are targeted. Throws if any channel source command appears. Flush isolated to initial-state commands.
  2. SetEntityName — only if template.Name != null.
  3. RunActivationBodytemplate.Activate(scope, host, cmds) runs in a dedicated cmds buffer. Channel sources must target scope.Root only. Cross-scope effects must use named modifiers — validated by ValidateActivationChannelSources. Buffer flushes after.
  4. InstanceStore.Add — entity becomes tracked.
  5. RunLifecycle(Activation) — looks up ContractLifecycleIds.Activation binding. If binding exists and template implements the matching method, invokes via fresh TemplateCommandContext and flushes after. If no binding or method missing, silent skip (returns default).

The five phases use separate command buffers; nothing carries between them.

For destruction (Destroy):

  1. RunLifecycle(Deactivation).
  2. RemoveAllChannelSources + RemoveAllBindings + RemoveAllBindingsOnTarget with flush.
  3. InstanceStore.Remove.
  4. IEntityCreator.DestroyEntity.

18.3 Host-callable vs SECS-callable call shapes

OperationDelegate signatureContext
Template query (read-only)(ScopeFrame, ISecsHostReads, in TArgs) → TResultNo TickContext; no command buffer
Activation delegate(ScopeFrame, ISecsHostReads, CommandBuffer)Direct cmds; no TickContext
Template command method (method void)(ITemplateCommandContext, in TArgs)CommandBuffer accessible via context; no TickContext

Both host-callable and SECS-callable paths land on the same registered delegate with the same SecsTypeRef-driven type check.

  • Host-callable query: SecsRegistry.EvaluateTemplateQuery<TArgs, TResult>(templateId, queryId, ScopeFrame|EntityHandle, in args, host).
  • SECS-callable query (from formula / event-condition / read-only body): ISecsHostReads.CallScopeQuery<TArgs, TResult>(target, scopeId, methodId, in args).
  • Host-callable command: TemplateActivator.CallMethod<TArgs>(templateId, methodId, owner, scopeTarget, in args). Constructs TemplateCommandContext, validates args, invokes, flushes.
  • SECS-callable command (from command-producing context): ITemplateCommandContext.CallScopeCommand<TArgs>(target, scopeId, methodId, in args) or TickContext.CallScopeCommand<TArgs>(...).

The query/command discriminator is ContractMethodDeclaration.IsQuery + ReturnType.IsVoid. Calling a void method through a query path or a non-void method through a command path throws.

18.4 ScopeFrame construction rules

ScopeFrame has three named constructors and no empty factory:

ConstructorWhen
ScopeFrame.ForInstance(owner, root)Standard case: activation, lifecycle methods, CallMethod, Transition, deactivation. Owner / This = the entity; Root / Current = scope root.
ScopeFrame.ForCreation(root)Pre-instance queries — EvaluateTemplateQuery with EntityHandle scopeTarget overload. Owner = EntityHandle.Null, HasOwner = false.
ScopeFrame.WithCurrent(current)Iteration lowering inside generated bodies; changes Current without changing Owner or Root.

Host code never constructs ScopeFrame directly for activation; TemplateActivator does it internally. Host constructs ScopeFrame.ForCreation(scopeTarget) only for pre-instance eligibility queries (e.g. "can this template be placed under this parent?").

The zero-value ScopeFrame struct (all-null handles) is invalid for all surface calls.

18.5 registry_only contracts — current state vs design intent

A registry_only contract qualifier marks a contract whose templates are pure data (template fields + structured fields), with no entity instantiation. Canonical examples: SpellDefinition, RecipeDefinition.

Design intent (docs/design/02-templates.md:772-831):

  • ContractDeclaration.IsRegistryOnly = true.
  • TemplateEntry.Activate = null.
  • Engine skips activation; create_entity rejects registry-only template ids.
  • Diagnostic SECS0213 blocks queries / methods / lifecycle bindings on registry-only contracts.

Current implementation:

  • ContractDeclaration.IsRegistryOnly does NOT yet exist as a property.
  • SecsRegistry.ValidateTemplateEntry requires Activate != null — registry-only templates wire a NoOpActivate delegate as a workaround.
  • Current content follows the intended metadata-only convention: registry-only-style contracts keep LifecycleBindings, QueryMethodIds, and MethodIds empty, so RunLifecycle finds no binding and silently skips. No entity is materialized only because host/content code avoids CreateEntity; the engine does not enforce that yet.
  • create_entity rejection for registry-only template ids is not enforced today. Closing this gap requires adding IsRegistryOnly to ContractDeclaration and a guard in TickContext.CreateEntity / TemplateActivator.CreateCore. This remains future engine work.

18.6 Missing lifecycle / missing method behavior

CaseBehavior
Contract has no binding for a lifecycle id (e.g. Deactivation)contract.GetLifecycleMethodId(id) returns 0 → RunLifecycle returns default. Silent no-op.
Contract has the binding but template does not implement the methodtemplate.Methods.TryGetValue returns false → RunLifecycle returns default. Silent no-op.
CallMethod on a method the template does not implementTemplateActivator.CallMethod throws InvalidOperationException naming the missing method id.
EvaluateTemplateQuery on a query the template does not implementSecsRegistry.EvaluateTemplateQuery throws InvalidOperationException naming the missing query id.
Channel source command emitted from a method void bodyTemplateCommandValidation.RejectMethodChannelSources throws at flush time. Channel sources are activation-only.
ApplyInitialState emitting any command other than SetScopeField targeting owner / rootThrows at flush time.

Lifecycle methods are optional by design — templates only implement the bindings they need. Direct method calls fail loudly because the caller named a specific method id; the contract is "name a method that exists or fail."

18.7 No-fallback invariant for templates and entities

  • SecsRegistry.GetTemplate(id) throws InvalidOperationException on unregistered template id. No engine-synthesized fallback.
  • SecsRegistry.GetDefaultTemplate(scopeId) returns TemplateId.None when no default registered; TickContext.Spawn(scopeId) returns EntityHandle.Null in that case. No silent fabrication of a "bare" template.
  • "Bare" templates in valenar (BareCharacter, BareLocation, etc.) are explicit registered templates that the host registers as defaults. They are content, not engine fallbacks.
  • ScopeSlotStore.Read<T> throws on uninitialized slot — see § 17.2.

If no template is registered for a scope id and Spawn(scopeId) is called, the result is EntityHandle.Null — host code must check and fail rather than relying on the engine to fabricate something.

18.8 Cross-references

  • § 11 A8 (template activation), A9 (template deactivation), A10 (template query), A11 (template command method).
  • § 14.3 (entity-lifecycle host-callable surfaces).
  • § 14.4 (template-query / template-command host-callable surfaces).
  • § 17.5 (slot seeding ownership; the post-activation step that runs in a generated system body).
  • src/SECS.Engine/TemplateActivator.cs, TemplateCommandContext.cs, TemplateCommandValidation.cs, Instances/TemplateEntry.cs, Instances/InstanceStore.cs.
  • src/SECS.Abstractions/Contracts/ContractDeclaration.cs, ContractMethodDeclaration.cs, ContractLifecycleIds.cs, LifecycleBindingDeclaration.cs.
  • src/SECS.Abstractions/Scopes/ScopeFrame.cs.
  • docs/design/02-templates.md § Templates under registry_only contracts.

19. Systems / events / on_actions ordering

Specifies dispatch ordering, subscriber command visibility, and direct-call legality for the three command-producing dispatchable surfaces. Cross-references § 11.1 (A1-A11, F6-F8), § 14.7-14.8, § 13.

19.1 System execution ordering

TickPipeline.Execute(TickContext) is the single canonical tick entry. Source: src/SECS.Engine/Pipeline/TickPipeline.cs:156.

On the first call, Materialize() stable-sorts all SystemStep entries:

  1. Primary key: phaseRank ascending (lower = earlier).
  2. Secondary key: original registration index (preserves registration order within the same phase).

Phase rank is built by UseRegisteredPhases(registry) from registry.OrderedPhases(), which itself is populated from Generated/Declarations.cs phase declarations. Systems without a defined phase (PhaseId.None) sort after all phased systems.

Non-SystemStep entries — ContractTickStep, EventPulseStep, custom ITickStep — are NOT moved by Materialize. They remain at their literal insertion position. This means inserting a custom step between two systems in the same phase gives that step a deterministic place in the materialized order.

SystemStep.Execute flow (SystemStep.cs:26-63):

  1. Frequency gate: ctx.TickNumber % frequency == 0 (or run-once gate for SystemFrequency.Once = -1).
  2. system.Execute(ctx).
  3. ctx.FlushCommands() on success path.
  4. ctx.DiscardCommands() on exception path, then rethrow.

Frequency is resolved at registration time from registry.AllTickRateDeclarations() and is fixed for the system's lifetime.

System direct-call legality. Systems are not host-callable outside the pipeline by any documented API. The interface ITickSystem.Execute(TickContext) is technically public, but no host or test in the codebase calls it directly. Treat direct calls as architecturally undefined: not guarded by the engine, not documented as a host surface.

SECS-callable systems. No surface starts a system from inside another body. There is no ctx.StartSystem(...) API. Cross-system invocation must go through fire-event or fire-on_action.

19.2 Event pulse evaluation ordering

Pulse events are evaluated in registration orderEventDispatcher.pulseEvents is a plain List<EventEntry> with no sort applied. Source: src/SECS.Engine/Events/EventDispatcher.cs:19, 134.

Per entity per pulse, EventDispatcher.TickPulse runs:

  1. Frequency gate: ctx.TickNumber % evt.Frequency == 0.
  2. Chance roll: ctx.Random.Next(100) >= evt.Chance short-circuits the event for this entity.
  3. Condition body — read-only by enforcement (commands discarded in finally). See EvaluateCondition lines 344-354.
  4. If Condition returns true: BuildOptions (read-only) — InvokeBuildOptions lines 356-366.
  5. If BuildOptions produced any options: park a PendingChoice (see § 19.5).
  6. If no options: Execute body — command-producing — InvokeEventBody lines 368-380.

Execute and option handlers call ctx.FlushCommands() after the body and ctx.DiscardCommands() on throw.

There is no committed ctx.FireEvent(eventId) API for direct programmatic fire of a specific pulse event. Pulse events fire only through the pipeline's EventPulseStep. Cited as F6 NOT-COMMITTED in § 11.6.

19.3 on_action subscriber ordering and command visibility

EventDispatcher.FireOnAction(onActionId, target, ctx) at src/SECS.Engine/Events/EventDispatcher.cs:166 is the single firing surface. Selectable callers: host code outside the pipeline (e.g. GameRuntime.OnBuildingComplete), system bodies, event Execute and option handlers, activity lifecycle hooks. All must hold a live TickContext.

Required pre-fire. For every scope id named in OnActionDeclaration.ProvidedScopes, the caller must call ctx.SaveScope(nameHash, entity) before FireOnAction. Missing scope throws InvalidOperationException at fire time. FireOnAction calls ctx.FlushCommands() at entry, then pushes a child saved-scope frame from the pre-fire pending scopes.

Subscriber order. Subscribers (events that subscribe to this on_action) are sorted by EventEntry.Priority ascending. Lower priority value fires first. Default priority = 0; negative priorities fire before zero, positive after. The runtime sorts the list at every RegisterEvent call (line 50). Today, the only ordering guarantee is that lower priorities run before higher priorities; callers must not rely on registration-order preservation among equal-priority subscribers because the runtime uses List<T>.Sort, which does not guarantee stability.

Synchronous, in-tick dispatch. FireOnAction runs all selected subscribers inline within the calling thread of execution. There is no deferred queue for on_action subscribers.

Selection mode. OnActionDeclaration.SelectionMode controls how many of the priority-sorted subscribers actually run:

  • All — every subscriber whose Condition passes runs.
  • FirstValid — the first subscriber (by priority) whose Condition passes runs; the rest are skipped.
  • WeightedRandom — exactly one subscriber is chosen, weighted by EventEntry.Weight.

Selection mode does not change ordering for the subscribers that do fire.

Command visibility between subscribers. Subscribers do NOT share a live command buffer. Each subscriber's Execute body calls ctx.FlushCommands() before the next subscriber starts. Subscriber N+1 sees the committed world state after subscriber N's commands have been applied — it does not see N's buffered-but-unflushed commands.

This means subscribers are a sequence, not a transaction. If subscriber N adds a modifier and subscriber N+1 reads the modified channel, N+1 will see the new value. Author intent must take this into account when assigning priorities.

Saved-scope visibility within a dispatch frame. All subscribers of a single FireOnAction dispatch share the same pushed child frame. A save_scope_as performed by subscriber N is visible to subscriber N+1 in the priority order. Scope state accumulates across the dispatch and is popped when FireOnAction returns.

on_action has no effect bodies. OnActionDeclaration is metadata-only: OnActionId, Name, ScopeId, ProvidedScopes, SelectionMode, plus reserved runtime-only fields (ChainedOnActionIds, FallbackOnActionId). All effects belong to subscribed events. Reserved fallback / chaining fields are stored as deferred metadata only: live dispatch ignores them, and the registry does not validate them as active follow-on links.

19.4 Host-outside-tick fire legality

FireOnAction is callable from any code holding a live TickContext. The host pattern:

ctx.SaveScope(H.Location, location);
ctx.SaveScope(H.Building, entity);
ctx.Events.FireOnAction(H.OnAction_BuildingComplete, settlement, ctx);

There is no engine guard restricting FireOnAction to inside-tick. The host stores TickCtx on the runtime and may fire on_actions between ticks (e.g., GameRuntime.OnBuildingComplete fires after a synchronous host operation). FireOnAction's entry-time FlushCommands ensures any pre-existing buffered commands are applied before the dispatch begins.

If the host wants to defer firing to inside the next tick, it must implement that deferral itself; the engine does not.

19.5 Pending choice save/load identity

PendingChoice carries:

  • EventId (ulong, FNV-1a-64 of the event canonical id string) — stable across saves if the event's canonical identity is unchanged.
  • EventName (string) — display only.
  • Target (EntityHandle) — the entity the event was targeting at fire time.
  • Options (IReadOnlyList<EventOption>) — built at fire time, held in memory.
  • SavedScopes (Dictionary<ulong, EntityHandle>?) — snapshot of the dispatch frame at queue time.

Pending choices are NOT currently persisted. The pending-choice queue is in-memory only. After a save/load, all pending choices are lost. GameSnapshotBuilder projects pending choices to a PendingChoiceSnapshot DTO for transport to the UI, but there is no deserialization path back into live PendingChoice objects.

Option identity within a choice uses integer index (optionIndex parameter to ResolveChoice) rather than a stable string key. If event content changes between saves (option added or removed at a different index), index-based deserialization would silently misalign options. Section 20.3 defines the replacement protocol.

A future save-protocol design must decide whether to persist pending choices, and if so what option identity to use (stable key string, hash of option label, or accept the "lose pending choices on save/load" semantic as final).

19.6 Cross-references

  • § 11.1 A1 (system Execute), A2 (Condition), A3 (pulse Execute), A4 (BuildOptions), A5 (option handler), A6 (on_action declaration), A7 (on_action fire), F6 (fire-event-by-id NOT-COMMITTED).
  • § 13.1 / § 13.2 — read-only vs command-producing taxonomy. Condition, BuildOptions are read-only by enforcement; Execute, option handler, on_action firing are command-producing.
  • § 14.7 — on_action dispatch host-callable surface table.
  • § 14.8 — player choice resolution.
  • § 20.2 — save payload protocol (covers pending-choice persistence).
  • src/SECS.Engine/Pipeline/TickPipeline.cs:207-241Materialize.
  • src/SECS.Engine/Pipeline/SystemStep.cs:26-63 — system frequency + flush.
  • src/SECS.Engine/Events/EventDispatcher.cs:50 — subscriber priority sort.
  • src/SECS.Engine/Events/EventDispatcher.cs:166FireOnAction.
  • src/SECS.Engine/Events/EventDispatcher.cs:344-380 — Condition / BuildOptions / Execute discriminators.
  • src/SECS.Engine/Events/PendingChoice.cs — choice carrier.

20. Save-load / determinism / reentrancy

Documents the per-surface save-load contract, the deterministic-replay contract, and the reentrancy boundary.

20.1 Per-surface save-load matrix

SurfacePersisted today?Stable identityFailure mode on restore mismatchDeterministic on replay?
ActivityRunStorePARTIAL — ToState/Restore existsActivityRunId (monotonic ulong) + seed via NextRunIdSeedSECS0810 / 0811 / 0812 throwsYes given seed
ScopeSlotStoreYES — Snapshot/Restore(EntityHandle, SlotKindId)SECS0820 / 0821 throws on bad inputYes
PrevTickSnapshotStoreNO — re-derived after first tick(EntityHandle, ChannelId)missing snapshot should throw until warm-up has produced oneYes after one tick warm-up
PendingChoice queueNO — implementation gap(today: array index; future candidate: EventOption.Key string)silent wrong choice todayNo until a future save protocol lands
ModifierBindingStoreNO — biggest gap(Owner, Target, ModifierId)N/A — no surface todayNo
PolicyDispatcher.activeChildrenNO — gap(EntityHandle, PolicyId) → ActivityRunIdN/ANo
TickContext.RandomNO — host owns seedseed intdepends on host re-seed contractReplay-equivalent only with seed re-feed; not bit-for-bit across save/load
TickContext.TickNumberNO — host ownsintrun progression freezes if mis-set lower than LastUpdatedTickYes given correct value
InstanceStoreNO — restore path blockedEntityHandleCurrent TemplateActivator.Create / Activate fires activation and lifecycle behavior; a no-lifecycle rehydrate path is not designed yetNo until restore-only rehydrate lands

20.2 Future SecsSavePayload protocol candidate

Status: backlog design, not a committed runtime boundary.

Today there is no unified SecsSavePayload record and no single SECS-owned SaveSnapshot/RestoreSnapshot entry point. The following shape is a future candidate direction for closing the current partial-save gap while keeping the host in charge of byte serialization.

If this protocol is implemented, the SecsSavePayload record would carry:

record SecsSavePayload(
int TickNumber,
ulong NextRunIdSeed,
IReadOnlyList<ActivityRunState> ActivityRuns,
IReadOnlyList<SlotSnapshot> SlotSnapshots,
IReadOnlyList<ModifierBindingEntry> ModifierBindings,
IReadOnlyList<PendingChoiceSave> PendingChoices,
IReadOnlyList<PolicyChildEntry> PolicyChildren)

Proposed restore order:

  1. Call ActivityExecutor.SeedNextRunId(NextRunIdSeed).
  2. Restore each ActivityRunState via ActivityRunStore.Restore.
  3. Restore ScopeSlotStore on a fresh instance via Restore(SlotSnapshots).
  4. Restore ModifierBindingStore via Restore(ModifierBindings) — new surface.
  5. Restore EventDispatcher.PendingChoices from PendingChoiceSave list — new surface.
  6. Restore PolicyDispatcher.activeChildren from PolicyChildEntry list — new surface.
  7. Set TickContext.TickNumber.

If adopted, SECS would provide the record shape and the host would serialize it (JSON, binary, MemoryPack, etc. — host's choice). No wire format is committed here.

PrevTickSnapshotStore is intentionally not in the payload — it warms up after one post-restore tick and is not save-critical.

InstanceStore restore remains a blocker. The current reactivation route through TemplateActivator.Create / Activate fires activation and lifecycle behavior, so it is not a valid durable restore path for already-created entities. A future no-lifecycle rehydrate path must repopulate InstanceStore and any required template runtime state without normal activation side effects.

20.3 Future PendingChoice option-identity protocol

The current EventDispatcher.ResolveChoice(choiceIndex, optionIndex, tick) uses array indices as identity, which is unstable across content patches. A future protocol upgrade would:

  • Add string Key to EventOption. Keys are designer-authored, unique within a single event's option list, and content-stable across patches.
  • Add EventDispatcher.ResolveChoiceByKey(int choiceIndex, string optionKey, TickContext) companion to the existing index-based method.
  • PendingChoiceSave carries EventId, Target, and (for already-selected choices) SelectedOptionKey. On restore the host re-presents choices with null SelectedOptionKey; pre-selected choices auto-resolve via key match.

Migration semantics:

  • Adding a new option in a patch: existing saves restore with no impact (the new key was unknown at save time).
  • Removing an option in a patch: saved choices that selected the removed key auto-cancel on restore. Host responsibility to surface the cancellation to the player or rebuild a new choice.
  • Renaming an option's display label: no save impact (label is presentation-layer, key is identity-layer).

Hash-of-label and array-index were both rejected as identity — labels are localizable presentation strings, indices are content-order-fragile. Designer-authored stable keys are the only option-identity that survives patches.

20.4 Random / determinism contract

TickContext.Random is a standard System.Random initialized by the host. The engine has no seed-save or seed-restore API.

Determinism contract:

  • The host generates and saves a per-game seed at new-game creation.
  • At restore, the host constructs TickContext with Random = new Random(savedSeed).
  • Post-load RNG is not bit-for-bit equivalent to a "continued without saving" run — System.Random state cannot be portably serialized in netstandard2.1 without reflection hacks.
  • This is acceptable for grand-strategy save semantics: the seed re-feed gives reproducible RNG sequences from the load point forward; pre-load RNG calls cannot be replayed.

Strict replay determinism (same seed → same outputs from tick 1) is not currently a protocol guarantee. A host that needs strict replay must serialize and replay the full input log from game start, not the world snapshot.

20.5 Reentrancy boundary

FireOnAction is synchronous and runs all subscribers inline (see § 19.3). A subscriber may, in turn, call FireOnAction to trigger another on_action — this is reentrant by design and the engine supports it via the saved-scope frame stack (PushSavedScopeFrameFromCurrent / PopSavedScopeFrame).

ActivityExecutor.Start may be called from inside a running activity's lifecycle hook (parent → child run). The child run is tracked via ActivityRunState.ParentRunId / ChildRunId. PolicyDispatcher.activeChildren records the policy → child run linkage for cancel/replace semantics; persisting that linkage remains future protocol work.

ScopeSlotStore.Read<T> and Initialize<T> are not reentrant on the same (actor, slot) key within a single transaction — the standard pattern is "seed at activation, never re-init during the same tick." Reentry on different (actor, slot) keys is safe.

ChannelResolver.SyncDirty must not be called from inside a system body that is itself producing dirty channels — the pipeline calls SyncDirty once per tick at the post-system-step boundary. Re-calling it from a system body would double-flush bindings and is an architectural error caught at runtime by BindingUpdater's "already-syncing" guard.

20.6 Deferred source items

The future-protocol candidate above would require the following source-code additions:

  • src/SECS.Engine/Events/EventOption.cs — add string Key property.
  • src/SECS.Engine/SaveLoad/SecsSavePayload.cs — new file, the structured record.
  • src/SECS.Engine/SaveLoad/PendingChoiceSave.cs — new serializable pending-choice shape.
  • src/SECS.Engine/SaveLoad/PolicyChildEntry.cs — new (EntityHandle, PolicyId, ActivityRunId) triple.
  • src/SECS.Engine/SaveLoad/ModifierBindingEntry.cs — new serializable modifier-binding shape.
  • src/SECS.Engine/Events/EventDispatcher.cs — add ResolveChoiceByKey.
  • src/SECS.Engine/ModifierBindingStore.cs — add Snapshot() / Restore(...).
  • src/SECS.Engine/Policies/PolicyDispatcher.cs — expose ActiveChildren enumerable and RestoreChildren(...).

These additions remain future engine work. This section records a proposed direction for later implementation work; it is not describing shipped runtime surfaces.

20.7 Cross-references

  • § 11.1 A5 (option handler), F8 (player choice resolution), F3 / F4 (modifier attach / remove).
  • § 14.7 (on_action dispatch host API), § 14.8 (player choice resolution).
  • § 16.4 (ActivityRunState 11-field record).
  • § 19.5 (PendingChoice in-memory queue).
  • src/SECS.Engine/Activities/ActivityRunStore.cs — Restore + SECS0810/0811/0812.
  • src/SECS.Engine/Slots/ScopeSlotStore.cs — Snapshot/Restore + SECS0820/0821.
  • src/SECS.Engine/Resolution/PrevTickSnapshotStore.cs — re-derived after first tick.
  • src/SECS.Engine/ModifierBindingStore.cs — current API (no Snapshot/Restore yet).
  • src/SECS.Engine/Policies/PolicyDispatcher.csactiveChildren map.
  • src/SECS.Engine/Events/PendingChoice.cs, EventOption.cs.

21. Mod / source-set / diagnostic catalog

Consolidates the mod-runtime architecture, the host-capable vs data-only source-set boundary, and the diagnostic catalog cross-reference.

21.1 Compile-time merge plus startup-finalization runtime target

Mod composition has two layers:

  • Compile-time semantic merge — the future SECS compiler (Roslyn fork, currently deferred) performs source-set-aware merge in Phase 3, producing a single immutable Generated artifact. This is the long-term target.
  • Startup-finalization runtime targetSecsRegistry.RegisterActivityMod and RegisterPolicyMod accumulate mod patches at boot; FinalizeModRegistration() merges them once into a frozen view. Source: src/SECS.Engine/Modding/ModRegistry.cs:114. Documented as "the committed permanent architecture, not a transitional stand-in" in `docs/design/06-overrides-and-modding.md § 1.2.

After Finalize() returns, the merged registry is read-only for the rest of the process. Subsequent calls to RegisterActivityMod or RegisterPolicyMod throw InvalidOperationException ("ModRegistry is finalized"). The ticking game sees the frozen merged view; there is no runtime hot-patching.

21.2 Host-capable vs data-only source-set boundary

Two classes of source set with different rights:

CapabilityHost-capable (base game, official expansion)Data-only mod (third-party)
Add scopes / scope fieldsYesNo
Add walks_to edgesYesNo
Add scoped collectionsYesNo
Add scope methods (query or void)YesNo
Add templates, modifiers, formulas, triggersYesYes
Add systems, events, on_actions, activities, policiesYesYes
Add inject / replace modsYesYes

The boundary exists because adding host-backed surfaces requires host-side bridge code (see § 5, § 15). A data-only mod cannot ship that code, so SECS prohibits the declaration at compile time.

Enforcement today is compiler / doc level only. The runtime has no SourceSet enum or HostCapable / DataOnly flag (see § 6, § 15.7). Diagnostic codes:

  • SECS0604 — operation-kind / declaration-kind mismatch in mod registry. Implemented in ModDiagnosticCode.cs.
  • SECS0609 — third-party data-only mod declares host-backed surface. Compiler-phase-only; no runtime enum entry. The ModRegistry does not see scope / field / walk declarations (those are registered directly in SecsRegistry from generated C#), so a runtime check is architecturally not applicable. Reserved for the Phase 3 source-set-aware merger.

21.3 No runtime hot-patching

Once FinalizeModRegistration() returns, the merged view is immutable for the lifetime of the process:

  • Mod registration calls throw post-finalize.
  • Activity / policy declarations may not be added or removed.
  • The host may not "reload mods" without recreating SecsRegistry and re-running the entire boot sequence.

This is by design. The runtime is single-pass at startup, then frozen. Any future save/load protocol will use the frozen registry as its schema.

21.4 Phase / cadence — content-authored pass-through C#

PhaseDeclaration and TickRate instances are declared as plain C# static readonly fields inside .secs files (e.g. static readonly PhaseId DayStart = ...;). The compiler does not parse phase / cadence / tick_rate as SECS keywords — those keywords were explicitly rejected (06-overrides-and-modding.md § 19.2-19.3).

Ownership classification: a fourth category alongside host-owned (§ 3.1), SECS-owned (§ 3.2), and shared/bridged (§ 3.3) — content-authored pass-through C#. They are:

  • NOT host-owned (no host storage).
  • NOT SECS-owned (parser does not treat them as keywords).
  • NOT data-only-mod territory (they affect system scheduling, which requires host-capable rights).

Source-set classification: host-capable source sets only (base game, official expansion). Data-only mods may not add new phase or tick rate declarations (06 § 1805).

Compiler treatment: the Phase 4 compiler type-checks phase = X; and frequency = Y; slot expressions in system declarations as PhaseDeclaration and TickRate expressions respectively, then emits the resolved ids into SystemRegistration. The compiler consumes static helper class references (Phases.Growth, Cadence.Daily) as plain C# identifiers, not as SECS keywords.

21.5 Diagnostic band allocation in the 08xx range

The committed 08xx allocations are:

  • SECS0800 — runtime SlotConflict warning (ModDiagnosticCode.SlotConflict = 800).
  • SECS0801 — runtime InjectClosedSlot (ModDiagnosticCode.InjectClosedSlot = 801).
  • SECS0802 — compiler reservation for propagates_to where non-structural-predicate restriction.
  • SECS0803 — compiler/analyzer reservation for "command call in read-only body".
  • SECS0804 — compiler/analyzer reservation for "host command without command context".
  • SECS0810-SECS0812 — runtime activity-run restore validation (RegistryDiagnosticCode.ActivityRestore* and ActivityRunStore.Restore throws).
  • SECS0820-SECS0821 — runtime slot-store restore validation (RegistryDiagnosticCode.SlotRestore* and ScopeSlotStore.Restore throws).
  • SECS0822 — compiler binder reservation for ScopedList<T> indexed access.
  • SECS0830 — compiler/analyzer reservation for prev-tick reads without track_prev = true.
  • SECS0840 — runtime collection hook-name collision (RegistryDiagnosticCode.CollectionHookNameCollision and SecsRegistry.RegisterCollections throws).

Before adding a new 08xx diagnostic, audit both the runtime enums and SECS-Compiler-Plan.md so the band stays collision-free.

21.6 Diagnostic catalog cross-reference

The single source of truth for the SECS diagnostic catalog is SECS-Compiler-Plan.md § Diagnostic Code Catalog plus the runtime-shipped enums:

  • src/SECS.Engine/Modding/ModDiagnosticCode.cs — mod-registry diagnostics (06xx, 08xx mod codes).
  • src/SECS.Engine/Diagnostics/RegistryDiagnosticCode.cs — registry diagnostics (03xx, 04xx, 08xx, 09xx).

For the compact status split between runtime-shipped, compiler-planned, doc-only analyzer reservation, and current test coverage, see SECS-Compiler-Plan.md § Mod-operation / slot / source-boundary status crosswalk.

Band allocations:

  • 01xx — channel declaration validity.
  • 02xx — template / contract declaration validity.
  • 03xx — formula / dependency analysis.
  • 04xx — activity / policy declaration validity.
  • 05xx — name resolution / binder.
  • 06xx — mod merger and source-set rules.
  • 07xx — structured-type rules.
  • 08xx — modifier / propagation / runtime-restore rules.
  • 09xx — runtime registry validation.

Before adding a new diagnostic, audit existing band occupants in both runtime enums and the compiler plan to avoid collisions.

21.7 Cross-references

  • § 6 — host-capable vs data-only mod boundary (high-level summary).
  • § 15.7 — SECS0609 compiler-phase-only status.
  • docs/design/06-overrides-and-modding.md § 1.2 / § 19.2-19.3 — startup-finalization architecture, phase/cadence keyword rejection.
  • docs/design/01-world-shape.md:253-272 — host-capable vs data-only declaration rules.
  • SECS-Compiler-Plan.md § Diagnostic Code Catalog — single source of truth.
  • src/SECS.Engine/Modding/ModRegistry.cs:114Finalize() implementation.

22. Engine-owned public API and context placement

22.1 Runtime context placement

TickContext, EventContext, ActivityRunContext, and PolicyRunContext stay in SECS.Engine, not SECS.Abstractions. That placement is intentional: the contexts carry engine-internal types, some have internal constructors, and moving them into SECS.Abstractions would invert the current assembly boundary rather than clarify it.

BRIDGED in § 3.3 therefore means "types the host receives through generated callbacks", not "types that must physically live in SECS.Abstractions". Future hardening may add narrower read-only query interfaces, but it does not require moving the concrete runtime contexts.

22.2 SECS public engine API

SecsRegistry and the executor/dispatcher family are a fourth ownership bucket: SECS public engine API. These are concrete SECS.Engine types that the host constructs, calls, or inspects directly with no bridge interface in between.

Members of this bucket include SecsRegistry, ActivityExecutor, PolicyDispatcher, PolicyExecutor, EventDispatcher, TemplateActivator, ScopeSlotStore, and ActivityRunStore. They are not host-owned, and they are not bridge interfaces; they are engine API that host code consumes directly.

No ISecsRegistry abstraction is planned. The concrete registry exposes engine-internal declaration and finalize semantics that would become less usable, not more, behind an artificially thin interface.

22.3 Doc-hygiene backlog

Remaining Valenar-label normalization belongs in docs/design/FUTURE_WORK.md § 4a. It does not change the boundary contract above.