Skip to main content

SECS Roslyn Fork — Implementation Plan

Repository Setup

Fork Structure

secs-workspace/
├── src/ existing — engine runtime
│ ├── SECS.Abstractions/ shared types (netstandard2.1)
│ ├── SECS.Engine/ runtime engine (netstandard2.1)
│ └── SECS.Localization/ YAML localization (netstandard2.1)
├── secs-roslyn/ forked Roslyn submodule
│ └── (forked from dotnet/roslyn, latest)
├── examples/
│ ├── valenar/ reference colony-builder example
│ └── character-trainer/ smaller training-loop example
└── docs/

The VS Code extension and LSP server live inside the secs-roslyn fork itself — Roslyn already contains the Workspaces and LSP infrastructure, so the extension is built from the same codebase rather than being a separate repo.

Git Setup

Fork: Create a private fork of dotnet/roslyn on GitHub. Fork from latest. Create a secs branch — this is the working branch where all SECS modifications go.

Submodule: secs-roslyn/ is the Roslyn submodule in this workspace. Configure it to track the secs branch. Full clone — no shallow, we want the complete history for tracing how upstream implemented features like record.

Branch strategy:

  • secs — working branch with all SECS modifications
  • upstream/main — tracks dotnet/roslyn for periodic merges
  • Upstream merges happen deliberately (quarterly or when needed), not continuously

Target Framework

  • Roslyn fork toolchain: follows secs-roslyn/global.json (currently .NET 10.0.105)
  • secsc distribution target: unresolved packaging choice — self-contained binary vs dotnet tool
  • Engine libraries (SECS.Abstractions, SECS.Engine): netstandard2.1 — Unity compatible
  • Compiler output: configurable target — the game developer chooses their target framework. The compiler emits code referencing SECS.Abstractions.dll (netstandard2.1), so the output is compatible with any .NET runtime that supports netstandard2.1

What We're Adding to Roslyn

Current Lowering Baseline

The compiler target is the current design/runtime plus the hand-written stand-ins under examples/valenar/Generated/. Valenar is the reference example, not the generic language vocabulary. The generic compiler must bind whatever scopes, contracts, methods, events, activities, policies, and fields the game declares.

Post-Wave-7 baseline: the runtime now ships activities (Wave 1), EffectPlan previews (Wave 2), typed activity args (Wave 3), policies + utility AI scoring (Wave 4), runtime per-actor slots (Wave 5), the full mod-operation runtime for activities and policies (Wave 6), and warn-only registry static analysis (Wave 7). Every one of these surfaces is already implemented in src/SECS.Engine/ and exercised by hand-written stand-ins under examples/valenar/Generated/. The compiler's job is to emit the same shape the stand-ins emit today.

The most important correction to this plan is that template is not a standalone first feature. Even an empty template<C> T { } needs the structural world already bound: scope declarations, contract declarations, owner-and-signature method ids, FNV-1a-64 hashes, and module registration. Roslyn work must therefore start with a structural declaration prepass before or alongside template-shell lowering.

Second correction: the callable/type foundation is no longer SecsValueKind plus scalar-only template fields. The compiler target is C# source type binding plus generated SecsTypeRef metadata as described in docs/design/07-structured-template-data-and-callables.md: structs, records, enums, arrays, chosen C# collection-shaped values, structured template fields, typed query returns, typed template commands, and generated scope command/query facades. Runtime metadata and template query/command dispatch now use typed registrations; SecsValue remains only for command payload storage and explicitly erased dynamic/tooling adapters, not normal template or scope callable ABI.

"Generated C#" in this plan means the ordinary C# syntax tree that SECS lowers to, not a requirement that the compiler writes .cs files during a normal build. The primary pipeline is .secs syntax trees -> merged SECS syntax tree -> lowered C# SyntaxTrees -> Roslyn emit -> DLL. A deterministic --emit-generated-cs diagnostic mode should write those lowered trees under an obj/ artifact directory for review and comparison with today's examples/valenar/Generated/ stand-ins, but physical files are not the architectural boundary.

Source keyword whitelist and grammar status

The compiler must keep source syntax, diagnostics, generated stand-ins, and runtime ABI in lockstep. This table is the whitelist for committed source grammar categories; anything outside it is either ordinary C# collected for metadata, a member/body form valid only in a containing SECS declaration, or a runtime/generated concept that is not a source keyword.

CategoryCommitted source formsStatus
SECS declarationsscope, contract, template, channel, field, modifier, system, event, on_action, activity, policyTop-level SECS declaration forms. field is valid as top-level template-field metadata and as a template member assignment form.
Member forms inside declarationsroot_scope, activation, deactivation, context, query, method, phase =, frequency =, tags =, track_prev =, reads, propagates_to, provides, mode, need, selector, ruleValid only in the declaration/member contexts that own them; they are not free-standing top-level keywords.
Mod operationsinject, replace, try inject, try replace, inject_or_create, replace_or_createPrefix a normal SECS declaration and lower through the slot-merge pipeline. The old single override keyword is not committed source syntax.
Ordinary C# collected for metadatastruct, record, enum, static readonly TagId, PhaseDeclaration.Create(...), TickRate.*(...)Parsed as C#; the SECS binder harvests type/tag/phase/cadence metadata from these shapes. Top-level tag, phase, and tick_rate declarations are not SECS keywords.
Body expression/statement formsresolve, template_field, increment, add_modifier, fire on_action, save_scope_as, foreach, any, every, count, random, first, has_tag, has_template, has_contract, registry[...]SECS-aware forms inside method/formula bodies. Arbitrary direct event firing is not committed; only fire on_action ... is in the current source contract.
Runtime/generated concepts, not source keywordsformula delegates, triggers, CandidateBuilderId, SelectorSource.FromCollection, SecsTypeRef, ActivityRequestOrigin, ContractDeclaration.IsRegistryOnly, bridge prev-tick readsThese are generated/runtime ABI concepts. A formula top-level keyword is not committed; formulas are lifted from dynamic channel sources and dynamic modifier effects.

New Contextual Keywords and Source Forms

SurfaceRoleNotes
struct, record, enumDeclares C# value types that participate in SECS metadataOrdinary C# declarations collected by the SECS binder. Lowers to generated C# value types plus SecsTypeDeclaration[]; referenced by generated SecsTypeRef metadata.
scopeDeclares host-owned entity shapes, fields, collections, walks, and host-exposed methodsLowers to ScopeDeclaration[], ScopeFieldDeclaration[], and ScopeMethodDeclaration[].
contractDeclares a template API and root scopeLowers to ContractDeclaration[], ContractMethodDeclaration[], and optional ContextProfileDeclaration[].
templateDeclares a template bound to a contractLowers to a static class plus TemplateEntry.
fieldDeclares template-definition dataTop-level field metadata lowers to TemplateFieldDeclaration[] with SecsTypeRef; template assignments lower to typed scalar or structured accessors.
channelDeclares channel metadata or an own-root template intrinsic sourceTop-level channels lower to ChannelDeclaration[]; template-body channels lower to activation-time channel-source registration.
modifierDeclares reusable channel/template-field effectsLowers to ModifierDeclaration[] plus formula/trigger helpers when dynamic.
systemDeclares a scheduled tick procedureLowers to a class implementing ITickSystem and a SystemRegistration.
eventDeclares a SECS eventAt declaration level, lowers to a SecsEvent subclass plus EventEntry. C# event remains legal inside ordinary C# class bodies.
on_actionDeclares an extension pointMetadata-only in committed source syntax; effects live in subscriber events or the firing site.
activityDeclares a class-style actor-driven activity (Wave 1 rename of the prior action keyword)Lowers to a SecsActivity subclass with typed IActivityArgs<TSelf> args (Wave 3) and a declarative Preview returning EffectPlan (Wave 2). Old allow / effect block syntax is prototype-only and is not the compiler target.
policyDeclares a utility-AI policy with needs, selectors, and rules (Wave 4)Lowers to a SecsPolicy subclass plus per-rule body delegates and NeedTargetExpr discriminated-union targets (Wave 5).
injectPartial slot patch — replaces listed semantic slots, retains the restSECS mod operation; not C# member override.
replaceFull declaration replacement — discards the old declaration bodySECS mod operation.
try injectPatch if target exists; no-op if missingCompatibility patches across DLC/loadout combinations.
try replaceFull replacement if target exists; no-op if missingSame use-case as try inject but broader replacement.
inject_or_createInject if target exists; otherwise create as a new declarationUseful for mod-additive modifiers and templates.
replace_or_createReplace if target exists; otherwise createTotal-conversion and broad compatibility layers.
query / methodDeclares contract callable signaturesquery is read-only and may return any bound source type that lowers to SecsTypeRef; method void is command-producing and must return void.
@<declared-scope-name>, @root, @this, etc.Declared scope sigils plus built-in frame references@Location / @Settlement are Valenar examples; the compiler binds whatever scope names the game declares. These are expression forms, not a single @scope keyword.
resolve, template_field, increment, add_modifier, fire on_action, save_scope_as, foreachLowered expression forms inside bodiesParsed as SECS-aware expressions/statements over normal C#-style bodies. Arbitrary direct event firing remains future work.

New Syntax Nodes

Each SECS construct needs a Roslyn syntax representation that can survive merge, binding, diagnostics, and lowering:

  • ScopeDeclarationSyntax, with field, collection, walks_to, and scope-method members.
  • ContractDeclarationSyntax, with root_scope, lifecycle bindings, query / method signatures, and context profiles.
  • SecsTypeDeclarationSyntax for C# struct, record, and enum declarations, plus binding of C# array and collection type syntax to recursive SecsTypeRef metadata.
  • TemplateDeclarationSyntax, including template fields, own-root intrinsic channels, contract query/method bodies, modifiers, and transition edges.
  • TemplateFieldDeclarationSyntax / TemplateFieldAssignmentSyntax.
  • SecsChannelDeclarationSyntax for top-level channel metadata and template-body channel sources.
  • ModifierDeclarationSyntax and modifier effect syntax.
  • SystemDeclarationSyntax.
  • SecsEventDeclarationSyntax.
  • OnActionDeclarationSyntax.
  • SecsActivityDeclarationSyntax (Wave 1 — renamed from the prior SecsActionDeclarationSyntax).
  • SecsPolicyDeclarationSyntax (Wave 4 — needs/selectors/rules with rule-body delegates and NeedTargetExpr targets).
  • ScopeSigilExpressionSyntax, saved-scope expressions, contract-call expressions, and SECS-aware invocation/iteration forms.
  • ModOperationDeclarationSyntax — wraps any inject, replace, try inject, try replace, inject_or_create, or replace_or_create operation around a standard SECS declaration syntax node. Carries the ModOperationKind enum and the inner declaration's identity/body. Replaces the old OverrideDeclarationSyntax.

Lowering Targets (Matching Current Engine API)

Each SECS node lowers to ordinary C# syntax trees matching the current Generated/ contract. Rows that use Valenar names are examples of the generic lowering shape, not compiler-reserved vocabulary.

