agentleFS
alexander-zuevalexander-zuev/agent-skills/skills/typescript-standards/SKILL.md

typescript-standards

Skill0 starsChanged 11 days agoApache-2.0View on GitHub

Domain modeling and module design in TypeScript: DDD roles, ports and adapters, branded types, tagged errors, and tests through real seams. Use when writing or reviewing any TypeScript.

  • Reads credentials

9 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

76 rules. 0 are common, 54 are distinctive, and 22 are very specific to this project.

  • 0

    Common

    Most of the 9 edited versions we read keep these.

  • 54

    Distinctive

    This author's own choices. Worth a look.

  • 22

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use tanstack-frontend for FSD, React, queries, mutations, and UI conventions.

    line 11
  2. Distinctive:

    Use cloudflare-backend for application boundaries, transport contracts, retries, and persistence.

    line 12
  3. Distinctive:

    Keep unrelated code unchanged.

    line 17
  4. Distinctive:

    Default to typed thrown errors and rejected promises.

    line 21
  5. Distinctive:

    Prefer correct-by-construction APIs over convention-based invariants.

    line 24
  6. Very specific:

    Use branded/refined/domain types when they prevent a realistic mistake, such as mixing identifiers or units, bypassing parsing, or …

    line 25
  7. Distinctive:

    Prefer composition over inheritance.

    line 26
  8. Distinctive:

    Prefer imperative shell / functional core.

    line 27
  9. Distinctive:

    Test behavior through real seams.

    line 29
  10. Distinctive:

    Keep code discoverable for humans and agents.

    line 30
  11. Distinctive:

    Before writing general-purpose logic, identify which runtime, framework, SDK, or installed dependency could own it.

    line 34
  12. Distinctive:

    Check its exports and documentation.

    line 35
  13. Distinctive:

    Write custom logic last, with a reason.

    line 36
  14. Distinctive:

    Before adding a new pattern or library, inspect the repo for existing choices around:

    line 49
  15. Distinctive:

    Do not move, re-indent, re-wrap, reword, rename, or retype existing code.

    line 68
  16. Distinctive:

    Use typed errors with stable tags or codes and useful context.

    line 79
  17. Very specific:

    Document non-obvious thrown failures with @throws.

    line 89
  18. Very specific:

    Use better-result for an isolated operation only when explicit result composition provides a concrete benefit.

    line 95
  19. Distinctive:

    Do not adopt it merely because the dependency is available or expected failures exist.

    line 99
  20. Distinctive:

    When an isolated operation returns Result, its adapter can extract success or throw the typed error into existing middleware.

    line 106
  21. Distinctive:

    Use the project's existing conversion helper.

    line 107
  22. Distinctive:

    Read the backend skill before changing this contract, including when adding server stubs.

    line 108
  23. Very specific:

    Never use throw new Error(...) in production code.

    line 112
  24. Distinctive:

    Use established shared defect helpers where available, or the panic helper from the project's result library:

    line 124
  25. Very specific:

    Use casesHandled for exhaustive union handling.

    line 132
  26. Distinctive:

    Keep error unions precise at module boundaries:

    line 166
  27. Very specific:

    Avoid broad AppError-style types except near entrypoints, orchestration, logging, and rendering layers.

    line 172
  28. Distinctive:

    Do not wrap when:

    line 178
  29. Distinctive:

    every inner failure becomes the same outer tag (a lie: invalid input is not "unavailable")

    line 181
  30. Distinctive:

    Prefer end-to-end structured tracing across requests, jobs, workflows, application modules, adapters, and external calls.

    line 217
  31. Distinctive:

    Do not put secrets in errors, traces, logs, or snapshots.

    line 229
  32. Very specific:

    Use a Redacted<T> wrapper for sensitive values such as tokens, API keys, passwords, raw credentials, and secrets.

    line 231
  33. Distinctive:

    Do not pass a schema-inferred transport shape throughout the application:

    line 251
  34. Distinctive:

    Use names that preserve meaning:

    line 257
  35. Very specific:

    Avoid validateX when the function returns a refined value.

    line 264
  36. Distinctive:

    Use schema libraries as boundary parsers, not as ad-hoc validators sprinkled through core logic.

    line 268
  37. Distinctive:

    use the repo's established schema library if one exists

    line 272
  38. Distinctive:

    use Effect Schema in Effect codebases

    line 273
  39. Distinctive:

    prefer Standard Schema compatibility for generic helpers

    line 274
  40. Distinctive:

    use hand-written smart constructors/parsers for small domain types when clearer

    line 276
  41. Very specific:

    Use top-level format helpers: z.uuid(), z.email(), z.url(), z.httpUrl(), z.iso.datetime(), z.iso.date(), and z.iso.time().

    line 282
  42. Very specific:

    Use z.uuidv7(), z.nanoid(), z.ipv4(), z.base64(), z.jwt(), and z.stringbool() where applicable.

    line 283
  43. Very specific:

    Use branded ID schemas, such as UserIdSchema, instead of bare z.string() for typed IDs.

    line 285
  44. Distinctive:

    Use branded/refined types when they prevent realistic misuse or invalid construction, especially for:

    line 289
  45. Very specific:

    Avoid optional/null/undefined values in functions that require a value.

    line 298
  46. Very specific:

    Avoid Partial<T> as an application/domain input unless partiality is the real domain concept.

    line 300
  47. Distinctive:

    Keep canonical facts or an authoritative result.

    line 305
  48. Distinctive:

    Before adding a field:

    line 308
  49. Distinctive:

    When an entity has meaningful lifecycle states, model them with tagged unions or equivalent value classes.

    line 321
  50. Distinctive:

    Avoid boolean parameters that control behavior:

    line 343
  51. Distinctive:

    Prefer named options or domain types:

    line 349
  52. Distinctive:

    Use ordinary function calls or database transactions for simple single-boundary operations.

    line 383
  53. Distinctive:

    Use a saga or durable workflow when progress must survive process loss or redelivery, or when the operation requires long delays, …

    line 385
  54. Distinctive:

    Do not hold database transactions open across network calls or long-running operations.

    line 389
  55. Very specific:

    Never use vi.mock or jest.mock.

    line 403
  56. Distinctive:

    Every non-JSDoc comment is exactly one line.

    line 412
  57. Distinctive:

    Delete comments that describe what the code already shows.

    line 413
  58. Distinctive:

    Document key modules and exported architectural boundaries.

    line 416
  59. Very specific:

    Do not repeat TypeScript types or add boilerplate to every field.

    line 417
  60. Very specific:

    Do not use @inheritDoc or @inherit.

    line 418
  61. Very specific:

    Use @throws for defects, framework-required throws, and temporary unimplemented paths.

    line 420
  62. Distinctive:

    Document internal expected failures as return values.

    line 421
  63. Distinctive:

    Use the language's deprecation directive for each deprecated symbol or path.

    line 423
  64. Distinctive:

    Use a one-line comment only when no directive exists.

    line 424
  65. Very specific:

    Use apps/ for applications and packages/ for shared code.

    line 429
  66. Very specific:

    Use workspace:* for local dependencies.

    line 432
  67. Very specific:

    Do not read process.env throughout the app.

    line 439
  68. Distinctive:

    Avoid top-level side effects except in true entrypoint/bootstrap files.

    line 441
  69. Distinctive:

    Avoid mutable singletons/global state.

    line 445
  70. Distinctive:

    Read existing conventions for errors, schemas, tests, adapters, telemetry, and module layout.

    line 453
  71. Very specific:

    Avoid raw DTOs, raw IDs, nullable bags, and Partial<T> in core/application logic.

    line 458
  72. Distinctive:

    Prefer typed errors as values for new expected failures.

    line 459
  73. Distinctive:

    Test through public interfaces and real seams.

    line 461
  74. Very specific:

    Use fast-check arbitraries for generated test data when practical.

    line 462
  75. Distinctive:

    Document exported contracts when callers need information the types cannot show.

    line 463
  76. Distinctive:

    Add an ADR only for a lasting architectural boundary, shared pattern, provider strategy, or deliberate exception discovered through the …

    line 464

