00 — Overview
If you have not yet read
README.mdin this directory, start there — it is the architectural orientation that frames what each numbered doc covers. This file is the lowering-contract entry point.
This directory is the live design spec for SECS. Each document pairs SECS source with the C# it must lower to, so compiler, runtime, and hand-written stand-ins can target the same concrete shapes. The numbered docs also carry the current semantic rules needed to interpret those shapes, so the live contract path stays self-contained.
These docs are written against the hand-written code in examples/valenar/Generated/, except where a numbered design doc explicitly supersedes an older stand-in. In those cases the design doc is the live contract and the Generated/ tree is supporting evidence until it is updated.
Historical audits and recovery notes remain available for provenance, but they are not part of the first-pass reading path for this doc set. Keep live sections focused on the rule they define and link out only when provenance materially helps the reader.
SECS stands for Scripting Engine C Sharp.
What this doc set is (and is not)
Is:
- A lowering contract. Each feature appears as a
.secssnippet and the standard C# it compiles to. - A compiler-implementer reference. The file structure follows the eight-phase plan in
SECS-Compiler-Plan.md, so a phase-N task has one place to look. - A cross-check against
examples/valenar/Generated/for implemented surfaces, and a forward contract for explicitly marked replacement surfaces.
Is not:
- A tutorial. Examples are minimal and assume the reader can already follow C#-shaped SECS syntax.
- A style guide or coding-standard doc.
- A runtime-engine reference. Engine types are cited where they appear in lowered output, but their internals live under
src/SECS.Engine/. When a doc says a current engine type is legacy, the design doc constrains the future engine change.
Feature block template
Most feature sections in docs 01–09 use this layout. It is a recommended live-contract pattern, not a requirement to manufacture speculative follow-up text:
### {Feature name / keyword}
**What:** One-line description.
**SECS:**
```secs
{minimal authentic syntax from Content/ or minimal adaptation}
```
**Compiles to:**
```csharp
{minimal authentic compiled C# matching Generated/ patterns}
```
{Cite Generated/<path>.cs if not verbatim from there.}
**Why this shape:** 1-3 lines on the lowering choice (what tradeoff resolved, why not alternative X).
**Notes / follow-up (optional):** Only when a short current-contract note or an external backlog pointer materially helps the reader.
Section notes:
- What is one sentence. Not a paragraph. The reader already knows what a
templateis by this point; they want the one-line reminder. - SECS is the minimum syntax that exercises the feature. Copy from
examples/valenar/Content/when possible; adapt only to keep the snippet small. - Compiles to is the minimum C# that preserves the feature's observable behavior. It must match the shape in
examples/valenar/Generated/— if the compiler emits something different, the generator-stand-in is wrong or the spec is wrong; do not tolerate divergence. - Why this shape explains the lowering tradeoff. Not the semantics, not the engine internals — just why the emitted C# looks like this and not like some obvious alternative.
- Notes / follow-up is optional. Keep live rules in the feature block and send unresolved implementation or backlog items to
FUTURE_WORK.mdunless the live contract truly needs an inline note.
Glossary
Grouped by concept. One or two lines per term. The numbered design docs expand each term in context.
Template — Declared as template<Contract> Name { ... }. Defines what an entity is. Static; no per-entity state. Lowers to a static C# class plus a TemplateEntry registration.
SECS type — An author-facing type written with ordinary C# type syntax wherever C# already has the construct: scalar aliases, string, declared enum / struct / record names, scope entity types such as Location, arrays such as FeaturePlacementResult[], and chosen C# collection types such as IReadOnlyList<T> / IReadOnlyDictionary<TKey,TValue>. The compiler lowers those source types to SecsTypeRef metadata. SecsTypeRef is not author syntax. Channels remain scalar because the channel resolver is an arithmetic pipeline. See 07-structured-template-data-and-callables.md.
Channel — A named channel resolved through the channel pipeline. The channel keyword is the boundary: channel int Defense = 10; inside a template is a per-instance channel source, while field int GoldCost = 10; is template definition data and does not enter channel resolution. Top-level channel declarations define identity, type, ChannelKind, clamps, source binding, and display metadata (name, description), with localization keys allowed to override the inline text.
track_prev / previous-tick bridge reads — track_prev = true on a channel declaration enables an end-of-tick snapshot. Current generated/runtime code reads the snapshot through the host bridge ReadPrevTick* methods (for example ISecsHostReads.ReadPrevTickInt(handle, channelId)). Standalone prev_tick(...) and a committed ctx.PrevTick(...) author syntax/API are not live source syntax. See 03-channels-and-modifiers.md § Channel snapshot, 08-collections-and-propagation.md § Part 4, and 06-overrides-and-modding.md § 9.5.
on Parent.FieldName — Channel declaration prefix that locates the channel on an anonymous collection scope reachable via a parent field. Used when a channel lives on child entities referenced through a named collection field rather than on the template's own root scope. See 08-collections-and-propagation.md § Part 1.
Field — A readonly template-definition value declared globally with metadata (field int GoldCost { name = "Gold Cost"; description = "..."; }, or field FeaturePlacementProfile Placement { ... }) and assigned inside a template. It lowers to typed template-field accessors and TemplateFieldDeclaration.ValueType = SecsTypeRef.*, is visible to generated/host code that asks about a template, and is not accepted by resolve(...).
Template-value read — template_field(Template, Field) reads the static base template value only. template_field(Template, Field, context(@Country, @Settlement, @Location)) reads the effective template value in an explicit ordered context chain. template_field(Template, Field, BuildCostContext) uses a named context profile declared by the template's contract and expands to that profile's ordered chain. Context profiles name where modifiers are searched; validation, affordability, payer selection, and side effects stay in query/method bodies such as CanBuild().
Intrinsic contribution — A template's own push into the channel pipeline. Either static (channel int Food = 5) or dynamic (channel int Food { return ...; }). Registered as channel sources on activation, removed on deactivation. Not a modifier.
Children.Sum / Min / Max / Average / Count(…).Where(…) — LINQ-flavored aggregate channel accessors evaluated over a scoped collection. Valid in dynamic channel formulas and modifier effect bodies. The .Where(...) clause accepts only structural predicates (has_tag, has_template, has_contract). See 08-collections-and-propagation.md § Part 2.
has_tag X / has_template X / has_contract X — Structural predicates usable in propagates_to where clauses and in .Where(...) on collection aggregates. Evaluated at bind time against the entity's static declaration; never re-evaluated per tick. Channel-based predicates are not permitted here (SECS0802). See 08-collections-and-propagation.md § Part 2 and Part 3.
Modifier — A reusable effect bundle (Production *= 110%, field GoldCost *= 80%). Declared globally or as a template member. Carries stacking policy, reapply mode, optional decay, optional tags. Lowers to a ModifierDeclaration struct and can affect either channel resolution or effective template-value reads.
propagates_to … where — Modifier declaration clause that automatically re-applies the modifier to all child entities matching a structural predicate on the collection scope. The predicate must be structural (has_tag, has_template, has_contract) — lambda or channel-based predicates raise SECS0802. See 03-channels-and-modifiers.md § Modifier propagation and 08-collections-and-propagation.md § Part 2.
Modifier binding — The single runtime record created by target.add_modifier ModifierName .... Carries owner (lifetime source), target (effect anchor), modifierId, triggerId, duration ticks (-1 = permanent), captured0 / captured1 entity references for while trigger re-evaluation, and state (Active / Inactive / Removed). Captured entities are predicate references, not lifetime owners.
Scope — A host-defined hierarchy node (e.g., Settlement, Location, Province). Declared in .secs as a schema (fields, ScopedList<T> / ScopedDictionary<TKey,T> collections, walks_to edges, and host-exposed method signatures). Walked via the scope sigil. Never stores data — the host owns the data.
ScopedList<T> / ScopedDictionary<TKey,T> — Scope-bound ordered and keyed entity collections respectively. Declared on scope schemas. ScopedDictionary<TKey,T> supports [key] indexed access; ScopedList<T> does not (raises SECS0822). The canonical key type for template-keyed dictionaries is TemplateId. See 08-collections-and-propagation.md § Part 1.
Scope method — A host-implemented method declared on a scope with typed parameters described by SecsTypeRef. Non-void methods are read-only queries that lower through ISecsHostReads; void methods are command-producing calls that require a command context and lower through ISecsHostCommands. Scope methods are game vocabulary such as BuildingCount, DrainStamina, GainXp, AdvanceLead, or Reveal; they do not introduce new activity syntax.
Callable method identity — Callable user/game methods use normal C# call syntax in .secs, while generated ids are owner-and-signature hashes. Scope methods hash scope:<scope-name>.<Method>(<param-type-refs>):<return-type-ref>. Contract queries and methods hash contract:<contract-name>.<Method>(<param-type-refs>):<return-type-ref>. Callable signatures lower to declaration rows using SecsTypeRef, not SecsValueKind, so the compiler and tools can validate structured return values, C# collection-shaped values, scope/entity parameters, and query-vs-command behavior without inventing parallel type syntax.
Scope frame — The execution frame for a contract/template function. It is seeded from the contract's root_scope: root is the supplied root entity, declared scope sigils walk from that frame, and existing-instance functions additionally bind this / owner. It replaces ad hoc world-parameter signatures such as CanBuild(Location, Settlement, Owner); value/helper parameters such as ulong templateId remain valid.
Contract — A named API surface plus a root_scope. Declared as contract Building { ... }. Every template binds to exactly one contract. Contracts declare typed read-only queries (query bool CanBuild();, query FeaturePlacementResult EvaluatePlacement(Location candidate, FeaturePlacementInput input);), void command-producing methods (method void OnBuilt();), lifecycle bindings (activation OnBuilt;, or an equivalent on_activate spelling if the grammar adopts that form), and optional named context profiles (context BuildCostContext { ... }). Lifecycle slot names (activation, deactivation, future transition slots) are engine vocabulary; method names (OnBuilt, OnEquipped, OnSpawned) are game vocabulary chosen by the developer. Lowers to ContractDeclaration plus ContractMethodDeclaration[] using SecsTypeRef; context profiles lower to ContextProfileDeclaration.
registry_only — Specified future contract qualifier / current implementation gap. The intended contract is metadata-only templates that can be looked up via registry[id] but cannot activate channel sources, run lifecycle bindings, or be created as live entities. End-to-end implementation still needs ContractDeclaration.IsRegistryOnly, registry validation, activation/create rejection, generated output, and tests before this glossary can mark it shipped. See 02-templates.md § Templates under registry_only contracts, 08-collections-and-propagation.md § Part 7, and FUTURE_WORK.md.
Formula — A delegate evaluated at channel-resolution time for dynamic channel sources and dynamic modifier effects. Compiled from the method body of a channel int X { return ...; } or a dynamic modifier effect. Registered on the SecsRegistry under a Formula_* hash.
registry[id] — Ambient read-only template metadata accessor in system Execute and formula bodies. Lowers to context.Registry.GetTemplate(id). Valid when id is a TemplateId and returns nullable metadata for any registered template. Reading metadata does not activate the template; actual entity creation still requires an activatable non-registry_only template. See 08-collections-and-propagation.md § Part 7.
System — A per-tick procedure. Declared system Name { phase = Phases.X; frequency = Cadence.Y; method void Execute() { ... } }. Non-static. The phase = ...; and frequency = ...; lines are slot assignments (system_phase, system_frequency) that mods can target via inject system X { phase = Phases.Y; }. Lowers to a class implementing ITickSystem.
phase / tick_rate (plain C#, not SECS keywords) — Top-level phase and tick-rate values are plain C# static readonly fields, not SECS source-language declarations. Phases live in a static class Phases with public static readonly PhaseDeclaration Growth = PhaseDeclaration.Create("game:phase/growth", SystemPhase.Main, 4);. Tick rates live in a static class Cadence with public static readonly TickRate Daily = TickRate.Days(1);. The system-body slots (phase = X;, frequency = Y;) reference these C# values. See 06-overrides-and-modding.md § 9.1 and § 9.3.
Event — A conditional/pulsed behavior. Declared with metadata plus explicit query/method bodies: event Name { trigger ...; chance ...; query bool Condition() { ... } method void Execute() { ... } method void BuildOptions(EventOptions options) { options.Add("Quarantine", Quarantine); } method void Quarantine() { ... } }. Non-static. Lowers to a SecsEvent class plus an EventEntry registration. Generated method overrides receive an EventContext that carries the target scope, tick context, and event metadata.
On-action — A metadata-only extension point that events subscribe to. Declared on_action on_building_complete { scope ...; provides ...; mode ...; }. Fired from systems, events, or host code via fire on_action ...; effects live in subscriber event methods or at the firing site, not in the on-action declaration.
Activity — A proactive player-, AI-, event-, or host-driven workflow. Declared as a class-based activity RecruitGarrison { actor settlement; query bool CanStart() { ... } method void OnStart(ActivityRun run) { ... } } surface with explicit query/method lifecycle members, not allow / effect blocks. Lowers to a SecsActivity definition plus one ActivityRun per active execution, executed via ActivityExecutor. SecsActivity is the executable behavior surface; policies select among activities. Activities occupy concurrency lanes (default ActivityLanes.Primary) so an actor may run at most one activity per lane simultaneously.
Activity request — A request to start a specific activity for a specific actor against an optional target with optional typed args. Carries ActivityId, Actor (EntityHandle), Target (EntityHandle), Args (ActivityArgsBlob), and Origin (ActivityRequestOrigin). The executor validates the request and materialises an ActivityRun; live player and policy producers stamp truthful origin before entering the executor, and ActivityRunState persists that origin across save/load. See 04-behavior.md § Activity request and candidate.
ActivityRequestOrigin — Enum naming where an ActivityRequest came from. Values: Unknown (legacy / best-effort call site with no declared producer boundary), Player, Policy, System, Event, Mod. It is runtime/save provenance for attribution, replay, and analytics; it is not a required UI projection.
Activity candidate — A bound activity option that has passed candidate-building validation (visibility, target shape, basic eligibility) but has not yet been committed to a run. Carries the underlying ActivityRequest, the candidate's EffectPlan? preview, the policy score, the score's PredictionConfidence, and the candidate's Status (CandidateStatus). Built and scored by PolicyExecutor.
CandidateStatus — Enum naming the lifecycle stage of an ActivityCandidate as it flows through PolicyExecutor. Values: Available (default; built and eligible for scoring), Filtered (failed eligibility, dropped before scoring), Selected (chosen as the winning candidate), Started (executor produced an ActivityRun), Rejected (failed at the executor boundary).
Activity run — An active or completed execution instance of an activity. Identified by a strongly-typed ActivityRunId, carries the request Origin, persisted via ActivityRunState, and stored in the engine's ActivityRunStore while running.
Activity lane — A concurrency domain on an actor. By default an actor has at most one active ActivityRun per ActivityLaneId (see 04-behavior.md § "Activity lanes"). The default lane is ActivityLanes.Primary; the four named lanes CombatEncounter, CombatActivity, Movement, Interaction are scaffolded for partial-concurrency work.
ActivityLanePolicy — Enum declaring an activity's behavior when its lane is already occupied on the actor. Values: Reject (default; StartAt returns null), Queue (RESERVED; throws NotImplementedException), Preempt (RESERVED; throws NotImplementedException). Per the no-silent-fallback tenet, Queue and Preempt surface a clear runtime error rather than silently degrading to Reject behavior.
Activity args — Typed parameters carried by an ActivityRequest and persisted on the ActivityRunState. Each typed-args record (record struct implementing IActivityArgs<TSelf>) declares a stable ulong SchemaId; the activity declaration that consumes those args overrides SecsActivity.ExpectedArgsSchemaId to that schema id. The mod system exposes this as the activity_args_schema slot — one of four architectural slots (activity_actor_scope, activity_target_scope, activity_lane, activity_args_schema) that are closed under inject and require replace to change. In content-family patterns, the args blob carries the chosen definition reference for a generic mechanic activity rather than introducing one activity declaration per content row. See 06-overrides-and-modding.md § 8.10.
Policy — Compile-time AI decision logic for an actor/domain pair. Declared policy Name { actor X; domain Y; need ...; selector ...; rule Decision ...(); }. Lowers to a SecsPolicy subclass with Needs, Selectors, Rules arrays plus an EvaluateRule(ruleId, context) switch. Registered through SecsRegistry.RegisterPolicy. See 04-behavior.md § Policies and 09-ai-policies-and-activities.md.
Need — Utility-AI scoring weight against a channel. Declared need Name { channel = ...; target = ...; curve = ...; threshold = N; weight = N; }. Lowers to NeedDeclaration (NeedId, Name, ChannelId, Target: NeedTargetExpr, NeedCurveKind, Threshold, Weight) inside a policy. The Target field is a discriminated union (Constant / Channel / ScopedStat) — see 09-ai-policies-and-activities.md § Needs for the shape.
Selector — Candidate-source declaration that builds ActivityCandidate lists. Declared selector Name { consider activity X from <source>; }. Lowers to SelectorDeclaration plus source metadata for the candidate pool. At the design level the rule stays generic: content definition data feeds a generic mechanic activity, and typed args carry the chosen definition reference into each candidate. Runtime bridging details for collection-backed selectors belong to 10-host-secs-execution-boundary.md, not standalone .secs vocabulary.
RuleDecision — Discriminated return type for policy rule bodies. Cases: continue, complete, fail, wait, call(activity, target, args), call_best(selector), cancel_child. The no-payload cases are singleton instances to avoid per-evaluation allocation.
NeedCurve — Built-in utility-AI urgency curve enum: Linear, Quad, InverseQuad, Sigmoid. Each curve maps (current, target, threshold) to [0, 1] urgency. See 09-ai-policies-and-activities.md § Needs.
PolicyExecutor — Engine runtime that builds candidates, scores them against needs, and returns the best per call_best. Source: src/SECS.Engine/Policies/PolicyExecutor.cs.
ActivityArgsBlob — Schema-tagged byte payload (ulong SchemaId, ReadOnlyMemory<byte> Payload) carrying typed activity args.
IActivityArgs<TSelf> — Interface implemented by typed-args records to declare a stable SchemaId and round-trip Encode / DecodeFrom.
EffectPlan — A read-only prediction of what an activity would do if it ran for a given actor/target/args. Returned by the query EffectPlan Preview(ActivityContext context) lifecycle member on SecsActivity and consumed by tooltips and the future policy/AI layer. Carries an IReadOnlyList<PredictedEffect> and an aggregate PredictionConfidence. Activities that don't override Preview return EffectPlan.Empty. See 04-behavior.md § Preview.
PredictedEffect — One row of an EffectPlan. Mirrors ModifierEffect (TargetKind + TargetId + IsDynamic/FormulaId) plus a PredictionOp (Add / Multiply / Set / Modify), a long Amount, timing window (DelayTicks / DurationTicks), Probability, and a PredictionConfidence (Exact / Probable / Estimated). Modifier-binding effects translate to PredictedEffect rows mechanically via EffectPlanner.FromModifierDeclaration; activities that mutate state directly (e.g. IncrementScopeField) emit rows by hand in their Preview body. See 03-channels-and-modifiers.md § Modifier effect preview and 04-behavior.md § Preview.
ScoreContribution — One need's contribution to a candidate's policy score. Records (NeedId, NeedName, Urgency, Relevance, Matched, Weight, WeightedContribution) for a single (candidate, need) pair. Built as part of PolicyExecutor.ScoreCandidatesWithBreakdown and consumed by AI debug overlays and tooltip explainers. Source: src/SECS.Engine/Policies/ScoreBreakdown.cs. See 09-ai-policies-and-activities.md § How the scorer picks.
ScoreBreakdown — The aggregate explainability surface for one scored candidate. Carries the ActivityCandidate, the total PolicyScore, the PredictionConfidence the executor would surface, and one ScoreContribution per need (in the policy's needs-list order). The total is bit-identical to what PolicyExecutor.ScoreCandidates would have written; the breakdown does not pay an extra scoring pass. Source: src/SECS.Engine/Policies/ScoreBreakdown.cs.
PreviewDriftEntry — One channel's predicted-vs-actual delta for a completed activity run. Records (ChannelId, PredictedDelta, ActualDelta) so hosts can flag activities whose Preview has drifted from runtime behavior. Source: src/SECS.Engine/Activities/PreviewDriftRecorder.cs.
PreviewDriftRecord — Captured preview-vs-actual telemetry for one successful activity run: (RunId, ActivityId, Actor, Preview, Entries, StartTick, CompleteTick). Emitted by PreviewDriftRecorder.OnRunComplete only on the success path; cancelled, stopped, and failed runs are not recorded. Source: src/SECS.Engine/Activities/PreviewDriftRecorder.cs.
PreviewDriftRecorder — Opt-in telemetry sink wired into ActivityExecutor.PreviewRecorder. When non-null, the executor snapshots preview-targeted channel values at run-start and re-reads them at run-complete, emitting a PreviewDriftRecord per success. Default null (no probe, no overhead) per the no-silent-fallback tenet. Source: src/SECS.Engine/Activities/PreviewDriftRecorder.cs.
Mod operations (inject, replace, try inject, try replace, inject_or_create, replace_or_create) — Six SECS-source-level operation keywords for layered content modding. inject template Farm { field GoldCost = 8; } is a partial slot patch; replace template Farm { ... } is full declaration replacement; try inject / try replace no-op if target is missing; inject_or_create / replace_or_create apply the operation if the target exists, otherwise create. The full source-set-aware merger is compiler-owned Phase 3 work: it walks declared semantic slots (per the slot schema in 06-overrides-and-modding.md § 8), applies operations in load order, then lowers one merged game. The current executable subset is activity/policy startup finalization: generated or hand-written stand-ins register ActivityMod / PolicyMod records and SecsRegistry.FinalizeModRegistration() freezes the merged runtime view. See 06-overrides-and-modding.md § 3 and docs/adr/ad-0001-runtime-mod-finalization-boundary.md.
Mod extension — A mod-level addition of a permitted SECS declaration or plain C# metadata constant such as static readonly TagId. Data-only mods may add data/content declarations that target the exported host API (templates, modifiers, systems, events, activities, policies, channels where the compiler allows the channel shape); host-capable source sets may add host-backed surfaces such as scopes, scope fields, scope methods, walks_to edges, and contracts because they ship corresponding bridge/runtime code. Dynamic formula delegates are added indirectly by declaring dynamic channel sources or dynamic modifier effects; there is no committed top-level formula declaration keyword. Distinct from the inject/replace operation set; requires no operation keyword — the mod simply declares the new identifier and the merger appends it to the relevant declaration array, while tag constants remain ordinary C# inputs to tag binding. Resolved in the compiler-owned pre-binding merge pass, with activity/policy runtime finalization as the shipped startup subset. What mods may not add (closed surfaces — closed enums like SecsScalarType / EffectMode, base contract query/method lists, provides scope:X lists, hand-picked hash sentinels, and data-only host-backed surfaces) is enumerated in 06-overrides-and-modding.md § 8.7-8.8.
Tags (tags = Tag1, Tag2;) — tags = Tag1, Tag2; is a slot-bearing assignment inside a template { } or modifier { } body that registers those tags on the declaration. The slots template_tags and modifier_tags are full-list-replacement mod-operation targets per 06-overrides-and-modding.md § 8.2 / § 8.3. Tag identities themselves are plain C# values: static class Tags { public static readonly TagId Food = TagId.Create("game:tag/food"); }. Top-level tag <Name>; is NOT a SECS keyword; declaring tag identifiers is plain C#. See 06-overrides-and-modding.md § 9.4.
H. / FNV-1a hash* — Every SECS identifier (channel name, template name, scope name, modifier name, trigger name, formula name, on-action name, etc.) is hashed to a ulong via FNV-1a-64 over its canonical id string at compile time, with explicitly documented wire-id strings used only by runtime-owned wire-id families. Emitted into a generated Hashes.cs (the H class) as public const ulong Farm = 0x...UL. Tag identities are the exception: their source of authority is a plain C# TagId.Create("ns:tag/name") constant, and any generated H.Tag_* mirror must equal that TagId.Value. All engine APIs key on these hashes — the source string identifier never leaves the compiler/runtime boundary. The runtime and hand-written generated stand-ins now use the same FNV-1a-64 ulong shape the compiler must emit. See 01-world-shape.md § "Canonical id string format" and § "FNV-1a-64, case-insensitive" for the rationale.
TemplateId / ChannelId — Strong-type record-structs wrapping ulong. TemplateId is the canonical key type in ScopedDictionary<TemplateId, T> and the input to registry[id]. ChannelId is the source-visible identity type for channel references stored as structured metadata, such as skill-sheet FeedsChannels. The compiler lowers them to distinct SecsTypeRef kinds so metadata does not collapse back to bare UInt64. See 01-world-shape.md § Built-in strong types and 08-collections-and-propagation.md § TemplateId.
Tick pipeline — The per-tick order: template activation/lifecycle bindings, systems (phase-sorted), event dispatch, modifier binding update, dirty-sync to host. Declared by the host (or the generated SecsModule). Runs once per TickContext.Tick().
Doc map
| Doc | File | Feature family | Reading prerequisite |
|---|---|---|---|
| 00 | 00-overview.md | Orientation, skeleton, glossary, doc map | None |
| 01 | 01-world-shape.md | scope, contract, top-level channel declarations | 00 |
| 02 | 02-templates.md | template<C> T, fields, intrinsic channels (static/dynamic), contract queries such as CanBuild, contract methods, lifecycle bindings, bare templates | 01 |
| 03 | 03-channels-and-modifiers.md | modifier, trigger lowering, formula lowering, 6-phase target pipeline, reads { }, ValidateDependencies | 02 |
| 04 | 04-behavior.md | system, event (pulse / options), on_action, activity | 03 |
| 05 | 05-expressions.md | scope sigils, resolve / template_field / increment / add_modifier / fire / save_scope_as, foreach, query keywords (any, every, count, random, first) | 02 (templates need some of these); full clarity after 04 |
| 06 | 06-overrides-and-modding.md | Mod operations (inject / replace / try / or_create), typed identity (TemplateId/PhaseId/TagId/...), canonical identity strings, FNV-1a-64 hashing, slot schema, AST-level merger, conflict detection | 01–05 |
| 07 | 07-structured-template-data-and-callables.md | C# source type binding, generated SecsTypeRef metadata, structured template fields, typed query returns, feature placement query contract | 01 |
| 08 | 08-collections-and-propagation.md | ScopedList<T>, ScopedDictionary<TKey,T>, TemplateId, tags = slot, modifier propagates_to where, previous-tick bridge reads, aggregate channel accessors (Children.Sum/Min/Max), system_phase / system_frequency slot model, recipe system (registry_only) | 03 |
| 09 | 09-ai-policies-and-activities.md | policy declarations, utility-AI scoring (needs / selectors / rules), runtime player slots, mod injection examples. The behavior-layer narrative. | 04 |
| 10 | 10-host-secs-execution-boundary.md | Bucket-classifies every SECS concept as host-owned / SECS-owned / shared, and names the bridge contract. | 01-09 |
Reading order
New contributor, cold read: 00 → 01 → 07 → 02 → 03 → 08 → 04 → 05 → 06. The structural docs and type model (01, 07, 02) come before the channel pipeline (03) so the reader has a world and value model to attach channels to. Expressions (05) come after behavior (04) because foreach/query keywords are easier to motivate once systems and events exist. Mod operations (06) come last because they touch every other shape.
Content author wanting to write .secs files: 00 → 01 (know the world shape) → 07 (know structured data and typed query contracts) → 02 (declare a template) → 05 (write the expressions inside it) → 03 (add modifiers) → 08 (collections, propagation, tags) → 04 (add a system or event). 06 is optional until modding matters.
Reader chasing one keyword: skim 00 for terms, jump to the doc from the doc map, and use the feature-block headings to find the rule.
Compiler-planning detail lives in SECS-Compiler-Plan.md. Use it when compiler implementation resumes; for first-pass reading, stay on the live contract path above.
Relation to other documentation
These are supporting references. Treat them as secondary unless a section in this doc set points you there for a specific reason.
-
Historical audits and recovery notes under
docs/— provenance and cleanup history. Useful when you need to understand how the live contract changed, but they are not fallback semantics. -
docs/decisions/anddocs/adr/— ADRs documenting strategic direction, such as the Roslyn-fork decision and the runtime/compiler mod-finalization boundary. Cite these when a lowering choice depends on an architectural decision. The two directories are separate legacy/new ADR namespaces; cite full paths. -
docs/research/— scratch notes, exploration dumps, and prior-art summaries. NON-AUTHORITATIVE. Promote conclusions into an ADR or this design set before treating them as live guidance. -
SECS-Compiler-Plan.md(workspace root) — the compiler implementation plan for when compiler work resumes. Useful for planning, not required for first-pass contract reading. -
examples/valenar/Generated/— the hand-written compiler output for implemented surfaces. This remains the executable lowering contract where a design doc does not explicitly supersede it. When a numbered design doc replaces an older stand-in, the design doc wins. -
examples/valenar/Content/— the SECS source examples that feed the "SECS" blocks in implemented-surface docs. When a non-superseding lowering block does not match Generated/, treat that mismatch as a contract problem to resolve. -
.claude/rules/secs-concepts.md— compact working-memory summary of the seven concepts and the resolution pipeline. Useful for quick lookup. -
FUTURE_WORK.md(in this directory) — backlog and deferred items. Useful for planning, not part of the live contract path.
Diagnostic code convention
SECS compile diagnostics use four-digit codes prefixed SECS. The first two digits match the doc number where the rule is specified — 01XX lives in 01-world-shape.md, 02XX in 02-templates.md, 03XX in 03-channels-and-modifiers.md, and so on. A reader encountering SECS0201 at the compiler output knows to open doc 02 for the full rule, rationale, and examples.
Active diagnostics as of 2026-05-12. The "Fires across source sets?" column states whether the check runs only inside one source set (per-set), only against the base/expansion host-capable layer, or across the merged base ∪ mods union. Phase 3 merger semantics require every diagnostic to be re-qualified for the mod case; this column is the canonical answer.
| Code | Severity | Rule | Home doc | Fires across source sets? |
|---|---|---|---|---|
SECS0030 | error | removed legacy action keyword; use activity | doc 00 / ADR-0002 | every set |
SECS0031 | error | removed legacy program keyword; use policy | doc 00 / ADR-0002 | every set |
SECS0032 | error | source keyword whitelist rejects uncommitted SECS-looking forms such as top-level formula, old override, top-level tag, top-level phase / tick_rate, or arbitrary direct fire EventName while preserving ordinary C# metadata collection | doc 00 / compiler plan | every set |
SECS0101 | error | kind = clause missing on channel declaration | doc 01 | every set (base and each mod) — fires on the offending declaration regardless of which source set authored it |
SECS0105 | error | kind = Base without source = | doc 01 | every set |
SECS0106 | error | kind = Accumulative without source = | doc 01 | every set |
SECS0107 | error | hash collision between distinct canonical identities (<namespace>:<kind>/<name>) under FNV-1a-64 — see 06-overrides-and-modding.md § 4.2 | doc 01 | base ∪ mods union — fires identically whether the colliders are in base, in two mods, or across base + mod |
SECS0108 | warning | cross-assembly redeclaration of an identifier without an inject / replace / try / or_create mod operation | doc 01 | base ∪ mods union — fires when a mod redeclares a base name, when two mods redeclare the same name, or when any source set redeclares its own name twice |
SECS0110 | error | clamp literal type does not match channel type | doc 01 | every set |
SECS0111 | error | scope walk traverses a walks_to edge not declared on the source scope | doc 01 | effective base/expansion scope graph — third-party mods consume exported edges but do not contribute host topology |
SECS0201 | error | template-body channel declaration on a scope that differs from the template's root_scope | doc 02 | every set |
SECS0301 | error | modifier effect on bool channel must use HardOverride (=); Additive (+=) and Multiplicative (*=) are not defined for bool | doc 03 | every set |
SECS0302 | error | formula uses dynamic resolve() without a reads {} block | doc 03 | every set |
SECS0303 | warning | reads {} declares a channel no resolve() call references | doc 03 | every set |
SECS0601 | error | merger hash-collision between distinct canonical identities (FNV-1a-64 of <namespace>:<kind>/<name>) | doc 06 | base ∪ mods union (merger-only — does not fire pre-merge) |
SECS0602 | error | duplicate declaration without an inject / replace / try / or_create operation when identifier already exists | doc 06 | base ∪ mods union (merger-only) |
SECS0102 | error | binder rejects an unsupported channel type (only int / long / float / double / bool are valid) | doc 01 | every set |
SECS0204 | error | template name is empty after name = substitution | doc 02 | every set |
SECS0209 | error | template query or void method not declared on the contract's query/method lists | doc 02 | every set |
SECS0210 | warning (reserved) | explicit lifecycle pair overridden asymmetrically; not emitted for ordinary modifier attachments because owner/target cleanup handles lifetime | doc 06 | base ∪ mods union (merger-only) |
SECS0212 | error | undeclared tag referenced in tags = ... or a structural predicate such as has_tag ... | doc 02 | every set |
SECS0213 | error (specified/future) | registry_only contract declares queries, methods, or lifecycle bindings; reserved until registry_only is implemented end-to-end | doc 02 | every set |
SECS0402 | error | event triggers an on_action that no on_action declaration matches in the merged registry | doc 04 | base ∪ mods union (merger-only) |
SECS0403 | error | mod attempts to extend a base on-action's provides list (closed surface) | doc 04 | base ∪ mods union (merger-only) |
SECS0501 | error | expression uses an undeclared scope sigil (unknownscope) | doc 05 | every set, checked against the exported base/expansion scope set |
SECS0603 | error | inject / replace (without try) targets a name that does not exist in the merged registry of base + earlier mods | doc 06 | base ∪ mods union (merger-only) |
SECS0604 | error | mod operation kind not valid for the declaration kind (e.g. inject scope X { ... } for a host-backed scope under a data-only mod) | doc 06 | base ∪ mods union (merger-only) |
SECS0605 | error | unknown slot — inject body addresses a slot identifier not in the slot schema for the parent declaration | doc 06 | base ∪ mods union (merger-only) |
SECS0606 | error | operation attempts to replace a scope's walks_to edge set; walks_to edges are additive, not replaceable | doc 06 | effective base/expansion layer plus mod diagnostics (merger-only) |
SECS0607 | error | walks_to references an undeclared scope (post-merge) | doc 06 | effective base/expansion layer (merger-only) |
SECS0608 | error | walks_to references a scope from a non-load-order-ancestor host-capable source set (ambiguous expansion ordering) | doc 06 | effective base/expansion layer (merger-only) |
SECS0609 | error | third-party data-only mod declares a host-backed scope surface (scope, scope field, collection, scope method, or walks_to) | doc 06 | third-party mod source set against the exported base/expansion SDK |
SECS0610 | error | ambiguous short identity reference in a mod operation | doc 06 | base ∪ mods union (merger-only) |
SECS0611 | error | replace declaration is missing a required slot | doc 06 | base ∪ mods union (merger-only); runtime-shipped for activity/policy finalization |
SECS0612 | error | duplicate slot in the same declaration or inject body | doc 06 | base ∪ mods union (merger-only) |
SECS0613 | error | phase / frequency refers to a value not registered in the scheduler / catalog | doc 06 | base ∪ mods union (merger-only) |
SECS0614 | error | targeted child-row operation matched zero targetable rows | doc 06 | base ∪ mods union (merger-only) |
SECS0615 | error | targeted child-row operation matched multiple targetable rows | doc 06 | base ∪ mods union (merger-only) |
SECS0616 | error | child-row selector is not activation-constant or not bindable | doc 06 | base ∪ mods union (merger-only) |
SECS0701 | error | unknown type name in a field, callable signature, struct/record field, array element, collection generic argument, or template/scope reference | doc 07 | every set, checked against the merged base/expansion type catalog plus permitted mod-added types |
SECS0702 | error | value initializer does not match the declared SecsTypeRef | doc 07 | every set |
SECS0703 | error | method declaration has a non-void return type; use query for read-only typed returns | doc 07 | every set |
SECS0704 | error | query body performs a command-producing operation | doc 07 | every set |
SECS0705 | error | callable body replacement changes parameter or return SecsTypeRef; body replacements may replace implementation only | doc 07 | base ∪ mods union |
SECS0706 | error | runtime template-field modifier attempts additive/multiplicative effect on a non-numeric structured field | doc 07 | every set |
SECS0800 | warning | two or more activity/policy mod writes target the same slot; later load order wins | doc 06 | activity/policy startup finalization via SecsRegistry.FinalizeModRegistration() |
SECS0801 | error | inject targets a replace-only architectural slot (activity_actor_scope, activity_target_scope, activity_lane, activity_args_schema, policy_actor_scope, or policy_domain) | doc 06 | activity/policy startup finalization via SecsRegistry.FinalizeModRegistration() |
SECS0802 | error | lambda or channel-based predicate in propagates_to where clause (must be structural: has_tag, has_template, or has_contract) | doc 08 | every set |
SECS0803 | error | command-producing call appears in a read-only body such as event Condition, activity IsVisible, or policy rule decision evaluation | doc 10 | every set — compiler/analyzer reservation |
SECS0804 | error | generated code reaches ISecsHostCommands without a command-producing context | doc 10 | every set — compiler/analyzer reservation |
SECS0810 | error | saved ActivityRunState.ActivityId not present in current registry | doc 04 § Activity run save/load migration | every set — surfaced at runtime by ActivityRunStore.Restore |
SECS0811 | error | saved ActivityArgsBlob.SchemaId does not match the activity's ExpectedArgsSchemaId | doc 04 § Activity run save/load migration | every set — runtime |
SECS0812 | error | saved ActivityRunState.Lane is the zero / None lane id | doc 04 § Activity run save/load migration | every set — runtime |
SECS0820 | error | saved SlotKindId is the zero kind id, or ScopeSlotStore.Restore was called on a non-empty store | doc 04 § Activity run save/load migration | every set — runtime |
SECS0821 | error | saved slot element does not match the snapshot's declared ElementType | doc 04 § Activity run save/load migration | every set — runtime |
SECS0822 | error | [] indexed access on ScopedList<T>; indexed access is only valid on ScopedDictionary<TKey,T> | doc 08 | every set |
SECS0830 | error | prev-tick bridge/API read such as ReadPrevTick*(entity, H.Channel) targets a channel that does not declare track_prev = true | doc 08 | every set |
SECS0840 | error | ambiguous lifecycle hook name produced by collection field naming; rename the field or the hook | doc 08 | every set |
SECS0410 | warning | activity references an unknown tag id (no RegisterTag call covers it) | doc 04 | base ∪ mods union — registry-wide warn-only check via SecsRegistry.Validate() |
SECS0411 | warning | activity declares a negative DurationTicks; runs cannot have negative duration | doc 04 | every set |
SECS0412 | warning | activity references an actor scope id with no registered ScopeDeclaration (gap reachable through mod patches that bypass RegisterActivity validation) | doc 04 | base ∪ mods union |
SECS0413 | warning | activity references a target scope id with no registered ScopeDeclaration | doc 04 | base ∪ mods union |
SECS0414 | warning | activity cost amount is non-positive after merge (the activity will never be affordable) | doc 04 | base ∪ mods union |
SECS0415 | warning | policy-visible activity inherits the default empty Preview() implementation | doc 04 | base ∪ mods union |
SECS0910 | warning | policy need references an unknown channel id | doc 09 | every set |
SECS0911 | warning | policy selector references an activity id that no activity declaration matches | doc 09 | every set |
SECS0912 | warning | two policy needs target the same channel with opposite-sign weights (the contributions cancel at scoring time) | doc 09 | every set |
SECS0913 | warning | policy selector FromTags references a tag id with no registered tag name | doc 09 | every set |
SECS0914 | warning | policy actor scope id has no registered ScopeDeclaration | doc 09 | every set |
Each active diagnostic is specified in full at its home doc's Diagnostics subsection. When adding a new diagnostic, pick the lowest free code within your doc's XX00-XX99 block, document it inline alongside the rule it enforces, and re-qualify it for cross-source-set behaviour using the same column above.
For the consolidated allocation table — every code reserved across phases, with its current shipping status (runtime enum vs future-compiler reservation) — see SECS-Compiler-Plan.md § Diagnostic Code Catalog.
Diagnostic channels
The compiler will surface diagnostics as part of its lowering pass. The runtime engine ships two complementary channels for the registration-time subset:
- Mod diagnostics — emitted by
SecsRegistry.FinalizeModRegistration()viaModFinalizeResult.Diagnostics. Errors throw at finalize time; warnings (e.g.SECS0800slot conflicts) accumulate silently. See06-overrides-and-modding.md § 12. - Registry static analysis — emitted by
SecsRegistry.Validate()viaRegistryDiagnostics.Diagnostics. This warn-only surface walks every registered activity, policy, and formula and reports suspicious patterns without ever throwing. Hosts call it after registration as a cheap "did I wire everything up?" check; the future compiler will preempt most of these at parse time.
Both channels share the Severity enum (Error, Warning, Info, Hint) defined in SECS.Engine.Diagnostics. The convention IsError predicate on each diagnostic is a derived view over Severity == Error — the canonical comparison is against the enum.
Compiler-output ordering convention
Every declaration array the compiler emits — Types[], Channels[], Scopes[], ScopeFields[], Contracts[], Modifiers[], the H.* constants in Hashes.cs, the Systems[] / Events[] / OnActions[] / Activities[] registration arrays in SecsModule.cs — follows a single three-level ordering rule. One rule, every table. The rule produces deterministic, diff-reviewable output across filesystems, compiler runs, and mod load orders.
Within a source file
Declarations emit in the order the author wrote them. If channels.secs declares Gold, then Food, then Wood, the emitted Channels[] array contains those three rows at consecutive indices in exactly that order. Rationale: this matches every other C# ordering convention the reader already expects — partial-class member order, enum value order, array-literal order. Source order is the authorial contract; a reordering pass would make the output surprising relative to the input.
Within a single source set
When a source set (base game, or one mod) spans multiple .secs files, the compiler processes those files in alphabetical path order under the set's root. Content/buildings/contracts.secs precedes Content/buildings/scopes.secs, and Content/buildings/scopes.secs precedes Content/buildings/channels.secs. Rationale: filesystem iteration order is not deterministic across platforms (readdir on Linux returns inode order; Windows FindFirstFile returns NTFS MFT order). Sorting by path string at the compiler input yields identical output on every machine, which is the foundation every other guarantee builds on.
Across source sets — base + mods
The compiler receives the base game's source set plus zero or more mod source sets with an explicit load order. Each set is internally ordered by the two rules above; across sets:
- Base game enters as load order 0. Its declarations populate positions
0..N_base - 1in the emitted array. - Each mod enters in ascending load order. For every declaration the mod carries:
- An
inject/replace/try/or_createoperation writes the targeted semantic slot or declaration in place. The array index does not change. The hash does not change. The name does not change. Only the merged slot payload updates. - A normal new declaration appends at the current end of the array, in the mod's own within-set order (rules 1 and 2 above applied to that mod's sources).
- An
Consequences follow directly from these rules:
- Toggle-friendly. Enabling or disabling a mod affects only (a) the slots it overrode — which revert to the previous winner in the remaining load order — and (b) the appended block it contributed. Base positions are untouched. A
Generated/diff for one mod toggle is bounded by that mod's footprint, not by a wholesale reshuffle. - Reorder-friendly. Reordering mods leaves base positions stable. Each mod's appended block reorders per the new load order. For every overridden slot, the winner becomes "the last mod in the new load order that targets it"; positions remain fixed.
Tables this rule covers
One rule applies uniformly to every declaration array, hash table, and generated registration manifest the compiler emits:
Declarations.Channels[]Declarations.Types[]Declarations.Scopes[]Declarations.ScopeFields[]Declarations.Contracts[]Declarations.Modifiers[]- Collected tag catalog registration via
SecsModule.RegisterTag(...), plus any generatedH.Tag_*convenience mirrors - The
public const ulong X = 0x…ULentries inHashes.cs SecsModule.Systems[]registration orderSecsModule.Events[]registration orderSecsModule.OnActions[]registration orderSecsModule.Activities[]registration order
Nested slots (template fields, template modifiers, template methods, modifier effect bundles, modifier metadata fields) follow the same rule inside their parent — within-template source order at the base; mod operations replace slots in place; mod-new nested members append at the template's current end.
Why Position Preservation Matters
Position preservation is what makes Generated/ diff-reviewable under mod load. A mod operation that changes one slot produces one changed row or initializer in generated output — the payload at that index updates; every other row is byte-identical. Without position preservation, a single slot write would reshuffle every downstream index and produce a diff the size of the array. Code review, git blame, and mod-compat tooling all key on line-level stability, and a Generated/ tree that churns under every mod operation defeats all three. The tenet is: mod diffs should be proportional to mod changes, not to mod position in the load order.
Status flags used in docs 01–08
- NOT YET IMPLEMENTED — Feature is specified and has a lowering shape, but the compiler or engine runtime does not yet support it end to end. Used today for: the generic source-set-aware Phase 3 merger for declaration kinds beyond the shipped activity/policy finalizer, full source-level conflict-report artifacts, and any feature-specific lowering still called out by the numbered docs. Activity/policy startup finalization is not in this bucket: it is implemented by
RegisterActivityMod/RegisterPolicyModplusFinalizeModRegistration(). - DEFERRED — Explicitly out of scope for the initial compiler. E.g., dependency-aware channel caching,
Singletonconstraint on templates, inline modifier declarations in method bodies. These should not appear in compiled output. SeeFUTURE_WORK.mdfor the maintained backlog of deferred surfaces.
These flags describe lowering-contract state — whether the compiler should emit the specified C# and whether the engine yet executes it correctly. They do not describe mod-lifecycle states. Mod lifecycle (compile-time merger, hot-reload re-merge, sideload, save-game migration) is its own axis tracked under 06-overrides-and-modding.md and the relevant ADRs; if hot-reload becomes a scoped feature, it lives in a new mod-lifecycle category, not as a value of the lowering-contract status flag set.