SECS ConstructLowers To (matching current engine)
struct / record / enum declarationsGenerated ordinary C# types plus SecsTypeDeclaration[]. Every reference to these source types in fields/callables lowers to SecsTypeRef metadata.
scope <declared-scope> { ... }ScopeDeclaration with ParentScopeIds, ScopeFieldDeclaration[], ScopeMethodDeclaration[], and H.* hashes.
int SomeQuery(ulong templateId); / LocationFacts Facts(); on a scopeScopeMethodDeclaration keyed by scope:<scope-name>.<Method>(<params>):<return>; read-only calls lower to generated typed facades such as LocationScopeQueries.BuildingCount(host, location, templateId) over ISecsHostReads.CallScopeQuery<TArgs,TResult>(...). Erased adapters are only dynamic/tooling boundaries.
void SomeCommand(<declared-scope> target, int amount); on a scopeScopeMethodDeclaration with ReturnType = SecsTypeRef.Void and ParameterTypes = [SecsTypeRef.EntityInScope(...), SecsTypeRef.Int]; call sites lower through generated typed command facades such as CharacterScopeCommands.AdvanceLead(tick, character, target, amount) over the active command context / ISecsHostCommands, then flush after the source statement. Scope-typed parameters use declared scope names.
contract C { root_scope S; query bool CanX(); method void OnY(); }ContractDeclaration with query/method id arrays and lifecycle bindings, plus one ContractMethodDeclaration row per callable signature.
context SomeContext { @SomeScope; @root; }ContextProfileDeclaration owned by the contract; expands to an ordered context chain for template_field(...). SomeScope stands for a source-declared scope name; it is not engine vocabulary.
template<C> T { ... }Static class T with TemplateEntry { TemplateId, Name, ContractId, RootScopeId, Activate, StructuredFields, Queries, Methods, GetField* }, registered from SecsModule.Initialize.
field int SomeField = 10 inside a templateTyped scalar GetFieldInt switch on the template entry, with top-level field metadata using SecsTypeRef.Int.
field FeaturePlacementProfile Placement = new(...) inside a templateStructured immutable template data accessor returning concrete FeaturePlacementProfile, plus top-level field metadata using SecsTypeRef.Record(H.FeaturePlacementProfile).
channel int Food = 5 inside a templatecmds.RegisterChannelSource(scope.Owner, scope.Root, H.Food_Channel, 5) inside Activate; legal only when the channel belongs to the contract root scope.
channel int Food { return ...; } inside a templatecmds.RegisterDynamicChannelSource(scope.Owner, scope.Root, H.Food_Channel, H.Formula_TFood) plus a formula delegate and registry.RegisterFormula(...); cross-scope effects use named modifiers instead.
bool SomeQuery() { ... } on a template whose contract declares itTyped contract query implementation returning concrete bool, registered by contract owner+signature id with metadata ReturnType = SecsTypeRef.Bool. CanBuild is only Valenar vocabulary; there is no special template activation/can-activate hook.
FeaturePlacementResult EvaluatePlacement(Location candidate, FeaturePlacementInput input) { ... }Typed read-only query returning a declared record. Used by feature generation as local scoring only; global budgets/spacing/conflicts are applied by systems after query results are collected.
void SomeMethod() { ... } on a template whose contract declares itGeneric contract method implementation with ITemplateCommandContext and typed argument structs/adapters, registered in TemplateEntry.Methods as TemplateCommand<TArgs> by contract owner+signature id. Generated bodies use context.Scope, context.Host, context.Commands, and context.FlushCommands() after command-producing source statements. Lifecycle bindings call this same generic method surface; there is no raw-entity world-parameter method ABI and no erased argument ABI for normal generated calls.
@SomeScope.SomeFieldDeclared scope walk from the current ScopeFrame, then typed host read such as host.ReadInt(...) selected by the declared scope-field type. Valenar example: @Location.Fertility.
@SomeScope.resolve(SomeChannel)Declared scope walk, then typed channel resolution (host.ResolveChannel* in formulas/triggers or ctx.ChannelResolver.Resolve*Fast in tick bodies). Valenar example: @Settlement.resolve(Morale).
template_field(SomeTemplate, SomeField, SomeContext)Base/effective template-field read with the contract-owned context profile expanded to an explicit ordered entity chain. Return type is the field's SecsTypeRef; additive/multiplicative effective reads are numeric scalar only, while structured fields are whole-value exact-type only.
modifier X { SomeChannel += 5; field SomeField *= 80%; }ModifierDeclaration with ModifierEffectTargetKind.Channel or TemplateField, typed effect values, stacking/reapply/decay/tag metadata.
target.add_modifier X while cond duration NCommandBuffer.AddModifier(owner, target, H.X, H.Trigger_Cond, N, captured0, captured1); stored triggers use TriggerDelegate(target, captured0, captured1, host).
system SomeSystem { ... }Class implementing ITickSystem with Frequency, Phase, Execute(TickContext), a static SystemRegistration, and statement-level ctx.FlushCommands() after command-producing source statements.
event SomeEvent { ... }Sealed class deriving from SecsEvent, with Condition(EventContext), Execute(EventContext), optional BuildOptions(EventOptions options, EventContext context), and static EventEntry Registration.
on_action SomeOnAction { ... }OnActionDeclaration metadata (OnActionId, ScopeId, ProvidedScopes, SelectionMode) only; no source-authored effect delegate.
activity SomeActivity { ... }Sealed class deriving from SecsActivity (Wave 1), using ActivityContext for read-only methods (CanStart, IsTargetValid, Preview) and ActivityRunContext for command-producing lifecycle methods (OnStart, OnUpdate, OnComplete, OnStop, OnCancel, OnFail). Typed args declared as a record struct implementing IActivityArgs<TSelf> (Wave 3) and serialized through ActivityArgsBlob. Preview returns EffectPlan with PredictedEffect rows (Wave 2).
policy SomePolicy { ... }Sealed class deriving from SecsPolicy (Wave 4), with Needs : IReadOnlyList<NeedDeclaration>, Selectors : IReadOnlyList<SelectorDeclaration>, Rules : IReadOnlyList<RuleDeclaration>, and EvaluateRule(RuleId, PolicyRunContext) -> RuleDecision dispatching by rule id. Need targets use `NeedTargetExpr.Literal
foreach entity in Contractctx.InstanceStore.AllByContract(H.ContractId) or AllByContractForTick(...) for distributed system frequencies.
fire on_action X target targetSaved-scope validation + ctx.Events.FireOnAction(H.X, target, ctx).
inject template Farm { field GoldCost = 8; }Slot-level partial patch: the template_field:GoldCost slot is replaced; all other slots are retained. Emitted C# reflects the merged winner only.
replace template Farm { ... }Full declaration replacement: the old body is discarded and the new body becomes the declaration.
try inject / try replaceSilent no-op when the target identity is absent; otherwise behaves as inject / replace.
inject_or_create / replace_or_createInject/replace if target exists; create new declaration if absent. Body must satisfy full declaration requirements on the create path.

Formula and trigger delegates remain lower-level callbacks with raw EntityHandle parameters because channel resolution and binding updates run outside a normal contract scope frame. Template activation and queries use ScopeFrame directly; template command methods use ITemplateCommandContext, which carries the same scope frame plus the command surface. No generated template or contract callable may use ad hoc world-parameter signatures.

Generated Command-Flush Contract

Generated template contract methods, system bodies, event bodies, and activity bodies flush after every command-producing source statement:

  • increment, add_modifier, and void scope-method calls lower to the command surface and then ctx.FlushCommands() / context.FlushCommands(). save_scope_as writes the saved-scope frame directly; fire on_action dispatches through EventDispatcher after the required scopes are present.
  • A later statement, later subscriber, later activity lifecycle callback, or later loop iteration sees writes from earlier command-producing statements in the same tick.
  • Runtime dispatchers still flush or discard pending commands at body boundaries as a safety net, but the compiler must not rely on end-of-body batching for ordinary behavior bodies.
  • Template contract method void bodies use ITemplateCommandContext, so they follow the same statement-level flush contract as systems, events, and activities (policy rule bodies are read-only — see docs/design/10-host-secs-execution-boundary.md § 13). Template Activate remains a generated intrinsic-channel-source registration method that receives CommandBuffer; the runtime validates activation channel-source targets and flushes activation at the boundary.

Current Engine Types the Compiler Targets

Engine TypeLocationUsed For
TemplateEntrysrc/SECS.Engine/Instances/TemplateEntry.csTemplate id/name, ContractId, RootScopeId, Activate, typed query registrations, methods, typed scalar accessors, and future structured field accessors.
TemplateIdsrc/SECS.Abstractions/Templates/TemplateId.cs (new)Strong-type wrapper for template-id ulong hashes; the canonical key type for keyed scoped collections. Zero-cost record struct.
ScopeFramesrc/SECS.Abstractions/Scopes/ScopeFrame.csRuntime frame for template/contract functions: Root, Owner / This, and Current.
TemplateActivationDelegatesrc/SECS.Abstractions/Delegates.csvoid(ScopeFrame, ISecsHostReads, CommandBuffer).
Typed template query registrationsrc/SECS.Abstractions/Delegates.cs, src/SECS.Engine/SecsRegistry.cs, src/SECS.Engine/TemplateActivator.csConcrete generated query methods return their declared type (bool, FeaturePlacementResult, etc.); erased dynamic adapters validate against SecsTypeRef.
TemplateCommandDelegate<TArgs> / TemplateCommand<TArgs>src/SECS.Abstractions/Delegates.csvoid(ITemplateCommandContext, in TArgs); command-producing methods remain void with typed argument structs/adapters and statement-level flushes.
ITemplateCommandContext / TemplateCommandContextsrc/SECS.Abstractions/Interfaces/ITemplateCommandContext.cs, src/SECS.Engine/TemplateCommandContext.csCommand-producing template method context: Scope, Host, Commands, FlushCommands(), and host void-method forwarding.
Formula*Delegatesrc/SECS.Abstractions/Delegates.csTyped formula callbacks: (EntityHandle target, EntityHandle captured0, EntityHandle captured1, ISecsHostReads host) -> T.
TriggerDelegatesrc/SECS.Abstractions/Delegates.csbool(EntityHandle target, EntityHandle captured0, EntityHandle captured1, ISecsHostReads host).
ScopeDeclaration / ScopeFieldDeclaration / ScopeMethodDeclarationsrc/SECS.Abstractions/Scopes/ScopeDeclaration.csScope graph, host fields, and host-exposed callable signatures. Callable signatures use SecsTypeRef; scoped entity parameters are represented directly as SecsTypeRef.EntityInScope(...).
SecsTypeRef / SecsTypeDeclarationsrc/SECS.Abstractions/SecsTypeMetadata.csGenerated type metadata for scalar values, scope/template refs, structs, records, enums, arrays, and C# collection-shaped values.
TemplateFieldDeclarationsrc/SECS.Abstractions/Templates/TemplateFieldDeclaration.csTop-level template-field metadata and SecsTypeRef/clamps. Scalar runtime accessors exist today; structured accessors are the remaining generated C# emission work.
ChannelDeclarationsrc/SECS.Abstractions/Channels/ChannelDeclaration.csChannel metadata (ChannelKind, scalar type, clamps, source binding).
ModifierDeclaration / ModifierEffectsrc/SECS.Abstractions/Modifiers/Modifier metadata and channel/template-field effects.
CollectionDeclarationsrc/SECS.Abstractions/Scopes/CollectionDeclaration.cs (new)Typed scope-bound collection metadata: ParentScopeId, FieldHash, Kind (List/Dictionary), KeyType, ElementScopeId. Registered via registry.RegisterCollections(Declarations.Collections).
ContractDeclarationsrc/SECS.Abstractions/Contracts/ContractDeclaration.csRuntime contract dispatch/lifecycle table.
ContractMethodDeclarationsrc/SECS.Abstractions/Contracts/ContractMethodDeclaration.csCompiler/tooling signature metadata for contract queries and methods using SecsTypeRef return/parameter types.
ContextProfileDeclarationsrc/SECS.Abstractions/Contexts/ContextProfileDeclaration.csNamed ordered context chains for effective template-field reads.
ITickSystem / SystemRegistrationsrc/SECS.Engine/Pipeline/Generated systems and host pipeline registration.
SecsEvent / EventContext / EventOptions / EventEntrysrc/SECS.Engine/Events/Generated event classes, per-fire context, player-choice options, event registration metadata.
OnActionDeclarationsrc/SECS.Engine/Events/OnActionDeclaration.csMetadata-only on-action declarations.
SecsActivity / ActivityContext / ActivityRun / ActivityRunContextsrc/SECS.Engine/Activities/Wave 1 — class-style activities and active execution state. Replaces the prior SecsAction / ActionContext / ActionRun / ActionRunContext types.
EffectPlan / PredictedEffect / PredictionOp / PredictionConfidence / EffectPlannersrc/SECS.Engine/Activities/Wave 2 — declarative preview pipeline. Preview methods return an EffectPlan of PredictedEffect rows with Add/Set/Multiply/Modify ops, optional ModifierId (Wave 7), Probability, and Confidence.
IActivityArgs<TSelf> / ActivityArgsBlobsrc/SECS.Engine/Activities/Wave 3 — typed activity args via IActivityArgs<TSelf> (instance interface — netstandard2.1 has no static abstract members). ActivityArgsBlob(SchemaId, Payload) is the schema-tagged byte payload; wire shape is (SchemaIdHex, PayloadBase64) to avoid 2^53 precision loss.
IMethodPreviewsrc/SECS.Engine/Activities/IMethodPreview.csWave 5 — declarative-preview hook for template-method bodies. TemplateEntry.MethodPreviews is the per-template dictionary; UseItemActivity.Preview consults it for items' OnUsed effects.
SecsPolicy / PolicyExecutor / RuleDecision / NeedDeclaration / SelectorDeclaration / RuleDeclaration / NeedTargetExpr / NeedCurves / CandidateBuilder / CandidateBuilderIdsrc/SECS.Engine/Policies/Wave 4 — utility-AI policy runtime. RuleDecision is a discriminated union (Continue / Complete / Fail / Wait / Call(activityId, target, args) / CallBest(selectorId) / CancelChild). NeedTargetExpr is a Wave 5 discriminated union (`Literal
ScopeSlotStore / SlotKindId / PolicySlotKinds / SlotSnapshotsrc/SECS.Engine/Slots/Wave 5 — per-actor mutable scope-keyed list storage. Throws on uninitialized read, double initialize, type mismatch. Keyed by (EntityHandle scope, SlotKindId kind). Surfaces Read<T> / Initialize<T> / Append / Insert / Replace / RemoveAt<T> / Contains / RemoveAllForScope. Wave 8 adds Snapshot() / Restore(snapshots) / EnumerateSlotKeys() for save/load with hard validation (SECS0820 unknown kind / non-empty store, SECS0821 element type mismatch).
ModRegistry / ModSlotKey / ActivityMod / PolicyMod / ActivityModPatch / PolicyModPatch / MergedActivity / MergedPolicy / ModOperationKind / DeclarationKind / ActivitySlotKind / PolicySlotKind / ActivityBodyDelegates / ModDiagnostic / ModDiagnosticCodesrc/SECS.Engine/Modding/Wave 6 — full mod-operation runtime. Implements all 7 ops (Create / Inject / Replace / TryInject / TryReplace / InjectOrCreate / ReplaceOrCreate) for activities and policies. ModSlotKey(DeclarationKind, DeclarationId, SlotKind, ChildId) is the structured slot key. SecsRegistry.FinalizeModRegistration() accumulates mods, finalizes once, replaces declaration maps with merged views. Runtime-shipped mod diagnostics are SECS0602, SECS0603, SECS0604, SECS0611, SECS0800, and SECS0801; the other 06xx source-set / slot-schema diagnostics are compiler-owned Phase 3 reservations. This is the lowering target Phase 3 emits. Slot schema enumerated in 06 § 8.10 (25 activity slots — Wave 6 added activity_args_schema) and 06 § 8.11 (6 policy slots).
RegistryDiagnostic / RegistryDiagnosticCode / RegistryDiagnostics / Severitysrc/SECS.Engine/Diagnostics/Wave 7 — warn-only registry static-analysis surface. Severity enum (Error / Warning / Info / Hint) used by both ModDiagnostic and RegistryDiagnostic. SecsRegistry.Validate() returns RegistryDiagnostics with codes SECS0303 (formula readsChannels over-approximation), SECS0410-0414 (activity static analysis), SECS0415 (policy-visible activity inherits the default EffectPlan.Empty Preview; emitted for selector sources AllRegistered and FromTagsFromCollection is not statically checked because the candidate builder yields activities at runtime), SECS0910-0914 (policy static analysis). Wave 8 reserves codes SECS0810-0812 (activity restore validation) and SECS0820-0821 (slot restore validation) on the same enum, but those failures are unrecoverable for the affected run and are surfaced as InvalidOperationException from ActivityRunStore.Restore and ScopeSlotStore.Restore — they are not collected by Validate(). When the compiler matures, the static-analysis checks can move to parse-time analyzers without changing call sites.
SecsRegistrysrc/SECS.Engine/SecsRegistry.csCentral registry for structural declarations, formulas, triggers, modifiers, contracts, templates, transitions, on-actions, activities (Wave 1), policies (Wave 4), mods (Wave 6), and validation. New entry points: RegisterActivityMod / RegisterPolicyMod (Wave 6), FinalizeModRegistration() (Wave 6), Validate() (Wave 7).
CommandBuffer / TickContextsrc/SECS.Abstractions/Commands/, src/SECS.Engine/Pipeline/TickContext.csBuffered command writes, command flushing, channel resolver access, events, saved scopes, and activity/policy/system/event execution context. TickContext.SlotStore (Wave 5) and TickContext.Registry (Wave 6) are required init members.
EntityHandle / erased value carriersrc/SECS.Abstractions/Entity identity plus command payload storage and explicitly erased dynamic/tooling adapters. Known generated template/scope call sites use concrete C# types and typed args structs validated by SecsTypeRef; they do not use an erased SecsValue callable ABI.
ActivityRequestsrc/SECS.Engine/Activities/ActivityRequest.csCarries (ActivityId, Actor, Target, Args, Origin) from caller to executor (Wave 2).
ActivityCandidatesrc/SECS.Engine/Activities/ActivityCandidate.cs(Request, Preview, PolicyScore, ScoreConfidence, Status) — the unit policy scoring sees (Wave 2).
ActivityRequestOriginsrc/SECS.Engine/Activities/ActivityRequestOrigin.csEnum {Unknown, Player, Policy, System, Event, Mod} — provenance of the request (Wave 2).
CandidateStatussrc/SECS.Engine/Activities/CandidateStatus.csEnum {Available, Filtered, Selected, Started, Rejected} — candidate lifecycle (Wave 2).
ActivityLanePolicysrc/SECS.Engine/Activities/ActivityLanePolicy.csEnum {Reject, Queue, Preempt} — only Reject is implemented; Queue / Preempt throw NotImplementedException at start time (Wave 2).
CandidateBuilderId / CandidateBuilderDelegatesrc/SECS.Engine/Policies/CandidateBuilder.csRuntime bridge for collection-backed selectors: (actor, slot, tick) -> IReadOnlyList<ActivityRequest>. Turns stored definition references into typed-args requests; not a standalone .secs declaration.
PolicyDispatcher / PolicyDispatchResultsrc/SECS.Engine/Policies/PolicyDispatcher.csReference dispatch loop processing RuleDecision cases. Result enum {Continue, Complete, Fail} (Wave 3).
ScoreBreakdown / ScoreContributionsrc/SECS.Engine/Policies/ScoreBreakdown.csPer-need explainability: (NeedId, NeedName, Urgency, Relevance, Matched, Weight, WeightedContribution) (Wave 5).
PreviewDriftRecorder / PreviewDriftRecord / PreviewDriftEntrysrc/SECS.Engine/Activities/PreviewDriftRecorder.csOpt-in telemetry: snapshots channel values at run start, compares to actuals at completion (Wave 5).

