Skip to main content

Generated Code Authority

Valenar compiler-owned schema output lives under examples/valenar-v2/Generated/obj/**/SecsGenerated; Endgate's generated project uses examples/endgate/Generated/ for the same authority model. Their project builds invoke secsc over the paired Content/ tree and compile generated C# from obj/**/SecsGenerated.

Tracked source files under those Generated/ directories are retained checked-in non-schema surfaces only. They may reference compiler-emitted IDs, expose explicit host runtime metadata, or carry temporary retained debt pending an ABI closure, but they must not construct schema row families, emit Hashes, or replace *.Generated.SecsModule.BuildSchema(). Do not hand-edit emitted files or reintroduce checked-in schema authority.

Why both Content/ and Generated/ exist

  • examples/valenar-v2/Content/*.secs — the compiler-consumed source of truth for Valenar schema-owned rows.
  • Valenar.V2.Generated.csproj in the Valenar generated project — invokes secsc build --generated-namespace Valenar.V2.Generated and compiles emitted C# from examples/valenar-v2/Generated/obj/**/SecsGenerated.
  • examples/valenar-v2/Generated/ValenarHostIds.cs — the only retained checked-in Valenar .cs file. Today it contains only FailureReasons.StaminaExhausted as a host runtime failure-reason ID; it is true host-owned and not schema/catalog authority. Scoped collection IDs are compiler-owned schema rows emitted from Content/. Valenar catalog/config/helper outputs such as ActivityDefinitions.g.cs, ResourceDefinitions.g.cs, RecipeDefinitions.g.cs, ItemDefinitions.g.cs, SkillProfiles.g.cs, FeatureDefinitions.g.cs, TravelEncounterDefinitions.g.cs, and ValenarConfigDefaults.g.cs are compiler-emitted from metadata in Content/.
  • examples/endgate/Content/*.secs — the compiler-consumed source of truth for Endgate schema-owned rows.
  • examples/endgate/Generated/Endgate.Generated.csproj — invokes secsc build --generated-namespace Endgate.Generated and compiles emitted C# from obj/**/SecsGenerated.
  • examples/endgate/Generated/ItemDefinitions.cs and WaveScripts.cs — retained non-schema catalog helpers. They are not fallback schema authority.

When you add or change schema-owned game content, edit the .secs source or the compiler, then build the generated project. Do not patch emitted schema rows by hand. When you add or change a retained checked-in surface, keep its authority explicit: true host-owned metadata stays narrowly scoped, temporary retained debt must say that it is debt, and nothing there may become hidden schema or catalog authority.

What "matches the compiler" looks like