“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

These standards describe how to design and write TypeScript code in this codebase. They are especially intended for agents: inspect existing code before adding patterns, libraries, Adapters, or abstractions, but apply these standards to all new and refactored behavior. Follow existing conventions only when they are compatible with these standards.

Rule ownership

This skill owns types, schemas, module design, comments, and general tests. Use tanstack-frontend for FSD, React, queries, mutations, and UI conventions. Use cloudflare-backend for application boundaries, transport contracts, retries, and persistence.

Those skills define their assigned topics. An internal Result convention does not prescribe a transport envelope. Explicit task instructions and global workflow constraints still apply. Apply these standards to new and refactored behavior. Contain incompatible existing patterns at their boundary. Keep unrelated code unchanged. Record lasting architectural decisions when they need an ADR.

Core principles

  • Default to typed thrown errors and rejected promises. Preserve established Result or Effect contracts.
  • Parse don't validate. Parse early and as close to composition or application roots as possible. Do not merely validate and throw away the information learned.
  • Make illegal states unrepresentable where practical.
  • Prefer correct-by-construction APIs over convention-based invariants.
  • Use branded/refined/domain types when they prevent a realistic mistake, such as mixing identifiers or units, bypassing parsing, or constructing an invalid value.
  • Prefer composition over inheritance.
  • Prefer imperative shell / functional core.
  • Design deep, cohesive modules with low caller burden.
  • Test behavior through real seams; avoid module mocks and spy-driven tests.
  • Keep code discoverable for humans and agents.