Diagnostic Code Catalog

A consolidated table of every SECS diagnostic code reserved across phases. Codes that appear in RegistryDiagnosticCode or ModDiagnosticCode enums are runtime-shipped; the others are future-compiler reservations.

CodePhaseSeverityDiagnosesStatus
SECS0030Phase 4 (parser)ErrorRemoved Wave-1 keyword (legacy activity declaration); use activity (see Phase 4 Reserved diagnostic codes below)Reserved (Wave 1)
SECS0031Phase 4 (parser)ErrorRemoved Wave-1 keyword (legacy policy declaration); use policy (see Phase 4 Reserved diagnostic codes below)Reserved (Wave 1)
SECS0032Parser / source keyword whitelistErrorUncommitted SECS-looking source form or keyword (formula top-level declaration, old override, top-level tag, top-level phase / tick_rate, arbitrary direct fire EventName)Compiler-planned source-boundary guard; ordinary C# metadata collection remains allowed
SECS0212Phase 2 (binder)ErrorUndeclared tag in tags = ... or structural predicateReserved
SECS0303Wave 7 (registry static analysis)WarningFormula readsChannels over-approximationShipped (FormulaReadSetOverApproximation)
SECS0410Wave 7 (registry static analysis)WarningActivity references unknown tag idShipped (ActivityUnknownTag)
SECS0411Wave 7 (registry static analysis)WarningActivity declares non-positive durationShipped (ActivityNonPositiveDuration)
SECS0412Wave 7 (registry static analysis)WarningActivity references unknown actor scopeShipped (ActivityUnknownActorScope)
SECS0413Wave 7 (registry static analysis)WarningActivity references unknown target scopeShipped (ActivityUnknownTargetScope)
SECS0414Wave 7 (registry static analysis)WarningActivity cost amount non-positive after mergeShipped (ActivityNonPositiveCost)
SECS0415Wave 5 (registry static analysis)WarningPolicy-visible activity missing Preview() overrideShipped (PolicyVisibleActivityMissingPreview)
SECS0420Phase 3 (mod parser)Errorinject activity X sets replace-only architectural slotReserved (Wave 6)
SECS0601Phase 1 (structural prepass)ErrorFNV-1a-64 collision between distinct canonical identitiesCompiler-planned
SECS0602Phase 3 (merger)ErrorDuplicate declaration without inject/replaceShipped (DuplicateDeclaration)
SECS0603Phase 3 (merger)Errorinject/replace target missingShipped (TargetMissing)
SECS0604Phase 3 (merger)ErrorOperation kind invalid for declaration kindShipped (OperationNotValid)
SECS0605Phase 3 (merger)ErrorUnknown slot in inject bodyCompiler-planned
SECS0606Phase 3 (merger)ErrorAttempt to override / replace additive-closed scope edge setCompiler-planned
SECS0607Phase 3 (merger)Errorwalks_to references undeclared scope after mergeCompiler-planned
SECS0608Phase 3 (merger)ErrorHost-capable expansion references a source set outside load-order ancestryCompiler-planned
SECS0609Phase 3 (merger)ErrorThird-party data-only mod adds host-backed structural surfaceCompiler-planned; no runtime enum by design
SECS0610Phase 3 namespace rulesErrorAmbiguous short identity reference in mod operationCompiler-planned
SECS0611Phase 3 (merger)Errorreplace declaration missing required slotShipped (ReplaceMissingRequiredSlot)
SECS0612Phase 3 (merger)ErrorDuplicate slot in same declaration / inject bodyCompiler-planned
SECS0613Phase 3 (merger)Errorphase / frequency refers to value not registered in scheduler / catalogCompiler-planned
SECS0614Phase 3 (merger)ErrorTargeted child-row operation matched zero targetable rowsCompiler-planned
SECS0615Phase 3 (merger)ErrorTargeted child-row operation matched multiple targetable rowsCompiler-planned
SECS0616Phase 3 (merger)ErrorChild-row selector is not activation-constant or not bindableCompiler-planned
SECS0800Phase 3 (mod runtime)WarningTwo or more mod writes to same slotShipped (SlotConflict)
SECS0801Phase 3 (mod runtime)Error (Wave 6)Inject targets a replace-only architectural slotShipped (InjectClosedSlot) — promoted from Warning to Error in Wave 6. Also rejected at parse time by SECS0420.
SECS0802Phase 4 (modifier parser)ErrorLambda or channel-based predicate in propagates_to where clause (must be structural: has_tag, has_template, has_contract)Reserved (renumbered from SECS0801 in Wave B11 to free SECS0801 for the runtime InjectClosedSlot shipped enum).
SECS0803Compiler/analyzer handoffErrorCommand-producing call appears in a read-only body (event Condition, activity IsVisible, policy rule Decision, etc.)Doc-only analyzer reservation; no runtime enum
SECS0804Compiler/analyzer handoffErrorGenerated code reaches ISecsHostCommands without a command-producing contextDoc-only analyzer reservation; no runtime enum
SECS0810Wave 8 (save/load runtime)ErrorSaved ActivityRunState.ActivityId not present in current registryShipped (ActivityRestoreUnknownActivity) — surfaced as InvalidOperationException from ActivityRunStore.Restore.
SECS0811Wave 8 (save/load runtime)ErrorSaved ActivityArgsBlob.SchemaId does not match the activity's ExpectedArgsSchemaIdShipped (ActivityRestoreSchemaMismatch) — surfaced as InvalidOperationException from ActivityRunStore.Restore.
SECS0812Wave 8 (save/load runtime)ErrorSaved ActivityRunState.Lane is the zero / None lane idShipped (ActivityRestoreUnknownLane) — surfaced as InvalidOperationException from ActivityRunStore.Restore.
SECS0820Wave 8 (save/load runtime)ErrorSaved SlotKindId is the zero kind id, or ScopeSlotStore.Restore was called on a non-empty storeShipped (SlotRestoreUnknownKind) — surfaced as InvalidOperationException from ScopeSlotStore.Restore.
SECS0821Wave 8 (save/load runtime)ErrorSaved slot element does not match the snapshot's declared ElementTypeShipped (SlotRestoreTypeMismatch) — surfaced as InvalidOperationException from ScopeSlotStore.Restore.
SECS0822Phase 2 (binder)ErrorScopedList<T> indexed access attemptCompiler-planned (renumbered from SECS0820 in Wave 8 to free 0820 for the slot save/load runtime band)
SECS0830Phase 4 (binder/analyzer handoff)ErrorPrevious-tick bridge/API read on channel without track_prev = trueDoc-only analyzer reservation
SECS0840Phase 2 (binder)ErrorTwo collection fields on same scope produce colliding hook namesShipped (CollectionHookNameCollision)
SECS0910Wave 7 (registry static analysis)WarningPolicy need references unknown channel idShipped (PolicyNeedUnknownChannel)
SECS0911Wave 7 (registry static analysis)WarningPolicy selector references unknown activity idShipped (PolicySelectorUnknownActivity)
SECS0912Wave 7 (registry static analysis)WarningTwo policy needs with conflicting weight signs on same channelShipped (PolicyNeedWeightConflict)
SECS0913Wave 7 (registry static analysis)WarningPolicy selector FromTags references unknown tag idShipped (PolicySelectorUnknownTag)
SECS0914Wave 7 (registry static analysis)WarningPolicy actor scope id has no registered scopeShipped (PolicyUnknownActorScope)

