SECS Behavior Vocabulary
Live SECS source, generated C#, engine code, and design docs use a fixed behavior vocabulary. The rules are normative.
Canon
activity— runnable behavior with body, args, lifecycle, slot binding, optionalon_actionmetadata id.policy— chooser that picks among activities.- Use
activityas the canonical Valenar behavior / execution / planning noun. - Do not use
ActionorProgramas live Valenar behavior or planning categories. - Use more specific terms where needed:
Atomic Activity,Composite Activity,Routine Activity,Mission,Assignment, andPolicy. - Do not preserve
Action/Programcompatibility language or redirect stubs in Valenar docs; git history is the legacy record. - Do not confuse deprecated
Actionvocabulary with SECSon_action. The SECS termson_action,OnActionId,OnActionDeclaration,FireOnAction, and on-action event subscription remain valid metadata / extension-point vocabulary and must not be renamed unless a future SECS decision explicitly changes them. - When docs, Content, Generated stand-ins, runtime code, or tests disagree on this vocabulary, mark the drift and update the affected artifact classes. Do not silently reconcile by keeping both terms.
Removed (do not introduce, do not perpetuate)
actionas a SECS keyword or behavior noun. The standalone tokenaction, plusSecsAction,ActionRun,ActionId,ActionContext,GetAiWeight,AiTickFrequency, andaction_*snake-case identifiers are banned in live source/docs.programas a SECS keyword or behavior noun.SecsProgram,ProgramRun,ProgramIdare banned.Activity_<ContentName>content-explosion identifiers (e.g.,Activity_FrostBolt,Activity_IronSword,Activity_HealingPotion,Activity_Swordsmanship) — any activity name that encodes a specific content row rather than a generic mechanic. The fix is to express the content as a data template and let a generic-mechanic activity dispatch via typed args. See "Generic-mechanic vs content-row" below.candidate_builderas a.secskeyword, clause, or reserved live source term.with builderandfrom collection X with builder Yas accepted or reserved live.secssyntax.- Runtime or lowering helper names such as
CandidateBuilderId,CandidateBuilderDelegate, orSelectorSource.FromCollection(...)promoted into.secssyntax or language docs.
Allowlisted (these tokens may appear; the CI guard tolerates them)
ActionSpeed— Valenar in-game channel (declared inexamples/valenar/Generated/Declarations.csas aChannelDeclaration, used inexamples/valenar/Content/characters/channels/movement.secs); the player-facing label for this and its sibling channels is "Stats". Same kind of name asMoveSpeed— has nothing to do with the legacyactionkeyword.- C#
Action<...>delegates — the BCL delegate type, used throughoutActivityExecutor,EventDispatcher,SecsEvent, host read models. Language primitive, not SECS vocabulary. inject_or_create/replace_or_create— committed mod operation keywords (docs/design/06-overrides-and-modding.md § 3).on_actionkeyword,OnActionIdtyped id,OnActionDeclaration,FireOnAction, and theOnAction*C# type family (OnActionDeclarations, etc.) — metadata id system retained per ADR-0002. These are labels on activities and valid event subscription / extension-point surfaces, not separate behavior kinds.- Plain English
actionable,actioned,actionably— natural-language usage in prose is fine.
When you would have written action or program
| You would have written... | Write instead |
|---|---|
action BuildRoad { ... } | activity BuildRoad { ... } |
program ChooseFoo { ... } | policy ChooseFoo { ... } |
SecsAction, ActionRun(...) | SecsActivity, ActivityRun(...) |
ActionId typed id | ActivityId (plus OnActionId if attaching gameplay-event metadata) |
ActionContext | ActivityRunContext |
| "the unit picks an action" | "the unit picks an activity" |
| "the AI runs a program" | "the policy selects an activity" |
Generic-mechanic vs content-row
Activity_<MechanicName> (one generic activity per content family, parameterized by typed args) is GOOD. Activity_<ContentName> (one activity per spell/recipe/item) is the BANNED content-explosion smell.
The seven canonical generic-mechanic activity names are allowlisted by the guard:
Activity_BuildStructure · Activity_UseItem · Activity_CraftRecipe · Activity_CastSpell · Activity_TrainSkill · Activity_PerformWorkOrder · Activity_PerformRitual
Each carries typed args (e.g. CastSpellArgs(TemplateId) for spells) and dispatches the content shape from a <X>Definition template (e.g. SpellDefinition).
Keep mechanic families generic and game-agnostic in shared docs and engine surfaces. Concrete game rows stay data definitions, not promoted syntax.
Adding an eighth generic mechanic family requires extending GROUP_D_ALLOWLIST_REGEX in scripts/check-behavior-vocabulary.sh, adding the row to docs/design/behavior-vocabulary.md § "Group D", and re-baselining. See that doc for the full procedure.
One-off activities like Activity_RecruitGarrison, Activity_ScoutNearbyLead, Activity_RestAtCamp are NOT the smell — they are structurally one-of-a-kind, not parameterized variants of a family. They live within the project's per-baseline floor.
CI guard
scripts/check-behavior-vocabulary.sh enforces these rules:
--check(default) — fails if hit count exceeds the recorded baseline.--baseline— records the current hit count toscripts/.behavior-vocabulary-baseline.
The target end state is 0 hits. Until the repository reaches that floor, each cleanup wave should lower the recorded baseline rather than letting it drift upward.
Sources of truth
docs/decisions/ADR-0002-behavior-vocabulary.md— the decision and rationale.docs/design/behavior-vocabulary.md— the full normative rules and allowlist tables.scripts/check-behavior-vocabulary.sh— the CI enforcer.
If the script and the docs disagree, the docs win and the script is wrong.
If this rule and docs/design/behavior-vocabulary.md drift, the design doc wins and this rule is stale.