Reuse and operation contracts

Before writing general-purpose logic, identify which runtime, framework, SDK, or installed dependency could own it. Check its exports and documentation. Consider a maintained library if the stack has no suitable capability. Write custom logic last, with a reason.

An owned operation must answer only when its promised effect exists. Fix that operation instead of adding downstream polling, deferred registries, or completion waiters. Bridge an event to a promise only at its owner, when the event source cannot provide completion directly. An asynchronous operation may return an accepted job. Its contract must distinguish acceptance from completion.

A signature declares exactly what the function receives. Do not pass a broader object than the parameter type admits. Consume declared API response types. Check client configuration and generic inference before reconstructing response types with ad-hoc guards. For owned APIs, fix missing schemas or types at the boundary. Parse untrusted input with schemas.

Adapting to existing codebases

Before adding a new pattern or library, inspect the repo for existing choices around:

  • error handling
  • schema parsing
  • dependency injection
  • testing
  • observability
  • adapters/services
  • module layout

Apply these standards to all new code and to the full behavior being refactored. Do not preserve weaker patterns merely for consistency. Keep unrelated old code unchanged and translate incompatible patterns at the nearest boundary.

Most apps use thrown errors and rejected promises. Follow that convention in new and refactored code. A dependency on better-result, including a transitive dependency, does not justify adopting Result. Preserve existing logging, tracing, metrics, and error-reporting hooks.

Minimal diffs

A change touches only the lines that its agreed behavior needs. Keep every other existing line byte-identical. Do not move, re-indent, re-wrap, reword, rename, or retype existing code. Add new code around it; do not restructure it to fit. Make an unrequested cleanup only when the user agrees or it is a real improvement, and then as a separate change. A refactor is the exception only when the user asked for one. If a change forces other lines to move, say why first.

Unrequested edits hide the real change in review and move git blame to the wrong commit. A renamed exported symbol can also break callers.

Errors and failures

Define each operation's failures

Think through known failures even when TypeScript cannot declare thrown errors in the return type. Use typed errors with stable tags or codes and useful context. Never classify errors from message text.

Before implementation, identify:

  1. Which failures the operation can produce, including cancellation and partial completion.
  2. Which failures the caller must distinguish and what action each requires.
  3. Which layer converts external failures and which boundary logs the final error.
  4. Whether repetition is safe and which layer owns retries.
  5. Which focused tests prove important failure behavior and cleanup.

Document non-obvious thrown failures with @throws. Do not add unused error unions to imply checked exceptions. Convert external failures at the owning adapter when needed. Preserve their causes and distinctions the caller needs. Expected absence, completion, and no-op outcomes may be ordinary data rather than errors.

Limited use of Result

Use better-result for an isolated operation only when explicit result composition provides a concrete benefit. Examples include nuanced retry classification or exceptional workflows with several recoverable outcomes. Retries alone do not require Result. Check the project's existing retry capability first. State the local benefit and keep conversion at that operation's boundary. Do not spread Result through callers to satisfy this skill. Do not adopt it merely because the dependency is available or expected failures exist. Adopting Result across an app requires a separate, explicitly approved change. Preserve established Result or Effect code; this default does not require migrating it to throws.

Transport conversion

A transport adapter uses the transport's failure channel. Shared middleware owns serialization and error conversion. When an isolated operation returns Result, its adapter can extract success or throw the typed error into existing middleware. Use the project's existing conversion helper. Do not add a success envelope or repeat serialization in every function. Read the backend skill before changing this contract, including when adding server stubs.

Defects may throw or panic

Never use throw new Error(...) in production code. Use a typed error with a stable tag or code. Migrate raw throws when touched. Deliberate test and failure-probe throws are exempt.