Diagnostic-code numbering ranges:

  • SECS00xx — parser-level errors (Phase 4 lex/parse, Wave 1 reservations, source keyword whitelist guard).
  • SECS02xx — Phase 2 binder errors (template members, scope frames, expressions).
  • SECS03xx — formula-level warnings (registry static analysis).
  • SECS04xx — activity-level errors and warnings (parser + registry).
  • SECS06xx — Phase 3 merger errors (mod operation parse/merge).
  • SECS08xx — Phase 3 mod runtime warnings + (post-Wave-6) closed-slot errors + compiler/analyzer boundary reservations + (Wave 8) save/load runtime errors. Sub-bands: 0800 mod runtime warnings, 0801 closed-slot runtime error, 0802 propagation parser reservation, 0803-0804 host-boundary analyzer reservations, 0810-0812 activity restore validation, 0820-0821 slot restore validation, 0822-0840 Phase 2 / Phase 4 binder and analyzer reservations plus shipped collection-hook validation.
  • SECS09xx — policy-level warnings (registry static analysis).

Mod-operation / slot / source-boundary status crosswalk

This crosswalk is limited to diagnostics that sit on the source/compiler/runtime boundary. "Runtime-shipped" means an existing runtime enum value or thrown diagnostic string owns the code today. "Compiler-planned" means Phase 1-4 compiler work must emit the code when the parser/merger/binder exists. "Doc-only reservation" means the number is allocated in the design docs so analyzer work cannot collide with runtime codes, but no runtime enum should be added.

Code(s)Current allocation statusRuntime surface todayTest coverage todayBoundary note
SECS0032Compiler-plannedNoneNo compiler tests yetSource keyword whitelist guard; rejects uncommitted SECS source forms while allowing ordinary C# metadata collection.
SECS0601Compiler-plannedNoneNo compiler tests yetDeterministic canonical-id collision check before binding / lowering.
SECS0602Runtime-shipped + compiler-plannedModDiagnosticCode.DuplicateDeclarationtests/SECS.Engine.Tests/ModRegistryTests.cs (CreateActivityDuplicateTargetRaisesDuplicateDeclarationDiagnostic)Phase 3 compiler emits the same code for duplicate declarations before lowering.
SECS0603Runtime-shipped + compiler-plannedModDiagnosticCode.TargetMissingModRegistryTests.InjectActivityMissingTargetRaisesDiagnosticIncluded here as the companion missing-target operation code.
SECS0604Runtime-shipped + compiler-plannedModDiagnosticCode.OperationNotValidtests/SECS.Engine.Tests/ModRegistryTests.cs (InvalidActivityOperationRaisesOperationNotValidDiagnostic)Runtime covers activity/policy operation-kind mismatches; source-set checks remain compiler-owned.
SECS0605-SECS0608Compiler-plannedNoneNo compiler tests yetSlot-schema and host-capable expansion checks from 06 § 12.
SECS0609Compiler-planned; no runtime enum by designNoneNo compiler tests yetData-only mods cannot add scopes, scope fields, scoped collections, scope methods, walks_to, existing contract method lists, or on-action provides entries. ModRegistry never sees those source declarations.
SECS0610Compiler-plannedNoneNo compiler tests yetAmbiguous short-name binding against exported source-set / SDK identity tables.
SECS0611Runtime-shipped + compiler-plannedModDiagnosticCode.ReplaceMissingRequiredSlottests/SECS.Engine.Tests/ModRegistryTests.cs (ReplaceActivityWithoutCreateBaseRaisesMissingRequiredSlotDiagnostic)Runtime validates activity/policy replace bases; compiler must validate every declaration schema.
SECS0612-SECS0616Compiler-plannedNoneNo compiler tests yetDuplicate-slot, scheduler/catalog, and targeted child-row diagnostics from 06 § 12.
SECS0800Runtime-shippedModDiagnosticCode.SlotConflictModRegistryTests.TwoModsWriteSameSlotEmitConflictDiagnostic, DemoModIntegrationTestsWarning conflict report; later load-order write wins.
SECS0801Runtime-shippedModDiagnosticCode.InjectClosedSlotMultiple ModRegistryTests closed-slot assertionsParser-side precheck uses SECS0420; SECS0801 remains the runtime finalization error.
SECS0802Compiler-planned / doc-only reservationNoneNo compiler tests yetpropagates_to where must use structural predicates only.
SECS0803-SECS0804Doc-only analyzer reservationNoneNo analyzer tests yetHost execution-boundary analyzer checks; no runtime enum should be added.
SECS0810-SECS0812Runtime-shippedRegistryDiagnosticCode.ActivityRestore* plus ActivityRunStore.Restore exceptionstests/SECS.Engine.Tests/ActivityRunStoreRestoreValidationTests.cs asserts SECS0810, SECS0811, and SECS0812 restore failuresSave/load restore validation, not compiler source diagnostics.
SECS0820-SECS0821Runtime-shippedRegistryDiagnosticCode.SlotRestore* plus ScopeSlotStore.Restore exceptionsScopeSlotStoreSnapshotTestsSave/load restore validation, not compiler source diagnostics.
SECS0822Compiler-plannedNoneNo compiler tests yetScopedList<T> indexed access binder error; moved to avoid colliding with runtime SECS0820.
SECS0830Doc-only analyzer reservationNoneNo analyzer tests yetPrev-tick bridge/API read requires track_prev = true; helper shape remains deliberately narrow.
SECS0840Runtime-shipped + compiler-plannedRegistryDiagnosticCode.CollectionHookNameCollision plus SecsRegistry.RegisterCollections exceptionLifecycleHookCollisionTestsRuntime catches generated collision; compiler should preempt when it owns hook generation.

No numeric collision exists across the runtime enums and compiler/doc reservations above: runtime mod codes occupy 0602, 0603, 0604, 0611, 0800, and 0801; registry/runtime restore and hook codes occupy 0810-0812, 0820-0821, and 0840; all remaining rows in this crosswalk are compiler/analyzer reservations.

This catalog is the single source of truth for diagnostic-code allocation. The phase-specific Diagnostics subsections in docs/design/00-overview.md § Diagnostic code convention and the per-doc Diagnostics tables (01–09) describe each code's full semantics; this table catalogs which codes exist, where they live in the runtime today, and which phase reserves the unshipped ones.

Signature Id Rules

Every callable user/game method uses ordinary C#-style source syntax, but generated ids are owner-and-signature hashes:

  • Scope method canonical text: scope:<scope-name>.<Method>(<param-type-refs>):<return-type-ref>.
  • Contract query/method canonical text: contract:<contract-name>.<Method>(<param-type-refs>):<return-type-ref>.
  • Examples from Valenar: scope:Location.BuildingCount(ulong):int, scope:Character.AdvanceLead(Location,int):void, contract:Building.CanBuild():bool, contract:Building.OnBuilt():void.
  • The canonical text is built from the declared owner, source method name, source parameter SecsTypeRefs, and source return SecsTypeRef. Zero-argument signatures use empty parentheses in the hash input; NoArgs appears only in generated constant names.
  • Scope-typed entity parameters use the source scope name in the canonical text and SecsTypeRef.EntityInScope(H.ScopeName) in declaration metadata.
  • Structured parameters and returns use canonical type names from the SecsTypeRef, e.g. FeaturePlacementInput and FeaturePlacementResult in contract:Feature.EvaluatePlacement(Location,FeaturePlacementInput):FeaturePlacementResult.
  • Hash constants use the generated naming convention shown in Generated/Hashes.cs; Valenar examples include H.Scope_Location_BuildingCount_TemplateId_Int and H.Contract_Building_OnBuilt_NoArgs_Void.
  • Never hash or emit bare callable names. Every scope method, contract query, and contract method belongs to its scope or contract owner.
  • Source calls still bind by normal name/signature resolution. The hash id is the emitted runtime key.

Hand-stand-in canonical-name drift. The current examples/valenar/Generated/Hashes.cs was authored across multiple waves and uses three different canonical-name schemes:

  • Bare lowercase ("settlement", "food") — early scope/channel hashes.
  • Prefixed lowercase ("activity_scoutnearbylead", "policy_charactersurvival") — Wave 1+ behavior hashes.
  • Namespace-qualified ("valenar:tag/disease", "valenar:phase/phasegrowth") — Wave 30+ tag/phase hashes.

The Phase 1 compiler MUST normalize to the namespace-qualified form <namespace>:<kind>/<name> for all hash inputs. The hand-stand-in's mixed scheme is grandfathered: changing it now would invalidate every saved game, test snapshot, and Generated cross-reference. The first compiler-emitted re-baseline of Hashes.cs will be the authoritative form; until then, the canonical-name field on each declaration is the source of truth, and the hash is a derived value.

What the Compiler Generates (Module Initialization)

The compiler produces a SecsModule class as part of the lowered C# syntax tree, matching examples/valenar/Generated/SecsModule.cs. The minimum current shape is broader than the old channels/modifiers/templates list:

public static class SecsModule
{
public static void Initialize(SecsRegistry registry)
{
registry.RegisterTypes(Declarations.Types);
registry.RegisterPhases(Declarations.Phases); // Wave 30+: phase declarations
registry.RegisterTickRates(Declarations.TickRates); // Wave 30+: cadence declarations
registry.RegisterScopes(Declarations.Scopes);
registry.RegisterScopeFields(Declarations.ScopeFields);
registry.RegisterScopeMethods(Declarations.ScopeMethods);
registry.RegisterCollections(Declarations.Collections); // Phase 2+: typed ScopedList/ScopedDictionary fields
registry.RegisterTemplateFields(Declarations.TemplateFields);
registry.RegisterChannels(Declarations.Channels);

registry.RegisterFormula(H.Formula_TSomeChannel, Formula_TSomeChannel.Evaluate, readsChannels: []);
registry.RegisterFormulaContribution(H.Formula_TSomeChannel, H.SomeChannel);

registry.RegisterModifiers(Declarations.Modifiers);
registry.RegisterContracts(Declarations.Contracts);
registry.RegisterContractMethods(Declarations.ContractMethods);
registry.RegisterContextProfiles(Declarations.ContextProfiles);
registry.RegisterOnActions(OnActionDeclarations.All);

// Tags — registered named so structural predicates and tooltips can address them.
registry.RegisterTag(H.Tag_SomeTag, "SomeTag");

// Validate dependency graph — catches formula/channel/scope cycles at registration time.
var errors = registry.ValidateDependencies();
if (errors.Count > 0)
throw new InvalidOperationException(
$"SECS dependency validation failed:\n{string.Join("\n", errors)}");

registry.RegisterTemplate(new TemplateId(T.Id), T.Entry);
// Valenar example; default-template generation is owned by a later phase.
registry.RegisterDefaultTemplate(H.Location, new TemplateId(BareLocation.Id));
registry.RegisterTemplateTransitions(Declarations.TemplateTransitions);

// Runtime collection bridge for selectors backed by definition-reference slots.
registry.RegisterCandidateBuilder(SomeBuilder.Id, SomeBuilder.Build);

registry.RegisterTrigger(H.Trigger_SomeCondition, Trigger_SomeCondition.Evaluate);

registry.RegisterActivities(Declarations.Activities); // Wave 1
registry.RegisterPolicies(Declarations.Policies); // Wave 4

// Mod registration is opt-in. Test harnesses install demo mods explicitly;
// the default game module emits empty Activity/PolicyMod arrays and may
// skip Finalize when there are no mods to merge.
if (host.UseMods)
{
foreach (var mod in Declarations.ActivityMods) registry.RegisterActivityMod(mod); // Wave 6
foreach (var mod in Declarations.PolicyMods) registry.RegisterPolicyMod(mod); // Wave 6
registry.FinalizeModRegistration(); // Wave 6
}
}

public static readonly SystemRegistration[] Systems = [ /* ... */ ];
public static readonly EventEntry[] Events = [ /* ... */ ];
public static readonly SecsActivity[] Activities = [ /* ... */ ]; // Wave 1 — replaces the prior `SecsAction[] Actions` array
public static readonly SecsPolicy[] Policies = [ /* ... */ ]; // Wave 4
public static readonly ActivityMod[] DemoActivityMods = [ /* ... */ ]; // Wave 6 — opt-in test/demo only
public static readonly PolicyMod[] DemoPolicyMods = [ /* ... */ ]; // Wave 6 — opt-in test/demo only
}

Phase 0 baseline: the actual hand-written examples/valenar/Generated/SecsModule.cs is currently the canonical shape the compiler must emit. Phase 1+ implementation should match this shape file-for-file. The example above is the abridged structural skeleton; the live file shows the full ordering (phases/tickrates registered before scopes; named tag registrations before ValidateDependencies; per-template-id new TemplateId(X.Id) wrapping; per-template RegisterDefaultTemplate calls keyed by scope hash) and is the binding contract.

For Phase 1, the required checkpoint is the structural subset of that shape, not the full Valenar module. A minimal empty-template source set must still lower to:

registry.RegisterTypes(Declarations.Types);
registry.RegisterScopes(Declarations.Scopes);
registry.RegisterScopeFields(Declarations.ScopeFields);
registry.RegisterScopeMethods(Declarations.ScopeMethods);
registry.RegisterTemplateFields(Declarations.TemplateFields);
registry.RegisterChannels(Declarations.Channels);
registry.RegisterContracts(Declarations.Contracts);
registry.RegisterContractMethods(Declarations.ContractMethods);
registry.RegisterContextProfiles(Declarations.ContextProfiles);
registry.RegisterTemplate(T.Id, T.Entry);

Everything else in the full module shape — formulas, formula contributions, modifiers, on-actions, default templates, transitions, triggers, systems, events, activities, policies, and mods — can be empty or deferred until the phase that owns that construct. The important point is that template shell lowering is not valid unless the registry has already seen the type catalog, scope graph, callable signatures, hashes, and contract metadata the shell references.

ContractDeclaration[] alone is never a correct contract lowering target. Each contract query/method id listed in QueryMethodIds or MethodIds must have a matching ContractMethodDeclaration row using SecsTypeRef, and each lifecycle binding must target a declared parameterless void method. Likewise, scope methods are structural declarations even when their implementation lives in the host: ScopeMethodDeclaration[] is what lets the binder type-check calls and what lets generated call sites pick typed read-only host calls vs command-producing method forwarding.


Implementation Phases

Phase 0: Orientation & Build

Goal: Fork builds successfully, can compile a normal .cs file with the forked Roslyn.

Status: Complete for the current workspace baseline: secs-roslyn/ is present as the Roslyn submodule and builds against unmodified Roslyn main. Phase 1+ work is currently deferred. The runtime + Generated/ stand-in pattern carries the workspace today; the 7-wave behavior-layer refactor (Activities, EffectPlan preview, typed activity args, Policies, Runtime slots, Mod runtime, warn-only static analysis) was completed without any compiler change. When compiler work resumes, Phase 1 starts the first SECS syntax change.

  • Fork dotnet/roslyn, create secs branch
  • Build the compiler and verify it works on a trivial C# file
  • Add as submodule in secs-workspace
  • Document build instructions in secs-roslyn/BUILDING-SECS.md

Key areas of the Roslyn codebase:

AreaWhat it doesWhy we touch it
src/Compilers/CSharp/Portable/Syntax/Syntax node definitionsAdding SECS node types
src/Compilers/CSharp/Portable/Parser/Parser (LanguageParser.cs)Adding SECS keyword parsing
src/Compilers/CSharp/Portable/Binder/Semantic analysisTeaching the binder about SECS types
src/Compilers/CSharp/Portable/Lowering/Desugaring transformsLowering SECS nodes to standard C#
src/Compilers/CSharp/Portable/Symbols/Symbol tableSECS identities in the symbol table
src/Workspaces/IDE/LSP supportMaking LSP aware of .secs files

Phase Scope Rule

Each phase below implements only committed current syntax and runtime shapes. Open questions are not parser, binder, lowering, CLI, LSP, or test commitments until the design docs settle them. If an open question is mentioned inside a phase, the phase must either use today's committed spelling/shape or explicitly reject/defer the alternative.

Phase 1: Structural Prepass + Template Shell

Goal: Parse enough SECS structure to lower an empty template for any declared contract, emit hashes/declaration tables/module registration as C# syntax trees, and compile to DLL.

Phase 1 is intentionally not "template only". A template shell cannot be correct until the compiler has already bound the contract's root_scope, generated owner-and-signature ids for callable methods, and emitted the structural arrays the registry validates at startup.

  • Add typed ID strong types to SECS.Abstractions. The full set required by the modding/merge architecture (per docs/design/06-overrides-and-modding.md § 4.1) is: TemplateId, ModifierId, ChannelId, TagId, PhaseId, TickRateId, ScopeId, ContractId, SystemId, EventId, OnActionId, ActivityId, ActivityRunId, ActivityLaneId, PolicyId, DomainId, NeedId, SelectorId, RuleId, SlotKindId. All are readonly record structs wrapping ulong. The compiler and generated code must not allow accidental cross-type mixing. TemplateId is also the canonical key type for ScopedDictionary<TemplateId, T> (Phase 2).
  • Study how record was added — trace the full path: SyntaxKind, parser method, syntax node definition, lowering pass.
  • Add syntax for structural declarations:
    • SecsTypeDeclarationSyntax for C# struct, record, and enum declarations. Phase 1 must bind C# source type syntax recursively into SecsTypeRef metadata for scalar, scope entity, template reference, enum, struct, record, array, and supported collection forms; do not lower callable signatures through SecsValueKind.
    • ScopeDeclarationSyntax for scope, walks_to, fields, collection names, and scope-method signatures. Collections in Phase 1 are name-only compile-time members used for hashes and foreach binding only; typed ScopedList<T> / ScopedDictionary<TKey, T> declarations and CollectionDeclaration[] emission are Phase 2 work (see Step 3 of docs/design/08-collections-and-propagation.md).
    • ContractDeclarationSyntax for root_scope, lifecycle bindings, query / method signatures, and context profiles. The committed lifecycle source spelling for now is activation SomeMethod;; on_activate SomeMethod; is an open spelling alternative, not a Phase 1 parser target.
    • Top-level channel and field declaration syntax, enough to populate declaration metadata and hash ids.
  • Emit deterministic FNV-1a-64 H.* constants for every declared identifier. Hash inputs must be canonical identity strings of the form <namespace>:<kind>/<name> (e.g. valenar:template/farm, valenar:channel/population), not bare display names. This prevents unrelated mods from accidentally colliding on common names. FNV-1a-64 collision between two distinct canonical identities is diagnostic SECS0601. Per docs/design/06-overrides-and-modding.md § 4.2.
  • Run a structural binding pass before template lowering:
    • Every source type reference in fields, callable signatures, struct/record fields, array elements, collection generic arguments, template references, and scope entity references must resolve to a built-in type or SecsTypeDeclaration.
    • Every contract root_scope must resolve to a declared scope.
    • Every query / method signature must lower to a ContractMethodDeclaration row with SecsTypeRef return/parameter metadata and owner-and-signature hash. query may return structured types; method must return void.
    • Every contract method id referenced by ContractDeclaration.QueryMethodIds, MethodIds, or LifecycleBindings must have matching metadata. Lifecycle binding spelling is syntax only; emitted metadata always maps ContractLifecycleIds.Activation to the declared parameterless void method id.
    • Every scope method signature must lower to a ScopeMethodDeclaration row with return and parameter SecsTypeRefs.
    • Every context profile entry must resolve through the contract root scope and declared walks_to graph.
  • Emit and register in the lowered C# tree:
    • SecsTypeDeclaration[]
    • ScopeDeclaration[]
    • ScopeFieldDeclaration[]
    • ScopeMethodDeclaration[]
    • TemplateFieldDeclaration[]
    • ChannelDeclaration[]
    • ContractDeclaration[]
    • ContractMethodDeclaration[]
    • ContextProfileDeclaration[]
  • Add TemplateDeclarationSyntax and lower template<C> T { } for any declared contract C:
    • Reject undeclared contract names; do not assume Building, Location, or any Valenar contract/root names.
    • Static class T
    • TemplateEntry with TemplateId, Name, ContractId, RootScopeId, and an empty Activate(ScopeFrame scope, ISecsHostReads host, CommandBuffer cmds)
    • registry.RegisterTemplate(T.Id, T.Entry) in SecsModule.Initialize
  • Emit SecsModule.Initialize in the Phase 1 structural order listed above: scopes/fields/methods/channels first, then contracts/contract methods/context profiles, then template entries.
  • Verification: compile a small generic .secs source set with declared enum/record/struct types, a declared scope, a scope method signature, a contract with a structured-return query and lifecycle method signature, a context profile, and an empty template; inspect --emit-generated-cs output or the decompiled DLL for SecsTypeDeclaration[], ScopeFrame, TemplateEntry, ScopeMethodDeclaration[], ContractDeclaration[], ContractMethodDeclaration[] with SecsTypeRef, ContextProfileDeclaration[], and registration. Do not start broad golden-test infrastructure in this phase.