The emitted patterns in obj/**/SecsGenerated are not arbitrary — they are the contract the compiler must satisfy. For Valenar and Endgate schema-owned rows, change Content/ or the compiler, then regenerate by building the generated project.

  • Templates lower to compiler-emitted TemplateRow entries. Template rows carry the template id, contract id, and dense range metadata; behavior is reached through contract/activity host handlers rather than v1 TemplateEntry self-registration.
  • Channel declarations lower to compiler-emitted ChannelRow entries with id, scalar type, mode, source field id, default/clamp metadata, contract id, and track_prev.
  • Modifier declarations lower to compiler-emitted ModifierRow plus ModifierEffectRow dense ranges. Effects carry channel id, phase, operation, static delta, or formula id.
  • Hashes live in compiler-emitted Hashes.g.cs as typed-id static fields created from canonical id strings (which hash through FNV-1a-64). Every named entity (template id, channel id, modifier id, system id, event id, activity id, policy id, need id, selector id, rule id, scope id, contract id) gets a stable typed id. The canonical id format the compiler emits is owner-less colon-slash with the declared name reproduced verbatim: <namespace>:<kind>/<Name> (see docs/design/01-world-shape.md § "Canonical id string format", docs/design/06-overrides-and-modding.md § 4.2, and docs/decisions/ADR-0003-canonical-id-grammar.md). Sub-ids are emitted as dotted-suffix forms (valenar:scope/Building.scope-field:JobsFilled, valenar:template/MainCharacter.channel-modifier:Attack) or kind-prefixed forms (contract-method:valenar:contract/Building.OnBuilt():void, event-option:valenar:event/RoadsideEncounter.OptionInvestigate). Type ids are namespace-less (type/FeatureDiscoveryState). The on_action name is the one segment emitted in snake_case (valenar:on_action/endurance_drill_completed). Owner-segmented snake_case top-level forms (valenar:channel/settlement/x), bare-name, and dot-form (valenar.channel.x) hashes are not live identity targets; the owner-segmented form was never implemented (ADR-0003). Hash collisions are caught at registration.
    • Dual-use commodity split. When a name is used as BOTH a Settlement scalar channel AND a Resource template id (Wood, Stone, Gold), the compiler emits the two as distinct constants from distinct canonical strings: H.Channel<Name> from valenar:channel/<Name> and H.Template<Name> from valenar:template/<Name> (for example H.ChannelWood from valenar:channel/Wood and H.TemplateWood from valenar:template/Wood). The constant name carries the kind prefix; there is no _Channel / _Template suffix and no owner segment. Every callsite picks the role explicitly through the kind-prefixed constant. Channel-only commodities (Food, Metal, Population) emit only the H.Channel<Name> constant (e.g. H.ChannelFood from valenar:channel/Food).
    • Mod-local rule / need / selector ids. Rules and other policy-substructure ids defined inside a mod live in the compiler-emitted mod file under obj/**/SecsGenerated/Mods/<Name>Mod.cs (not in Hashes.g.cs), use the mod's source-set namespace, and are emitted as kind-prefixed sub-ids that name the owning policy in the owner segment: rule:<source_set>:policy/<PolicyName>.<RuleName> (e.g. rule:cautious_survival:policy/CharacterSurvival.FleeOnLowHp). Collisions between mods are impossible by construction.
  • Scopes and contracts lower to compiler-emitted schema rows plus dense field/walk/method ranges.
  • Systems lower to compiler-emitted SystemRow declarations plus compiler-emitted Systems.Execute* delegate bodies; RuntimeModule.g.cs registers each Systems.Execute* handler via SecsRuntimeBuilder.WithSystemHandler keyed by system id. Systems whose emitted Execute* body is an empty stub get their live behavior from host-registered handlers (examples/valenar-v2/Host/*HostLogic.cs) that the host wires with WithSystemHandler in place of the stub.
  • Events lower to compiler-emitted EventRow plus EventOptionRow dense ranges.
  • Activities, on_actions, and policies lower to compiler-emitted row families and sidecar metadata where authored.
  • The registration entry point is compiler-emitted SecsModule.g.cs. It builds one deterministic WorldSchema for the host/runtime. New schema row families must be wired into BuildSchema() and into the second-pass With* copy if the row family is split across first/second pass arrays.

Hard rules

  • Do not invent patterns. If a new construct does not exist in emitted generated C# yet, look at the corresponding .secs syntax in Content/, look at the spec in docs/, and implement the compiler lowering that matches the committed runtime/schema semantics.
  • Do not bypass SecsModule. Every generated schema row goes through BuildSchema(). The host should not construct generated row arrays behind a parallel facade.
  • Do not use reflection in Generated/. The compiler emits static, AOT-friendly code. Reflection at startup would break Unity IL2CPP and would not match what the compiler can do.
  • Field hashes use FNV-1a-64 over the canonical id string. Computed identically to secs-roslyn's hash function. The compiler emits top-level canonical id strings as <namespace>:<kind>/<Name> colon-slash with the declared name reproduced verbatim (no owner/category segment), and scope-field ids as the dotted-suffix sub-id form <scope-id>.scope-field:<FieldName>. Compiler-emitted Hashes names are the public symbol authority; do not preserve old names through aliases, wrapper constants, or duplicate fields.
  • RegisterTemplateIdentifier registers ONLY the canonical string (Wave 5b). The dual-alias bridge that registered both the canonical id and the bare C# name (nameof(BareLocation) = "BareLocation") was collapsed; every WorldData JSON file, MapGenerationSystem fallback, test fixture, and TryGetTemplate consumer must quote the canonical id directly. The TemplatesByName dictionary on SecsModule is a SEPARATE designer-facing display-name lookup ("Great Hall" → TemplateId) and remains in place — do NOT collapse it; do NOT register its keys via RegisterTemplateIdentifier.
  • Scope-field hashes are per-scope, emitted from the dotted-suffix canonical string. A field name reused across scopes (OwnerId, MapCellId, LastCapitalChangedDay) is emitted as a distinct constant per owning scope from a distinct canonical string: valenar:scope/Territory.scope-field:OwnerId, valenar:scope/Camp.scope-field:OwnerId, and valenar:scope/Skill.scope-field:OwnerId each hash to their own value and emit their own H.ScopeField<Scope><Field>_H... constant. The engine still distinguishes by (ScopeId, FieldId); the FieldId differs per scope because the canonical string differs. Do not collapse these back to a single shared field-name constant — that disagrees with the compiler-emitted hash table.

When deleting generated content

If you delete schema-owned content, delete the .secs source or compiler support and rebuild the generated project. Remove any host/test references to the compiler-emitted IDs that disappear. Do not restore deleted Hashes, SecsModule, row-builder files, aliases, fallback builders, or override tables.

Retained host/catalog marker

When a tracked Generated/*.cs file is retained because it is a checked-in non-schema surface rather than compiler-emitted schema output, it must carry this exact header comment if it has no .secs source citation:

// Retained host/catalog surface: no schema rows.

This is the canonical text that tests/SECS.Standalone.Tests/Guards/V2GeneratedProvenanceGuardTests.cs recognises for retained Valenar checked-in non-schema files. Do not invent a paraphrase. The legal retained-file header forms are:

  1. // Source: <relative-path-to-.secs-file> — real committed source exists.
  2. // Retained host/catalog surface: no schema rows. — explicit retained non-schema surface with no schema row construction.

The scripts/check-secs-source-contract.sh CI guard enforces three additional patterns beyond provenance: bare phase/frequency slot syntax in .secs files, bare TemplateId strings in .secs files, and the absence of existing override/fire/prev_tick violations. See that script for the authoritative regex patterns.

Banned source-form syntax

The following tokens must not appear in .secs files under examples/valenar-v2/Content/, or in any Generated // Source: comment pointing to a hypothetical .secs file:

Banned formWhy bannedReference
candidate_builder Name { ... }Invented keyword not in committed listdocs/WAVE_REFACTOR_AUDIT.md § 1; docs/decisions/ADR-0002-behavior-vocabulary.md
slot X.Y of Type; inside candidate_builderSub-clause of an invented keywordsame
activity Y; inside candidate_builder bodyBody clause of an invented keywordsame
method ActivityRequest[] Build(...) inside candidate_builderBody method of an invented keywordsame
from collection X with builder YInvented selector source clausesame
with builder X as a selector source clauseSub-form of abovesame

The canonical runtime/compiler-lowering shapes that remain are CandidateBuilderId, CandidateBuilderDelegate, SelectorSource.FromCollection(...) — these are C# types, not .secs source syntax. Plain C# with these types is allowed in retained checked-in non-schema files and engine code.

The consider activity X from all_registered selector clause is COMMITTED source syntax and is not banned.

If you would have written candidate_builder in a .secs file, there is no committed source equivalent. Express the selection logic as an explicit retained checked-in non-schema surface or registered host delegate, and leave the source syntax for a future ADR.

Fabricated source files

Never create a .secs file to satisfy tests/SECS.Standalone.Tests/Guards/V2GeneratedProvenanceGuardTests.cs or any other provenance check. If a retained host/catalog file has no committed source citation, use the retained host/catalog marker. Creating a fabricated .secs file with invented syntax as a workaround is a governance violation that will be caught by the source-contract guard and the generated-source provenance tests, and must be reverted.

Source hygiene in Content/

Live .secs files under examples/valenar-v2/Content/ are subject to a separate source-hygiene rule (.claude/rules/secs-source-hygiene.md). That rule governs what comments must not appear in .secs source — wave labels, prototype status, temporary-output markers, old keyword compatibility notes, future compiler commentary, and deferral notes. This generated-codegen rule governs what headers and provenance markers must appear in Generated/; the source-hygiene rule governs what must not appear in Content/. The two are complementary.