Typed throws can represent expected failures. Reserve panic and defect helpers for conditions that make correct execution impossible. Defects include:

  • violated internal invariants
  • impossible branches
  • temporary notYetImplemented paths
  • catastrophic runtime conditions

Known configuration failures use typed errors; the composition root reports them safely and terminates startup.

Use established shared defect helpers where available, or the panic helper from the project's result library:

export function casesHandled(unexpectedCase: never): never;
export function shouldNeverHappen(msg?: string): never;
export function notYetImplemented(msg?: string): never;

Use casesHandled for exhaustive union handling. Avoid names like absurd or one-off assertNever helpers when the project already has these helpers.

Custom errors

Expected failures should use custom tagged errors, generally extending:

  • Error
  • TaggedError from better-result
  • Schema.TaggedErrorClass in Effect codebases

Custom errors should include:

  • stable tag using 'as const'
  • useful message
  • structured contextual fields
  • safe telemetry fields
  • optional cause: unknown

Example:

export class UserStoreUnavailable extends Error {
  readonly _tag = "UserStoreUnavailable" as const;

  constructor(
    readonly operation: "findActiveByEmail",
    readonly provider: "postgres",
    readonly cause: unknown,
  ) {
    super(`User store unavailable during ${operation}`);
  }
}

Keep error unions precise at module boundaries:

Result<User, UserNotFound | UserStoreUnavailable>

Avoid broad AppError-style types except near entrypoints, orchestration, logging, and rendering layers.

Wrapping and rethrowing

Wrap only when crossing a port whose callers must not see the inner error types. Map each inner tag onto one outer tag that callers already branch on. Preserve the inner error as cause. Callers match the outer tag; they must not unwrap cause to decide.

Do not wrap when:

  • you are still in the layer that owns the inner tags
  • every inner failure becomes the same outer tag (a lie: invalid input is not "unavailable")
  • the wrap exists only to avoid importing the inner type

The examples below preserve an existing throw-based boundary contract. For an internal Result port, return the mapped error as a typed value.

Good — map at the port:

async function startGrok(signal: AbortSignal): Promise<AcpTransport> {
  try {
    return await AcpTransport.start({ command: "grok", signal, /* … */ })
  } catch (cause) {
    throw new CodingAgentUnavailableError({ cause })
  }
}

Spawn failed. The coding-agent port has no ACP spawn type. Unavailable is the fact the caller can act on.

Bad — catch-all rethrow:

async function grokRequest(transport: AcpTransport, input: Request): Promise<Response> {
  try {
    return await transport.request(input)
  } catch (cause) {
    throw new CodingAgentUnavailableError({ cause })
  }
}

request already classified timeout, process exit, and JSON-RPC rejection. This wrap deletes that distinction. A bad configId looks like "Grok is down." Call transport.request instead, or map AcpRpcError to CodingAgentRequestError and only the transport/timeout/exit tags to CodingAgentUnavailableError.

Sensitive data, telemetry, and debugging

Prefer end-to-end structured tracing across requests, jobs, workflows, application modules, adapters, and external calls.

Tracing/logging should make failures diagnosable with safe fields:

  • domain IDs
  • operation names
  • dependency/provider names
  • state tags
  • retry counts
  • typed error tags
  • safe summaries

Do not put secrets in errors, traces, logs, or snapshots.

Use a Redacted<T> wrapper for sensitive values such as tokens, API keys, passwords, raw credentials, and secrets. Prefer Effect's Redacted.Redacted in Effect codebases or a local shared Redacted<T> wrapper.

Wrap sensitive values at the boundary and unwrap only where the raw value is needed, usually inside an adapter making an external call.

Parse, don't validate

Boundary code should turn unknown or less-structured input into application or domain types before it enters inner code.

Use a separate protocol projection only when its shape or meaning differs enough to be useful. DTO describes a boundary role in prose; never use DTO or Dto in a symbol name. Name the symbol after its actual protocol or persistence meaning, such as CreateUserRequest, StripeCustomerResponse, or UserRecord:

unknown -> CreateUserRequest -> CreateUserInput -> EmailAddress/UserId/etc.

Otherwise, parse directly into the application input:

unknown -> CreateUserInput

Do not pass a schema-inferred transport shape throughout the application:

unknown -> z.infer<typeof CreateUserSchema>

Use names that preserve meaning:

  • parseX(input): Result<X, ParseXError> for untrusted or less-structured input
  • makeX(...) / createX(...) for smart constructors from already-typed pieces
  • isX(value): value is X for true predicates
  • assertX(...) rarely, mostly at tests/framework boundaries