Phase 2: Template Members, Scope Frames, and Expressions

Goal: Lower the practical template surface on top of Phase 1 structure: fields, own-root channels, formula registration, contract query/method bodies, and scope-aware read/write expressions.

Valenar reference example:

template<Building> Farm
{
channel int FoodOutput
{
return (int)(5 * @Location.Fertility / 33);
}

field int GoldCost = 10;

bool CanBuild()
{
return @Settlement.Stage >= 1
&& @Settlement.Gold >= template_field(Farm, GoldCost, BuildCostContext);
}
}

Valenar's Building, Farm, Location, and Settlement names are examples only. The lowering is generic over whatever contract root scope and walks_to graph the source declares.

  • Add template-field assignments and typed scalar/structured accessors. Scalar fields use GetFieldInt / GetFieldLong / GetFieldFloat / GetFieldDouble / GetFieldBool; structured fields emit concrete immutable value accessors keyed by TemplateFieldDeclaration.ValueType = SecsTypeRef.*.
  • Add template-body channel declarations:
    • Static own-root channels lower to cmds.RegisterChannelSource(scope.Owner, scope.Root, ...) in Activate after compile-time scope validation.
    • Dynamic own-root channels lower to cmds.RegisterDynamicChannelSource(scope.Owner, scope.Root, ...), a formula delegate, registry.RegisterFormula(...), and registry.RegisterFormulaContribution(...).
    • Cross-scope channel effects are rejected as template channels and must be expressed as named modifiers.
  • Add ScopeFrame-based lowering for contract query and method bodies:
    • Queries return their declared concrete type through typed query registrations/adapters; erased dynamic dispatch validates against SecsTypeRef.
    • Void methods use TemplateCommand<TArgs> / TemplateCommandDelegate<TArgs> and ITemplateCommandContext.
    • Method/query ids are looked up through ContractMethodDeclaration[], not hard-coded names.
    • Implementing a query or method that is not declared by the template's contract is a compiler error; omitting an implementation follows the current runtime default/no-op behavior.
  • Add scope sigils and expression lowering:
    • @SomeScope.SomeField -> declared scope walk from the current frame, then a typed host read using the generated scope/field hash constants when a walk is required.
    • @SomeScope.resolve(SomeChannel) -> declared scope walk from the current frame, then typed channel resolution.
    • Read-only scope methods -> generated typed query facades over ISecsHostReads.CallScopeQuery<TArgs,TResult>.
    • template_field(...) -> base/effective template-field reads with explicit context chain or ContextProfileDeclaration expansion.
  • Add command-producing expression lowering in template contract methods (increment, add_modifier, void scope methods) through ITemplateCommandContext.Commands; void scope methods use generated typed command facades over the active command context. Emit context.FlushCommands() after each command-producing source statement.
  • Add the feature-placement typed query pattern as the structured-data smoke test: field FeaturePlacementProfile Placement = new(...) plus query FeaturePlacementResult EvaluatePlacement(Location candidate, FeaturePlacementInput input). The compiler must keep this query read-only; global budgets, spacing, uniqueness, and conflicts are applied by generated/host systems after collecting FeaturePlacementResult values.
  • Bind tag identities from plain C# static readonly TagId values such as public static readonly TagId Food = TagId.Create("valenar:tag/food");, and add tags = T1, T2; template/activity-body syntax plus policy selector from tags = T1, T2 binding. Top-level tag <Name>; is rejected per 06-overrides-and-modding.md § 19.4 Tag identities stay plain C#; there is no TagDeclaration[] array. Generated output must register collected tags with registry.RegisterTag(id, name) so structural predicates, validation, and tooling share one registry-backed tag catalog. Lower tags = ... inside a template body to the Tags initializer on the generated TemplateEntry; lower tags = ... inside an activity body to SecsActivity.Tags; lower consider activities from tags = ... to new SelectorSource.FromTags(new[] { H.Tag_* }). Emit/analyze unknown tag references against the registered catalog. Emit the three structural predicate operators (has_tag, has_template, has_contract) as (FilterKind, hash) pairs usable in propagates_to where clauses (Phase 4) and aggregate channel Children.Where(...) expressions (Phase 4). Current Valenar provenance covers template tags only; executable Valenar provenance for activity tags and policy from tags is deferred to a scoped tag-fixture wave that lands .secs, Generated, and tests together. Live Valenar fixtures for propagation has_tag and aggregate .Where(has_tag ...) also remain deferred until a real gameplay use exists, with generic runtime tests covering the APIs meanwhile. Undeclared tag diagnostics (SECS0212) are part of the same binding pass. Short tag aliases may resolve only when exactly one declared tag in the merged catalog exports that alias; if multiple tags share the alias, the compiler must reject the use with a dedicated future/reserved tag-alias ambiguity diagnostic (diagnostic number intentionally unassigned until alias binding is committed). Do not implement short-alias binding until that ambiguity behavior and diagnostic routing are committed.
  • Add typed collection declarations in scope bodies. Replace the Phase 1 name-only collection placeholder with ScopedList<T> and ScopedDictionary<TKey, T> field syntax, lowering each to a CollectionDeclaration entry (ParentScopeId, FieldHash, Kind, KeyType, ElementScopeId). Add CollectionDeclaration[] registration to SecsModule.Initialize. Emit H.<ParentScope>_<FieldName> hash constants for the anonymous collection scope identity. Add the GetChildByTemplate / WalkChildren bridge contract to ISecsHostReads as the host-side iteration surface. Verify ScopedDictionary<TemplateId, T> key-type checking (TemplateId from Step 1) and reject ScopedList<T> indexed access with SECS0822. Closes Open Question #5 — collection Name of ScopeType is superseded by typed ScopedList<T> / ScopedDictionary<TKey, T>.
  • Auto-generate anonymous-collection lifecycle hook method ids. For each ScopedList<T> / ScopedDictionary<TKey, T> field declared on a scope, emit On{FieldName}Added(T child) and On{FieldName}Removed(T child) as ContractMethodDeclaration entries on the parent scope's contract. If the contract does not declare matching method bodies, the engine silently skips dispatch — no warning. Emit SECS0840 when two collection fields on the same scope produce colliding hook names.

Phase 3: Mod Operation & Merger

Authority: docs/design/06-overrides-and-modding.md is the authoritative design document for everything in this phase. When this plan and that doc conflict, the doc wins.

Goal: Parse the six mod operation keywords, build the slot-based merge pipeline, and emit a single merged AST that the Phase 1/2 binder/lowerer sees as one compiled game.

Wave 6 baseline: the runtime side of mod operations is already implemented and shipped for activities and policies (Wave 6). Phase 3's job is to parse .secs mod source and emit ActivityMod / PolicyMod instances into the merged registration. The runtime types (ModRegistry, ModSlotKey, ActivityMod, PolicyMod, ActivityModPatch, PolicyModPatch, MergedActivity, MergedPolicy, ModOperationKind, ModDiagnostic, ModDiagnosticCode) live under src/SECS.Engine/Modding/. Slot schema is enumerated in 06 § 8.10 (25 activity slots — Wave 6 added activity_args_schema) and 06 § 8.11 (6 policy slots), in 1:1 correspondence with the ActivitySlotKind / PolicySlotKind enums. Diagnostic codes 600–611 are errors (throw on Finalize); SECS0800 is a warning (slot conflict); SECS0801 is an error (inject into replace-only architectural slot). The compiler does not need to re-implement merge semantics — it parses, builds the merged AST, and emits typed mod objects targeting the existing runtime.

The old single override Name { ... } keyword is replaced by explicit database operations per 06 § 3.

Operation syntax

Six operations are parsed; create is implicit (a normal declaration):

inject template Farm { field GoldCost = 8; }
replace template Farm { ... }
try inject template Farm { ... }
try replace system OldGrowthSystem { ... }
inject_or_create modifier GrainMarketBonus { ... }
replace_or_create template<Building> Lighthouse { ... }

ModOperationDeclarationSyntax wraps any of these six prefixes around a standard SECS declaration syntax node, carrying ModOperationKind and the inner declaration node.

SlotKey structure

Do not combine declaration and slot hashes with XOR. Use the structured key from 06 § 7.3. The Wave 6 runtime ships the concrete shape under src/SECS.Engine/Modding/ModSlotKey.cs:

public readonly record struct ModSlotKey(
DeclarationKind DeclarationKind,
ulong DeclarationId,
int SlotKind, // backed by the ActivitySlotKind / PolicySlotKind enums; cast to int for cross-kind storage
ulong ChildId); // 0UL when the slot is whole-declaration; non-zero when keying per-row (NeedId, RuleId, cost id, etc.)

Examples: (Activity, H.Activity_ScoutNearbyLead, (int)ActivitySlotKind.Duration, 0UL), (Policy, H.Policy_CharacterSurvival, (int)PolicySlotKind.Needs, H.Need_StayAlive).

Implementation pipeline (Steps 1-7, per 06 § 16)

