This document pins the stylistic conventions for src/ and tests/.
CONTRIBUTING.md covers workflow; this file is just the code-level rules
that prettier and eslint can't always enforce mechanically.
Where prettier or eslint already enforces a rule, we say so and move on.
- Strict mode on.
strict,exactOptionalPropertyTypes,noUncheckedIndexedAccess,noImplicitOverride,noFallthroughCasesInSwitch,noImplicitReturns— all on. Don't turn any off without discussion. - ESM only.
"type": "module"inpackage.json; all relative imports carry the.jsextension (verbatimModuleSyntax). No CommonJS, no dynamicrequire. typevs.interface. Prefertype X = …for unions, tuples, mapped types, conditional types, primitives, and any shape that looks algebraic. Useinterface Xfor open object shapes that consumers are expected to extend, or when declaration merging is deliberate. When in doubt →type.unknownoverany.anyis an escape hatch of last resort. Everyanyinsrc/should either be deleted or carry a one-line comment explaining whyunknownwasn't usable.- No enums. Use
as constunions of string literals. Enums are a runtime payload for a compile-time concept and don't play nicely with the library's determinism contract. - Immutable by default. Fields are
readonlyunless mutation is genuinely required. Arrays returned from public surfaces arereadonly T[]. - Exhaustiveness.
switchstatements over union discriminants end with adefault: { const _exhaust: never = value; throw new Error(...) }block when the union is closed.
- One concept per file. A file named
FooSkill.tsexportsFooSkilland at most its immediate helpers. No kitchen-sink modules. - Test mirror. Every
src/foo/Bar.tshas an optionaltests/unit/foo/Bar.test.ts. Integration tests go undertests/integration/. - Barrel control.
src/index.tsre-exports the public surface. Internal modules (anything undersrc/agent/internal/, anything prefixed_) do NOT appear in the barrel.
- Top-level JSDoc on every exported class / interface / type / const.
Three requirements:
- The first sentence describes the concept in one line.
- The body explains the non-obvious invariants — when to use it, what breaks, what it's NOT.
- Renderable in IntelliSense — no multi-page essays, no ASCII diagrams that break Markdown parsers.
- Barrel JSDoc. One-liner comments above each re-export when the
identifier name is opaque on its own (see the cognition + needs + mood
sections of
src/index.ts). - No default exports. Named exports only. Default exports break re-exporting and make grep harder.
- Alphabetized inside each group. Prettier doesn't sort them; eslint's
perfectionist/sort-importsor manual discipline does. - Grouped: standard library / node built-ins → third-party packages →
workspace (
../...) → local (./...). A blank line between groups is optional; consistency within a file is what matters. - Type-only imports. Use
import typewhen the import is purely a type —verbatimModuleSyntaxrequires it.
- Reserve
//comments for WHY-is-this-weird explanations that belong inline with the code. Everything else that documents a symbol lives in JSDoc. - No
@param/@returnstags when the types are self-documenting. Use them only when the prose clarifies units, valid ranges, or error modes. - No
@deprecatedwithout a migration path. - Code fences in JSDoc are fine; keep them short.
- Classes:
PascalCase, noun-phrased.UrgencyReasoner,AnimationStateMachine. - Interfaces + types:
PascalCase, noun-phrased. NoIprefix. - Functions + methods:
camelCase, verb-phrased.createAgent,satisfyNeed,selectIntention. - Constants:
SCREAMING_SNAKE_CASEfor module-scoped literals (DEFAULT_TIME_SCALE,DECEASED_STAGE). Local constants inside functions arecamelCase. - Booleans:
is/has/should/canprefix.isInvokeSkillAction,hasModifier,shouldSave. - Private / internal:
publicmethods needed by in-repo tick helpers (e.g.Agent.publishEvent,Agent.routeDeath) carry the TSDoc@internaltag and are not re-exported from the public barrel. Every other private member uses TypeScript'sprivateorprotected. Avoid leading-underscore names — the@internaltag and barrel discipline are the contract.
- Prose comments explain the WHY, not the WHAT — the latter belongs in the code itself.
- References to old milestones (
// M2:,// Phase B) rot fast. Use them only in the immediate commit they land in, and remove them in the next pass (or open an issue if the reference still has value). - No
TODOcomments without a linked issue number.// TODOalone is worse than no comment — it's a landmine.
- Arrange / Act / Assert. Blank lines between the three phases inside longer tests.
- Descriptive titles.
it('returns null when the only candidate scores below threshold')>it('threshold'). - Seed everything. Agent-level tests always construct with
SeededRng(<literal>)andManualClock(<literal>)— nevernew SystemClock(). - Observable assertions. Prefer asserting on event streams + public
state slices (
agent.getState(),agent.rng.next()) over reaching for protected fields. Reserve field access for the extraction tests of thesrc/agent/internal/*helpers.
The following files were authored in parallel by sub-agents during Phase A and should be held to this style going forward:
src/skills/defaults/*.ts(all ten default skills)src/body/*.tssrc/randomEvents/*.tssrc/agent/RemoteController.tssrc/agent/ScriptedController.ts
When editing any of the above, re-read the relevant section above and
match it. Any Phase B references left over from the subagent prompts
have been scrubbed in R-22; keep them out.
npm run lintcovers the eslint-enforceable rules.npm run format:checkcovers prettier rules.- Everything else in this guide is on human review at PR time.