Avoid validateX when the function returns a refined value. It parsed something.

Schemas

Use schema libraries as boundary parsers, not as ad-hoc validators sprinkled through core logic.

Preference:

  • use the repo's established schema library if one exists
  • use Effect Schema in Effect codebases
  • prefer Standard Schema compatibility for generic helpers
  • otherwise prefer Zod 4
  • use hand-written smart constructors/parsers for small domain types when clearer

Schema parsing should produce refined/domain types and typed custom errors where practical.

For Zod 4:

  • Use top-level format helpers: z.uuid(), z.email(), z.url(), z.httpUrl(), z.iso.datetime(), z.iso.date(), and z.iso.time().
  • Use z.uuidv7(), z.nanoid(), z.ipv4(), z.base64(), z.jwt(), and z.stringbool() where applicable.
  • Set messages with { error: "message" }, not message or invalid_type_error.
  • Use branded ID schemas, such as UserIdSchema, instead of bare z.string() for typed IDs.

Branded types and correct construction

Use branded/refined types when they prevent realistic misuse or invalid construction, especially for:

  • IDs: UserId, OrgId, WorkflowId
  • parsed strings: EmailAddress, NonEmptyString, Url
  • constrained numbers: PositiveInt, Cents, Percentage
  • units: Milliseconds, Bytes, UsdCents

Construct branded values through parsers or smart constructors. Avoid passing raw strings/numbers where a domain type exists.

Avoid optional/null/undefined values in functions that require a value. Push optionality outward. Branch or parse before calling.

Avoid Partial<T> as an application/domain input unless partiality is the real domain concept. Prefer explicit input types for each operation.

One fact, one representation

Represent each independent fact once in rows, contracts, events, caches, state, props, and configuration. Keep canonical facts or an authoritative result. Derive deterministic values at the owning or consuming layer. Expose a result when its derivation contains business policy. Do not duplicate its inputs for convenience.

Before adding a field:

  1. Is this an independent fact?
  2. Can existing fields determine it exactly?
  3. What happens if the values disagree?
  4. Which value is canonical?
  5. Which layer owns the derivation?

Allow redundancy only for external compatibility, immutable audit snapshots, measured performance, or unavailable private source data. Identify the canonical source. Synchronize atomically and enforce the invariant where possible. Test disagreement cases.

State machines and boolean blindness

When an entity has meaningful lifecycle states, model them with tagged unions or equivalent value classes.

Prefer:

type Invoice =
  | { readonly _tag: "Draft"; readonly id: InvoiceId; readonly lines: NonEmptyArray<LineItem> }
  | { readonly _tag: "Sent"; readonly id: InvoiceId; readonly sentAt: Instant }
  | { readonly _tag: "Paid"; readonly id: InvoiceId; readonly paidAt: Instant };

Avoid:

type Invoice = {
  readonly isSent: boolean;
  readonly isPaid: boolean;
  readonly sentAt?: Date;
  readonly paidAt?: Date;
};

Avoid boolean parameters that control behavior:

createUser(input, true);

Prefer named options or domain types:

createUser(input, { emailVerification: "skip" });

Booleans are fine as clear predicate return values:

isExpired(token): boolean;
hasPermission(user, permission): boolean;

Modules and abstractions

Domain Module, Application Service Module, and Adapter Module name responsibilities, not folders. Before you design or review a module, port, or adapter, read modules and ports. It defines each role, deep modules, application-owned ports, the adapter reuse audit, and repositories.

Functional core, imperative shell, and entrypoints

Domain Modules form the functional core. Application Service Modules and Adapter Modules form the imperative shell, but only Adapters contain technology-specific concerns. This keeps the same application operation reusable across REST, CLI, GraphQL, workers, and other entrypoints.

The functional core contains domain parsers, invariants, state transitions, calculations, combinators, and decision functions. It avoids I/O, hidden dependencies, ambient time/randomness, thrown expected failures, and framework-specific concerns.

The imperative shell has two distinct responsibilities:

  • Application Services apply application policy and sequence effects through explicit ports.
  • Adapters parse or project boundary values, classify external failures, and perform concrete I/O.

Entrypoint Adapters should be thin protocol translation layers. They parse protocol-specific input, call Domain Module parsers to obtain refined values, invoke an Application Service when application policy or effects are involved, and render protocol-specific output. A pure operation may call a Domain Module directly, as modules and ports describes. Do not duplicate business rules in controllers, resolvers, commands, or handlers.

