Skip to main content

Behavior Vocabulary — Governance Rules

Companion to: docs/decisions/ADR-0002-behavior-vocabulary.md (the decision) Enforced by: scripts/check-behavior-vocabulary.sh (the CI guard) Audience: anyone editing .secs source, generated C#, engine code, design docs, the compiler plan, the task list, or the in-repo example projects.

This document is normative. If a wording rule here disagrees with prose anywhere else in docs/design/, this document wins and the prose is wrong.


TL;DR

TermStatusWhere it may appear
activitycanonicaleverywhere
policycanonicaleverywhere
action (as a behavior noun, the standalone token)banned in live source/docsonly the listed exceptions below
program (as a behavior noun)banned in live source/docsonly the listed exceptions below
on_action keyword, OnActionId, OnActionDeclaration, FireOnActionretained, metadata / extension-point onlyengine, generated C#, .secs source, host runtime

If you are about to type action FooBar in a .secs file, you mean activity FooBar. If you are about to type program ChooseFoo in a design doc, you mean policy ChooseFoo.

Valenar behavior / planning vocabulary

  • Use activity as the canonical Valenar behavior / execution / planning noun.
  • Do not use removed legacy behavior / planning nouns as live Valenar behavior or planning categories.
  • Use more specific terms where needed: Atomic Activity, Composite Activity, Routine Activity, Mission, Assignment, and Policy.
  • Routine Activity and Composite Activity are planning-layer sub-forms of activity. They are not SECS keywords, not runtime nouns, and not separate behavior kinds. Routine Activity describes a reusable composed pattern (a repeating camp sequence, a recurring patrol approach). Composite Activity describes a one-time composed approach for a mission or complex operation. Both lower to sequences of atomic activity queue entries at execution time.
  • Do not preserve removed legacy behavior / planning compatibility language or redirect stubs in Valenar docs; git history is the legacy record.
  • Do not confuse deprecated legacy behavior 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.

What "live" means

The CI guard walks these roots and treats them as live:

  • docs/design/ (the authoritative spec)
  • examples/valenar/Content/ (.secs source)
  • examples/valenar/Generated/ (compiler-output stand-in)
  • examples/valenar/Host/ (host-side game code)
  • examples/valenar/Server/ (ASP.NET Core SignalR hub)
  • examples/valenar/docs/ (Valenar docs)
  • src/ (engine library code)
  • tests/ (test code)
  • README.md (workspace README)
  • SECS-Compiler-Plan.md
  • TASKS.md
  • CLAUDE.md
  • .claude/rules/

The guard explicitly skips these as not live by default:

  • deleted legacy specification material from earlier branches — historical wording only, never live guidance for this guard.
  • docs/research/ — scratch / exploration; never authoritative.
  • docs/decisions/ — ADRs document removal and may quote legacy terms. Selected decision notes may still be scanned explicitly when a wave treats them as live planning guidance.
  • docs/design/behavior-vocabulary.md — this file (defines the rules, must mention banned terms).
  • historical refactor plans and audit-only notes that exist to preserve traceability rather than define current live vocabulary.
  • bin/, obj/, node_modules/, .git/ — build/dependency output.
  • secs-roslyn/ — git submodule with its own toolchain.

If you are unsure whether a path is live, run scripts/check-behavior-vocabulary.sh --check and see whether it walks the file.


Group A — action and its derivatives

Banned in live source/docs

The following tokens must not appear in live source or live docs:

  • action as a standalone keyword in .secs source (e.g., action BuildRoad { ... }).
  • action as a noun in design prose referring to a behavior kind (e.g., "the unit picks an action").
  • SecsAction as a C# type name.
  • ActionRun as a method or hook name.
  • ActionId as a typed-id name (use ActivityId plus OnActionId where appropriate).
  • ActionContext as a type name (use ActivityRunContext).
  • GetAiWeight as a virtual method (the AI weight surface is policy-side, not action-side).
  • AiTickFrequency as a property (use the policy cadence model).
  • action_* as a snake-case identifier prefix (e.g., action_speed, action_id).

Allowlisted (will not trip the guard)

