- Status: Accepted
- Date: 2026-06-03
- Related: architectural-principles.md, tech-stack.md, error-handling.md, code-review.md
The binding code-style rules for all TypeScript in Relavium — every package and every surface. The stack is one language (one-language-TypeScript), so these rules apply uniformly. They serve the build-in-house principle: code we own is code we hold to a high bar.
- TypeScript runs in strict mode (
strict: true, plusnoUncheckedIndexedAccess,exactOptionalPropertyTypes,noImplicitOverride). The sharedtsconfigbase (packages/shared) sets this; packages extend it and do not loosen it. - The build is typecheck-clean: zero
tscerrors, zero suppressed errors. A@ts-expect-erroris allowed only with a one-line justification comment and is itself reviewed;@ts-ignoreis forbidden.
anyis banned. Useunknownat trust boundaries (parsed JSON, providerrawpayloads, IPC input) and narrow it with a Zod schema or an explicit type guard before use.unknownforces a decision;anyhides one.- Do not cast your way out of a type error (
as SomeType) except at a validated boundary, and prefersatisfiesoveraswhen asserting a literal's shape. - Lint enforces this (
@typescript-eslint/no-explicit-anyas an error).
- ESLint + Prettier, configured once at the repo root and shared by every package. Prettier owns formatting; do not argue formatting in review — run it.
- Lint and format are CI-required (see testing.md); a red lint blocks merge.
- No per-file disable of a lint rule without a justification comment naming the reason.
This is the load-bearing rule of the in-house multi-LLM decision (ADR-0011):
- No vendor SDK type ever crosses the
@relavium/llmseam. Provider SDKs (@anthropic-ai/sdk,openai,@google/genai) are imported only insidepackages/llm/src/adapters/*. The seam exposes only Relavium/Zod types —LlmRequest,LlmMessage,ContentPart,LlmResult,StreamChunk,LlmProvider,LlmError. packages/coreand every surface depend on the seam types only. The engine must never pattern-match on a vendor chunk object or re-export a vendor enum. A providerrawpayload may be carried through asunknownfor debugging but is never typed as a vendor shape above the adapter.- An ESLint boundary rule (e.g.
no-restricted-imports/ an import-zones plugin) forbids importing a provider SDK outside the adapter folder. Adding a vendor dependency to the core path requires an ADR and a code review sign-off. - The fence has a size half, CI-enforced: each engine package's runtime
dependenciesmust stay within an explicit, reviewed allowlist (tools/engine-deps/check.mjs, hard-zero — growing the list happens only together with the dependency's ADR). The same import-zone technique extends topackages/ui's internal layer boundaries when that package lands in build phase 3. - The same discipline applies to other framework seams (DB driver, keychain, IPC): the vendor lives behind a Relavium interface, and only that package imports it.
- Errors are typed, not bare
Erroror thrown strings. The engine and the LLM layer throw discriminated, classifiable error types per error-handling.md (notablyLlmErrorwith its retryable/fatal classification). Neverthrow "string". - Public async functions document their failure modes via the error type they reject with;
callers narrow on the discriminant, not on
error.message.
camelCasefor variables and functions,PascalCasefor types/interfaces/classes,UPPER_SNAKE_CASEfor true constants. No Hungarian notation; noI-prefix on interfaces.- File names are kebab-case (
tool-normalizer.ts,anthropic-adapter.ts), matching the docs file-naming rule. - Names match the canonical vocabulary: run events use their colon-namespaced names
(
node:started,agent:token,cost:updated, …) and the per-event ordinal issequenceNumber— neverseqNoor the legacy dotted names. Domain terms match the glossary. - Prefer descriptive over terse; a name that needs a comment to explain it is the wrong name.
- Identifiers stay plain
string(deliberate, not an oversight).runId/nodeId/gateId/workflowId/agentIdarez.string().uuid()(surrogate ids) orkebabIdSchema(authored ids), not branded/nominal types. Validation already happens at the Zod boundary, and branding would add brand/unbrand friction across every@relavium/shared → db → core → surfaceseam for little payoff. If a future class of id-mixup bug proves real, revisit via an ADR.
- Prefer pure functions and explicit data flow in the engine; side effects (IO, IPC, keychain, network) live at the edges, behind interfaces, so the core stays testable.
- Async over callbacks; always thread the
AbortSignalfor cancellable work (streaming, fallback). No floating promises (@typescript-eslint/no-floating-promises). - Public package exports are explicit (a curated
index.ts), notexport *of internals.