Within authentication and authorization, inbound Adapters verify boundary credentials and produce a parsed identity such as Principal, Session, or CommandActor. Domain Modules may define pure permission decisions over parsed domain values. Application Services gather the required context and enforce those decisions while carrying out an application operation. Adapters project missing or invalid credentials and denied operations into protocol-specific outcomes; they do not define permission policy.

Workflows, transactions, and idempotency

Use ordinary function calls or database transactions for simple single-boundary operations.

Use a saga or durable workflow when progress must survive process loss or redelivery, or when the operation requires long delays, compensation, resumability, timers, human approval, cross-service coordination, or multiple transaction boundaries. A short-lived retry by itself does not require durable workflow machinery.

Adapters own safe, short-lived technical retries. Application Services decide whether an application operation should be attempted again. Durable workflows own retries that must survive crashes, delays, or redelivery.

Do not hold database transactions open across network calls or long-running operations.

Any externally observable mutation or state transition that may be retried needs an explicit idempotency strategy:

  • idempotency key
  • natural unique constraint
  • deduplication record
  • state-machine transition guard
  • transactional outbox/inbox

Retrying should not rely on “probably safe” side effects.

Testing

Never use vi.mock or jest.mock; test through real seams. Before you write, review, or speed up a test, read testing. It is the default for every project. A project testing document records only exceptions.

TypeScript style and safety

No any, no non-null assertions, and no as cast without a SAFETY: comment. Before you write or review TypeScript, read style and files. It covers compiler settings, casts, imports, exports, and file names.

Comments and JSDoc

Every non-JSDoc comment is exactly one line. Explain why: a constraint, rejected alternative, or external rule. Delete comments that describe what the code already shows.

JSDoc defaults to two or three lines. Add lines only for a caller's non-obvious contract, side effect, ordering, or lifetime rule. Document key modules and exported architectural boundaries. Stop when the caller can use them correctly. Do not repeat TypeScript types or add boilerplate to every field. Document the original declaration, not its re-exports. Do not use @inheritDoc or @inherit.

Use @throws for defects, framework-required throws, and temporary unimplemented paths. Document internal expected failures as return values. Transport failure channels follow the backend skill.

Use the language's deprecation directive for each deprecated symbol or path. Use a one-line comment only when no directive exists. Create a Linear issue for each deprecation. State the removal condition, why the path remains, and how verification proves removal.

Workspace boundaries

Use apps/ for applications and packages/ for shared code. Workspace dependency direction is packages → ∅ and apps → packages. Never import between applications. Core exports shared types, schemas, constants, and common logic. Use workspace:* for local dependencies. Follow AGENTS.md for third-party version and catalog management. Build shared dependencies before consumers through the project's scripts or task graph.

Configuration and resources

Parse environment/config at startup or the earliest boundary into typed config with branded/redacted values where appropriate. Return known configuration failures as tagged error values. The composition root should report a safe startup message and terminate rather than treating invalid configuration as an internal defect.

Do not read process.env throughout the app. Missing or invalid config is a startup failure with useful, safe context.

Avoid top-level side effects except in true entrypoint/bootstrap files. Modules should not start servers, open connections, read env, register handlers, or perform I/O at import time.

Resource creation and cleanup should be explicit and owned by bootstrap/imperative shell code or Effect layers when using Effect.

Avoid mutable singletons/global state. Constants and pure lookup tables are fine. If a singleton is required by a framework/runtime, isolate it at the boundary.

Inject Clock / Random services into dependency-bearing modules. Pure domain functions may accept explicit now / random values.

Quick agent checklist

Before coding:

  • Read existing conventions for errors, schemas, tests, adapters, telemetry, and module layout.
  • Classify each changed concern as Domain Module, Application Service Module, Adapter Module, or composition-root wiring.
  • Reuse existing Domain Modules, Application Services, and Adapters before creating new ones.
  • Define effect dependencies as narrow, application-owned ports; keep raw external types in Adapters or the composition root.
  • Parse inputs at the edge and use domain types internally.
  • Avoid raw DTOs, raw IDs, nullable bags, and Partial<T> in core/application logic.
  • Prefer typed errors as values for new expected failures.
  • Preserve existing observability/error mechanics.
  • Test through public interfaces and real seams.
  • Use fast-check arbitraries for generated test data when practical.
  • Document exported contracts when callers need information the types cannot show.
  • Add an ADR only for a lasting architectural boundary, shared pattern, provider strategy, or deliberate exception discovered through the Adapter/Application Service reuse audit.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.