Skip to main content

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, optional on_action metadata id.
  • policy — chooser that picks among activities.
  • Use activity as the canonical Valenar behavior / execution / planning noun.
  • Do not use Action or Program as live Valenar behavior or planning categories.
  • Use more specific terms where needed: Atomic Activity, Composite Activity, Routine Activity, Mission, Assignment, and Policy.
  • Do not preserve Action / Program compatibility language or redirect stubs in Valenar docs; git history is the legacy record.
  • Do not confuse deprecated Action vocabulary with SECS on_action. The SECS terms on_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)

  • action as a SECS keyword or behavior noun. The standalone token action, plus SecsAction, ActionRun, ActionId, ActionContext, GetAiWeight, AiTickFrequency, and action_* snake-case identifiers are banned in live source/docs.
  • program as a SECS keyword or behavior noun. SecsProgram, ProgramRun, ProgramId are 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_builder as a .secs keyword, clause, or reserved live source term.
  • with builder and from collection X with builder Y as accepted or reserved live .secs syntax.
  • Runtime or lowering helper names such as CandidateBuilderId, CandidateBuilderDelegate, or SelectorSource.FromCollection(...) promoted into .secs syntax or language docs.

Allowlisted (these tokens may appear; the CI guard tolerates them)

  • ActionSpeed — Valenar in-game channel (declared in examples/valenar/Generated/Declarations.cs as a ChannelDeclaration, used in examples/valenar/Content/characters/channels/movement.secs); the player-facing label for this and its sibling channels is "Stats". Same kind of name as MoveSpeed — has nothing to do with the legacy action keyword.
  • C# Action<...> delegates — the BCL delegate type, used throughout ActivityExecutor, 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_action keyword, OnActionId typed id, OnActionDeclaration, FireOnAction, and the OnAction* 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 idActivityId (plus OnActionId if attaching gameplay-event metadata)
ActionContextActivityRunContext
"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 to scripts/.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.