Allowed tokenWhy
ActionSpeedValenar in-game channel. Same kind of name as MoveSpeed / AttackSpeed. Declared in examples/valenar/Generated/Declarations.cs and examples/valenar/Content/characters/channels/movement.secs. Has no relationship to the legacy action keyword.
Action<...> (any C# generic delegate spelling: Action<T>, Action<T1, T2>, Action<TickContext, ...>, etc.)The BCL delegate type. Used throughout ActivityExecutor, EventDispatcher, SecsEvent, host read models. A language primitive, not SECS vocabulary.
inject_or_create, replace_or_createCommitted mod operation keywords from 06-overrides-and-modding.md § 3. They contain _or_create and are not related to action.
on_action, OnActionId, OnActionDeclaration, OnActionDeclarations, FireOnAction, OnAction* type family generally; on-action / On-action in error messages and proseMetadata / extension-point label for an activity, plus the supporting typed-id, declaration, and host trigger surfaces that carry it. The whole OnAction* C# type family is retained by ADR-0002 because it implements the on_action metadata id system, not a separate behavior kind.
Plain-English actionable, actionably, etc. in proseNatural-language use of the English root word is fine. The guard does not match these as separate tokens.

How the guard distinguishes

The guard uses word-boundary regex and explicit allowlist literals. Action< is allowlisted as a substring; ActionSpeed is allowlisted as a literal; inject_or_create and replace_or_create are allowlisted as literals; on_action and OnActionId are allowlisted as literals. A bare Action followed by whitespace, comma, parenthesis, or end-of-line — outside those allowlisted contexts — is a hit.


Group B — program and its derivatives

Banned in live source/docs

  • program as a SECS keyword or noun referring to a behavior kind.
  • SecsProgram as a type name.
  • ProgramRun as a method or hook name.
  • ProgramId as a typed-id name (use PolicyId or ActivityId).

Allowlisted

No live allowlisted use of program remains. Historical removal notes in skipped or explicitly historical files may still mention the prior term when they are documenting its removal rather than presenting it as current vocabulary.

The English word programming (e.g., "logic programming") is unusual in this codebase; if it appears, it is reviewed case-by-case and the script is updated to allowlist the specific phrase if needed.


Group D — content-explosion smell

Activity ids must not encode game-content variants in their identifier. The guard greps for any Activity_<Camel> identifier shape and fails on hits not present in the Group D allowlist.

The rule has two halves — a positive form and a negative form. They are equally important.

The positive form — Activity_<MechanicName>

One activity per mechanic family, parameterized by typed args. A CastSpell activity carries a CastSpellArgs(TemplateId) blob; the spell template (Fireball, Heal, FrostBolt) is the data, the activity is the shape. Adding a new spell template does not add a new SecsActivity declaration. This is the prevention rule for "200 spells = 200 activities."

The seven canonical generic-mechanic activity names — committed in docs/design/04-behavior.md § "Content-family parameterization" — are allowlisted by the guard:

Mechanic familyActivity idArgs typeData template kind
SpellsActivity_CastSpellCastSpellArgs(TemplateId)SpellDefinition
BuildingsActivity_BuildStructureBuildStructureArgs(TemplateId)Building template
ItemsActivity_UseItemUseItemArgs(ItemId)ItemDefinition
RecipesActivity_CraftRecipeCraftRecipeArgs(TemplateId) (future)RecipeDefinition (future)
SkillsActivity_TrainSkillTrainSkillArgs(TemplateId) (future)SkillDefinition (future)
Work ordersActivity_PerformWorkOrderPerformWorkOrderArgs(TemplateId) (future)WorkOrderDefinition (future)
RitualsActivity_PerformRitualPerformRitualArgs(TemplateId) (future)RitualDefinition (future)

Adding an eighth generic mechanic family is a design-surface change. It requires:

  1. ADR or design-doc update committing the new family.
  2. Adding the Activity_<NewMechanic> name to GROUP_D_ALLOWLIST_REGEX in scripts/check-behavior-vocabulary.sh.
  3. Adding the row to the table above.
  4. Adding the row to .claude/rules/behavior-vocabulary.md.
  5. Re-baselining via --baseline.

The negative form — Activity_<ContentName> is banned

Activity_FrostBolt, Activity_IronSword, Activity_HealingPotion, Activity_Swordsmanship — any name that encodes a specific content row — is the smell. The guard refuses these. The fix is to express the content as a data template under the appropriate definition contract and let the generic-mechanic activity dispatch via typed args.

One-off activities are not the smell

Activities like Activity_RecruitGarrison, Activity_ScoutNearbyLead, Activity_RestAtCamp are structurally one-of-a-kind — they are a single activity with a single body, not parameterized variants of a family. They are legitimate. The guard tolerates them via the project-wide baseline (any Activity_<Camel> count present at baseline-record time stays allowed); they did not require allowlist additions because they are not a content family. New one-off activities follow the same convention: ship the activity, run --baseline to record the new floor.


Group C is reserved

"Group C" remains reserved so the guard-group lettering stays stable. program keyword fallbacks in design prose are already caught under Group B.


Group E — non-committed syntax forms

The following are not committed SECS syntax and must not appear in live source or be presented as accepted language forms in live docs: candidate_builder ..., any nested slot ..., activity ..., or method ActivityRequest[] Build(...) clauses inside it, and from collection X with builder Y. These may be mentioned only as rejected or historical forms. Runtime/lowering helpers such as SelectorSource.FromCollection(...) remain C# implementation surface, not .secs vocabulary.

CI guard coverage: scripts/check-behavior-vocabulary.sh catches these forms in live paths, with explicit exemptions for historical audit files and for this file, because the authoritative ban list necessarily quotes the banned forms. The provenance test GeneratedCitedSourceFilesMustNotContainBannedSyntaxForms catches the same forms in .secs files cited by Generated // Source: headers.


Workflow

When you are about to commit a change to a live path:

scripts/check-behavior-vocabulary.sh --check

A clean run prints OK and exits 0. A non-clean run prints file:line: <matching context> for each hit and exits non-zero.

If you have intentionally added a new historical mention (e.g., a new ADR), update the script's ignore list. Do not silence the guard with # noqa-style line comments; the rule is path-based, not line-based.

When a deliberate cleanup drives the hit count down and you want the recorded floor to move with it, update the baseline:

scripts/check-behavior-vocabulary.sh --baseline

This writes the current hit count to scripts/.behavior-vocabulary-baseline. Subsequent --check runs compare against that file.

The end state is 0 hits. Until then, the baseline records the current accepted floor.


What if you really need to add a new exception?

  1. Open an ADR in docs/decisions/ documenting why.
  2. Update this file's allowlist table.
  3. Update scripts/check-behavior-vocabulary.sh to allowlist the literal.
  4. Run scripts/check-behavior-vocabulary.sh --baseline to record the new accepted hit count.

Do not add an exception by editing the script alone. The governance trail (ADR + this doc + the script) must move together.

Wave 3 removed the Valenar player-doc carve-out for Action / Program from the design doc. Wave D (Valenar docs alignment) performed the actual doc cleanup — updating objectives-clues-missions.md, current-plan.md, objectives-screen.md, camp.md, glossary.md, character-skills.md, and combat-dungeon-screen.md to remove action/program as live planning nouns. Run scripts/check-behavior-vocabulary.sh --baseline after Wave D to record the reduced floor.