This is the core differentiator. The merger runs seven steps:

  1. Parse source sets independently. Each source set (base, expansion, each mod in load order) is parsed to its own syntax tree and source-set-local diagnostics. No cross-set dependencies at parse time.
  2. Build declaration catalog. Walk all source trees; record each declaration's kind, name, canonical id (<namespace>:<kind>/<name>), typed id, source set, load order, file path, source span, declaration AST node, and slot schema.
  3. Extract semantic slots. Each declaration kind uses its schema (per 06 § 8). A system yields (system_phase, null), (system_frequency, null), (system_execute_body, null). A template yields one template_field slot per field, one template_dynamic_channel_formula per dynamic channel, one template_query_method per query, etc. The full slot table is in 06 § 8.1-8.8 — reference that doc; do not duplicate it here.
  4. Apply operations in load order. For each source set in resolved load order: create → SECS0602 if identity exists; inject → write listed slots (SECS0603 if target missing); replace → replace full declaration (SECS0603 if target missing); try inject / try replace → silent no-op if target missing; inject_or_create / replace_or_create → inject/replace if exists, create if not. Every slot write is appended to slot history. The current value is the last write.
  5. Produce merged AST / merged IR. The merged result retains source provenance per slot: source-set id, load order, source path, source span, syntax node payload, typed semantic payload. Type errors in mod slot bodies must point to the mod's source line, not generated code. This is the primary benefit of the Roslyn fork.
  6. Bind and validate merged result. Run normal C#/SECS binding after merge. Catches unknown identifiers, type mismatches, invalid channel operations, illegal scope walks, bad contract methods, dependency cycles, read-set errors, and all diagnostics in 06 § 12.
  7. Lower once. The lowerer sees one merged world and emits Generated/Hashes.cs, Generated/Declarations.cs, Generated/Templates/*.cs, Generated/Systems/*.cs, Generated/Events/*.cs, Generated/SecsModule.cs, Game.Merged.dll, and conflict-report.json.

Slot schema

Every declaration kind has a schema (per 06 § 8) defining: allowed members, slot identifiers, cardinality, base/inject/replace requirements, merge rule, diagnostics, lowering target, and conflict-report kind. Adding a new language feature should mean adding a new schema entry, not writing one-off merge code.

Conflict detection

A slot is conflicted when 2+ mods write the same SlotKey. One mod writing is clean. Two or more mods on the same slot = load order matters, emit to conflict-report.json. Conflicts are not compiler errors by default; --strict-conflicts turns them into errors. Strict modes per 06 § 13.

Re-merge on reorder: Syntax trees are already parsed. Re-walk the operation list in new load order. No re-parsing. Fast enough for interactive mod manager UI.

Canonical diagnostics

Diagnostics SECS0601SECS0616 are defined in 06 § 12. The merger must emit these at the source line that caused each diagnostic.

Architectural-slot rejection (Wave 6 closed under inject)

Four activity slots — activity_actor_scope, activity_target_scope, activity_lane, and activity_args_schema (Wave 6, ActivitySlotKind.ArgsSchema = 14) — and two policy slots (policy_actor_scope, policy_domain) are replace-only at runtime: ModRegistry.RegisterActivityMod / RegisterPolicyMod emit SECS0801 (InjectClosedSlot) at Severity.Error when an inject / try inject / inject_or_create patch sets any of them. Phase 3 mod parsing must reject inject against these slots earlier, at parse time, so the user sees the error pinned to the offending source line rather than a runtime registration throw. Reserved diagnostic ids for the parser-side rejection:

  • SECS0420 (error) — inject activity X { actor ... | target ... | lane ... | (typed-args header) } cannot set replace-only architectural slots activity_actor_scope / activity_target_scope / activity_lane / activity_args_schema. Use replace activity to change architectural identity. Sub-cluster SECS0420 is reserved here so it does not collide with the Wave 7 activity static-analysis warnings at SECS0410-0415.

The activity-args schema slot is keyed by ActivitySlotKind.ArgsSchema = 14 and lowers from typed-args activities (activity Build(BuildStructureArgs args) { ... }) to an ExpectedArgsSchemaId override on the generated SecsActivity subclass. Untyped activities omit the override and inherit the runtime default 0UL ("accept any blob"); see Phase 4 below.

Structural merge rules

Host-capable source sets (base game plus official expansions/DLC that ship matching host code) may add new scopes, scope fields, collections, scope methods, and walks_to edges. Third-party mods are data-only by default: they consume the exported host-capable scope graph and cannot add host-backed structural surfaces (SECS0609). Contracts are closed surfaces; existing contract method lists, lifecycle bindings, and root_scope are not mod-extended (06 § 8.7). Types are additive but closed under inject/replace for existing exported types (06 § 8.8).

Signature stability

Scope and contract callable ids are regenerated from canonical owner+signature text after merge. Body replacements keep the same method id; additions of new scope methods or new contracts get new ids. Do not emit name-only method hashes.

Expansion/mod boundary

Official expansions are processed as part of the effective base layer before third-party mods and may update both host code and .secs declarations. Third-party mods do not ship host bridge DLLs under the committed runtime model.

Raw C# boundary

Phase 3 is committed for SECS constructs and mod-added SECS declarations. override class is not a committed Phase 3 feature; per 06 § 10 (open question), raw C# class override semantics are unsettled. Do not implement or depend on it here.

Phase 4: Modifiers, Systems, Events, On-Actions, Activities, and Policies

Goal: Complete the committed SECS syntax surface that executes during ticks or dispatches.

Contracts are no longer a Phase 4 afterthought; the structural subset lands in Phase 1 because templates depend on it. Phase 4 adds the remaining behavior constructs that consume those declarations. Activities (Wave 1, replacing the prior action keyword) and policies (Wave 4) ship in this phase alongside the older modifier/system/event/on-action surfaces. Their runtime targets — SecsActivity, EffectPlan, IActivityArgs<TSelf>, SecsPolicy, NeedTargetExpr, RuleDecision — are already shipped and exercised by hand-written stand-ins; Phase 4 emits the same shape from .secs source.

  • ModifierDeclarationSyntax -> ModifierDeclaration[] with channel/template-field ModifierEffect[], Stacking, MaxStacks, ReApplyMode, Decay, tags, dynamic formula effects, and triggers. Emit the PropagationClause field when propagates_to children [where ...] is present: PropagationMode, PropagationFilter, FilterTags. Reject channel-based and value-based predicates in propagates_to where with SECS0802. Virtual binding materialization in ModifierBindingStore follows the five propagation rules in docs/design/08-collections-and-propagation.md § Part 2: owner preservation (Rule 1), stacking keyed by (modifier_id, owner_id) on the collection (Rule 2), lifecycle mirroring the source binding (Rule 3), structural-only where predicates (Rule 4), and HardOverride closer-scope-wins priority (Rule 5).
  • Add AggregateChannelSource as a new channel source kind. In top-level channel declarations addressed on Parent.FieldName, lower source = Children.Sum/Min/Max/Average/Count/Where(...) expressions to AggregateChannelSource metadata in ChannelDeclaration. Register DirtySet dependencies on child channel changes and on child join/leave events so that aggregate channel cache entries are invalidated when collection membership changes. Only structural predicates (has_tag, has_template, has_contract) are valid in aggregate .Where(...); channel-based and arbitrary lambda predicates are rejected.
  • Add track_prev = true as a ChannelDeclaration property. When set, the engine snapshots the resolved channel value at end-of-tick into PrevTickSnapshotStore. The committed runtime/generated read shape is the host bridge ReadPrevTick* family; standalone prev_tick(...) syntax and a committed ctx.PrevTick(...) lowering are not part of the source contract. Reserve SECS0830 for the future binder/analyzer check that rejects prev-tick bridge/API reads on channels whose declarations do not have track_prev = true.
  • SystemDeclarationSyntax -> class implementing ITickSystem, static SystemRegistration, slot recognition for phase = X; and frequency = Y; inside the system body, load-distributed AllByContractForTick for distributed frequencies, and statement-level ctx.FlushCommands() after command-producing source statements. Top-level phase and cadence values are plain C# helper classes, e.g. Phases.Growth = PhaseDeclaration.Create(...) and Cadence.Daily = TickRate.Days(1), not SECS declarations. The compiler type-checks phase = ...; as a PhaseDeclaration expression and frequency = ...; as a TickRate expression, then emits the resolved SystemRegistration values.
  • Add the registry[id] ambient accessor. In system Execute bodies and formula bodies, lower registry[expr] to context.Registry.GetTemplate(expr). Return type is TemplateEntry? (nullable). Emit a compile error when registry[id] is used outside a context that has TickContext available (e.g., modifier effect bodies run in the resolution path, not the system path). Contract-typed uses (registry[recipeId] where the declared type is TemplateEntry<Recipe>) allow field access validated against the template's contract.
  • SecsEventDeclarationSyntax -> sealed class deriving from SecsEvent, with Condition(EventContext), Execute(EventContext), optional BuildOptions(EventOptions options, EventContext context), option handler methods taking EventContext, and static EventEntry Registration. Rejected stale shapes: interface-based events and the contextless one-parameter options builder.
  • OnActionDeclarationSyntax -> OnActionDeclaration metadata only: OnActionId, Name, ScopeId, ProvidedScopes, and SelectionMode. Source-authored on-action effect bodies, fallback, and chain syntax are not committed; guaranteed effects lower as ordinary subscriber events with priority. Reserved runtime fallback/chaining fields stay inert metadata and must not become live compiler-emitted dispatch behavior until the syntax and mod semantics are recommitted.
  • SecsActivityDeclarationSyntax (Wave 1, replacing the prior SecsActionDeclarationSyntax) -> class deriving from SecsActivity, using ActivityContext for read-only methods (CanStart, IsTargetValid, Preview) and ActivityRunContext for command-producing lifecycle methods (OnStart, OnUpdate, OnComplete, OnStop, OnCancel, OnFail). Typed activity args declared as record struct implementing IActivityArgs<TSelf> (Wave 3) and serialized via ActivityArgsBlob(SchemaId, Payload) with hex/base64 wire shape. When the source declares typed activity args (e.g. activity Build(BuildStructureArgs args) { ... }), the compiler MUST emit an ExpectedArgsSchemaId override returning default(BuildStructureArgs).SchemaId so the executor's boundary schema check (Wave 2 of the Behavior Refactor) rejects mismatched blobs before any lifecycle hook runs. Untyped activities omit the override and inherit the default 0UL ("accept any blob"). The compiler MUST also emit the activity's LaneOccupiedPolicy override only when the source declares a non-default lane-occupied policy; the runtime default is ActivityLanePolicy.Reject and only Reject is currently implemented — Queue and Preempt throw NotImplementedException at start time per the no-silent-fallback tenet. Preview returns EffectPlan of PredictedEffect rows (Wave 2) carrying optional ModifierId (Wave 7) for PredictionOp.Modify projection. Note: GetAiWeight was deleted in Wave 4 — utility-AI scoring is handled by PolicyExecutor against EffectPlans, not by per-activity weight overrides. Old action allow / effect block syntax is prototype-only and must be rejected unless the design docs recommit it.

Hand-stand-in batch wrappers. The current examples/valenar/Generated/Activities/Character/Site/CharacterSiteActivities.cs uses a single CharacterSiteActivity wrapper class that delegates to data-driven CharacterSiteActivityDefinition records, rather than emitting one SecsActivity subclass per activity. This is a legitimate hand-authoring optimization for content-heavy activity families with shared body shape; the future compiler may emit either shape. Both must be supported by the runtime registry — SecsActivity is duck-typed by ActivityId, not by class identity.

  • SecsPolicyDeclarationSyntax (Wave 4) -> class deriving from SecsPolicy with Needs : IReadOnlyList<NeedDeclaration>, Selectors : IReadOnlyList<SelectorDeclaration>, Rules : IReadOnlyList<RuleDeclaration>, and EvaluateRule(RuleId, PolicyRunContext) -> RuleDecision dispatching by rule id. NeedDeclaration carries (NeedId, Name, ChannelId, NeedTargetExpr Target, NeedCurveKind Curve, Threshold, Weight); NeedTargetExpr is a discriminated union (Literal | ChannelRef | Formula) — Formula evaluates at scoring time via Registry.GetFormulaInt (Wave 7). SelectorSource is a discriminated union (AllRegistered | FromTags | FromCollection(SlotKindId Slot, CandidateBuilderId Builder)); FromCollection is the runtime/lowering form used when a selector is backed by stored definition references in ScopeSlotStore, and the bridge keyed by Builder reads those references and emits typed-args ActivityRequest values. This is compiler/runtime support rather than standalone source vocabulary. RuleDecision is a discriminated union of seven cases: Continue / Complete / Fail / Wait / Call(activityId, target, args) / CallBest(selectorId) / CancelChild.
  • foreach entity in Contract -> ctx.InstanceStore.AllByContract(H.ContractId) or AllByContractForTick(...).
  • foreach child in parent.Collection -> ctx.Host.GetChildren(parent, H.CollectionId) + indexed loop.
  • increment, add_modifier, and void scope-method calls -> command lowering plus statement-level flushes in system/event/activity bodies; template contract methods already use the ITemplateCommandContext rule from Phase 2. save_scope_as and fire on_action -> saved-scope / dispatcher lowering with provided-scope validation.
  • remove_modifier remains a deferred source keyword even though runtime helpers exist; do not make it part of Phase 4 unless docs/design/05-expressions.md is updated first.
  • event disambiguation: SECS event at declaration level, C# event inside ordinary class bodies.
  • Reserved diagnostic codes for removed legacy keywords (Wave 1 of the SECS Behavior Refactor): Parser-level errors. Lex/parse phase. Distinct from runtime registry diagnostics SECS0410-SECS0414 (activity warnings) and SECS0910-SECS0914 (policy warnings). The reservation is locked here so the codes do not collide with future Phase 4 diagnostics.
    • SECS0030 (error) — 'action' keyword is removed; use 'activity'. Fires on top-level action Name { ... } declarations and on action_* slot identifiers in mod operations. Replacement guidance is in docs/design/04-behavior.md § Activities and docs/decisions/ADR-0002-behavior-vocabulary.md.
    • SECS0031 (error) — 'program' keyword is removed; use 'policy'. Fires on top-level program Name { ... } declarations. Replacement guidance is in docs/design/04-behavior.md § Policies and docs/design/09-ai-policies-and-activities.md.

Phase 4 verification should use narrow behavior smoke sources rather than broad golden suites: one pulse event, one on-action subscriber event with a provided saved scope, one metadata-only on-action declaration, one class-style activity that calls a void scope method and flushes, and one minimal policy with a need + selector + rule. Inspect lowered C# or decompiled output for SecsEvent, EventEntry, BuildOptions(EventOptions options, EventContext context), OnActionDeclaration[], SecsActivity, ActivityContext, ActivityRunContext, EffectPlan with PredictedEffect rows, IActivityArgs<TSelf> typed arg structs, SecsPolicy with NeedDeclaration / SelectorDeclaration / RuleDeclaration arrays and an EvaluateRule dispatch, and explicit FlushCommands() after command-producing source statements.

Phase 5: secsc CLI

Goal: Single binary wrapping the forked compiler with SECS-specific commands.

CommandWhat it does
secsc build --project Content/Parse → merge → lower → compile → Game.Content.dll
secsc build --base Content/ --mods A,B --order A,BParse → merge in order → lower → compile → Game.Merged.dll
secsc validate --base Content/ --mods A,BParse → merge → binder diagnostics → report (no DLL)
secsc conflicts --base Content/ --mods A,B --order A,BParse → merge → conflict report JSON only
secsc calc --template <template> --scope <scope>.<field>=<value> --resolve <channel>Load engine, register template, seed mock scope fields, resolve a channel
secsc new "ModName" --game-path /pathScaffold mod project
secsc pack --mod-path ./ModNamePackage mod into .secsmod archive
secsc export-sdk --game-path /pathExtract ModSDK (base .secs source, definitions.json, tools)
secsc lspStart language server
secsc watch --project Content/File watcher, re-validates on save

Ships as a single self-contained binary — no .NET SDK required on the target machine.

Phase 6: LSP & VS Code Extension

Goal: First-class editor experience for .secs files.

Built on Roslyn's existing Workspace and LSP infrastructure inside the fork.

LSP server (secsc lsp):

  • Diagnostics (error squiggles from full compiler pipeline)
  • IntelliSense for channel names, template names, scope paths, modifier names
  • Go-to-definition: jump from an inject / replace operation to the target base game definition
  • Hover info: show the base and winning slot payload when hovering a mod operation
  • Find all references: which mods write the same declaration or slot
  • Semantic highlighting for SECS keywords

VS Code extension:

  • TextMate grammar extending C# grammar with SECS keywords (syntax highlighting without LSP)
  • LSP client connecting to secsc lsp
  • Packaged as .vsix, included in game's mod tools
  • Standard LSP — works with any editor (JetBrains, Vim, Emacs)

Phase 7: Mod Manager UI

Goal: Player-facing conflict resolution tool.

  • Consumes secsc conflicts JSON output
  • Drag-to-reorder mod list → re-runs merge with new order (instant, no re-parsing)
  • Per-slot diff: base value vs each mod's version, winner highlighted
  • "Create Patch" button: scaffolds compatibility mod
  • Technology: Avalonia (cross-platform .NET desktop) or the game developer builds their own UI consuming the JSON
  • Can be embedded in game launcher or run standalone

Phase 8: Runtime Script Console

Goal: Paradox-style runtime console — evaluate SECS expressions against live game state.

// Example session using Valenar names.
> @Location.Fertility
33

> template_field(Farm, GoldCost, BuildCostContext)
10

> foreach entity in Settlement { print entity.resolve(Population) }
Entity#1: 42

> inspect Farm
FoodOutput: 5 (dynamic: 5 * @Location.Fertility / 33)
GoldCost: 10 (field)
...

Uses Roslyn's CSharpScript API. The console parses SECS expressions, lowers them (same lowering pass the compiler uses), and feeds the lowered C# to CSharpScript.EvaluateAsync() with the live game registry and an explicit ScopeFrame / TickContext as globals.

SECS provides the evaluation engine; the game developer provides the UI shell (ImGui overlay, in-game terminal, etc.).


Compilation Flow

Game Developer (no mods)

secsc build --project Content/

├── Parse all .secs files → AST
├── Structural prepass → scopes, contracts, method signatures, hashes
├── Validate (references, types, scope walks, cycles, callable signatures)
├── Lower SECS nodes → ordinary C# SyntaxTrees (matching current Generated/ pattern)
├── Add SecsModule.Initialize + Systems[] + Events[] + Activities[] + Policies[] + ActivityMods[] + PolicyMods[] to the lowered trees
├── Roslyn binder + emitter → IL
└── Output: Game.Content.dll

Normal builds do not need to materialize .cs files. secsc --emit-generated-cs is a diagnostic/review mode that writes the lowered C# trees to disk so the output can be compared against examples/valenar/Generated/.

Player with Mods

Game Launcher

├── 1. Discover mods (read mod.json manifests)
├── 2. Resolve load order (dependencies, before/after constraints)

├── 3. Validate + detect conflicts (fast, no compilation)
│ secsc validate --base Content/ --mods A,B --order A,B
│ Parse ALL .secs → structural prepass → merge per-slot → conflict report
│ Player sees conflict grid, adjusts load order

├── 4. Compile merged result
│ secsc build --base Content/ --mods A,B --order A,B
│ Merge ALL .secs into ONE AST → recompute structural ids → lower to C# SyntaxTrees → compile → Game.Merged.dll

└── 5. Launch game
Game loads Game.Merged.dll
SecsModule.Initialize(registry) — one call, fully merged
Game runs — zero mod awareness at runtime

What Ships with the Game

Game/
├── game.exe
├── Game.Content.dll compiled base content
├── SECS.Engine.dll runtime engine (netstandard2.1)
├── SECS.Abstractions.dll shared types (netstandard2.1)

├── Content/
│ └── base/ source .secs files (modder reference)
│ ├── common/ (channels, modifiers, scopes, contracts)
│ ├── templates/
│ ├── systems/
│ ├── events/
│ ├── on_actions/
│ ├── activities/
│ ├── policies/
│ └── mods/

├── Tools/
│ ├── secsc compiler CLI (self-contained, no SDK needed)
│ ├── secs-lang.vsix VS Code extension
│ └── definitions.json machine-readable catalog for tooling

├── ModSDK/
│ ├── mod-template/ scaffold project
│ └── MODDING.md game-specific modding guide

└── Mods/
└── (player-installed mod folders with .secs source)

Namespace Rules (Compiler-Enforced)

SECS Constructs

  • Flat global registry. template<SomeContract> SomeTemplate is SomeTemplate everywhere.
  • Identity is the canonical string <namespace>:<kind>/<name> (e.g. valenar:template/farm), hashed to ulong with FNV-1a-64. This prevents accidental collisions on common names across mods.
  • inject template Farm / replace template Farm match by canonical identity, not by C# namespace. Short names are resolved through the exported SDK/identity table; ambiguous short names are SECS0610.
  • No namespace qualifier is needed or allowed on bare SECS construct declarations.

Raw C# Code in .secs Files

  • Mandatory namespace isolation.
  • Base game: developer declares their own namespace (MyGame.Helpers).
  • Mods: compiler auto-prefixes with mod name if no namespace declared.
    • class Helper in ExampleMod -> ExampleMod.Helper
  • Prevents collisions between mods that happen to use the same class names.

Raw C# Class Replacement

  • Deferred/open. docs/design/06-overrides-and-modding.md explicitly leaves namespace-scoped override class Helper semantics unsettled.
  • Phase 3 is committed only for SECS construct operations (inject, replace, and the four variants). Raw C# helpers are namespace-isolated and compiled after the SECS merge; replacing raw C# class bodies is future design work.
  • Do not use raw C# class override as evidence for the committed SECS mod-operation model or as a Phase 3 deliverable.

Mod Manifest Format

{
"id": "example_mod",
"version": "1.2.0",
"name": "Example Mod",
"author": "ModderName",
"game_version": ">=1.0.0",
"dependencies": [],
"load_after": ["example_dependency"],
"load_before": [],
"tags": ["balance", "buildings"],
"content_roots": ["Content/"]
}

Risk Mitigations

RiskMitigation
Roslyn build is complex / slowDocument build steps in Phase 0. CI caches. Build only the C# compiler project, not full Roslyn solution.
Upstream Roslyn changes break forkMerge upstream quarterly, deliberately. Track release notes.
SECS syntax evolves during developmentEach keyword is isolated — changing channel syntax doesn't affect system parsing.
.secs files with broken C# in method bodiesRoslyn handles natively — diagnostics point at exact line in .secs file.
Large Roslyn submoduleFull clone is ~3GB. One-time cost. Needed for tracing record implementation.
event keyword collision with C#Context-based disambiguation: SECS event at declaration level, C# event inside class bodies. Same pattern as record.

Resolved Decisions

  1. File extension: .secs
  2. Compiler target: the Roslyn fork currently builds with the .NET SDK pinned by secs-roslyn/global.json (10.0.105 today). Engine targets netstandard2.1. Compiler output targets whatever the game targets (configurable).
  3. Hot reload: Not needed. Game requires restart after recompilation.
  4. Runtime console: Yes (Phase 8). Uses Roslyn's CSharpScript API with SECS lowering.
  5. Mod-operation merge: Compiler pass (pre-binding). Merge-first, compile-once.
  6. Conflict detection: Byproduct of merge pass. Structured JSON output consumed by mod manager UI.
  7. No runtime parsing: Mods ship .secs source to the compiler/launcher, not independent runtime DLLs. The compiler merges base content plus enabled mods in load order, lowers the merged tree, and emits one Game.Merged.dll that the game loads with no .secs parsing and no per-mod DLL awareness at runtime.
  8. Host extension boundary: base game and official expansions are host-capable source sets; third-party mods are data-only unless a future explicit mod-state/host-extension mechanism is designed.

Open Questions

Items in this section are explicit non-goals for the phase checklists above until their home design docs settle them.

  1. Namespace isolation for SECS constructs: Currently flat/global (Paradox-style). May revisit if collision becomes a problem at scale.
  2. secsc distribution: Self-contained single binary vs dotnet tool. Same code either way, just packaging.
  3. Unity integration: For Unity consumers, assembly loading and .asmdef considerations exist but are not the primary target.
  4. Lifecycle binding spelling: activation SomeMethod; vs a possible on_activate SomeMethod; is still open in docs/design/01-world-shape.md; Valenar's activation OnBuilt; is only an example. The emitted shape is settled: ContractLifecycleIds.Activation -> H.Contract_<Contract>_<Method>_....
  5. Collection declaration typing: Resolved. docs/design/08-collections-and-propagation.md supersedes the bare collection keyword with typed ScopedList<T> and ScopedDictionary<TKey, T> field syntax in scope bodies, lowering to CollectionDeclaration[]. Implemented in Phase 2.
  6. Bare template defaults: BareLocation's default Slots = 4 lacks a source declaration; see docs/design/02-templates.md. The compiler must not invent hidden Valenar defaults.
  7. Event option descriptions/localization: Generated event choices have descriptions by hand today; docs/design/04-behavior.md leaves the source/localization convention open.
  8. Activity cost metadata: Class-style activities (Wave 1 rename of the prior action keyword) are committed, but high-level inspectable cost metadata is open in docs/design/04-behavior.md.
  9. Contract-call helper names: docs/design/05-expressions.md marks helper names such as contract_query<T> / contract_method as target spellings, not final API names. The ABI shape is committed: template id, callable id, ScopeFrame, args, query read boundary, and template command context.
  10. Raw C# class overrides: docs/design/06-overrides-and-modding.md leaves override class Helper open. Until that design is settled, the compiler plan commits only to namespace isolation for raw C# helpers and the six SECS mod operations (inject, replace, try inject, try replace, inject_or_create, replace_or_create) for SECS constructs.
  11. Engine-owned mod state: There is no committed generic storage system that lets data-only mods add persistent per-entity fields, collections, walks, or host methods without host code. If this is desired later, design it explicitly instead of treating host-backed scope declarations as magically moddable.
  12. remove_modifier source keyword: Runtime helpers exist, but the .secs keyword is not committed; see docs/design/05-expressions.md. Do not include it in compiler phase scope until that doc changes.