一键导入
coding-standards
Correct-by-construction TypeScript standards. Use for TypeScript engineering or when another skill needs the user's coding standards.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Correct-by-construction TypeScript standards. Use for TypeScript engineering or when another skill needs the user's coding standards.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Prelude bootstrapping for TypeScript. Use when creating or rebuilding a prelude.ts from ambient generic helpers and types.
Control herdr from inside it. Manage workspaces and tabs, split panes, spawn agents or subagents, read output, start dev servers, and wait for state changes — all via CLI commands that talk to the running herdr instance over a local unix socket. Use when running inside herdr (HERDR_ENV=1).
| name | coding-standards |
| description | Correct-by-construction TypeScript standards. Use for TypeScript engineering or when another skill needs the user's coding standards. |
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.
When rules pull in different directions, use this order:
throw / rejected promises for expected failures.Before adding a new pattern or library, inspect the repo for existing choices around:
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.
For example, if existing code uses exception-style errors, do not rewrite the whole system for an unrelated change. Represent known failures as typed values in new or refactored code, then translate them at the boundary into the outcome required by the existing framework. Preserve existing logging, tracing, metrics, and error-reporting hooks.
Every known failure mode should appear in the return type as a custom tagged error, even when the immediate caller cannot recover. A caller must handle the error or return it upward. At the outermost boundary, translate it into a valid outcome such as an HTTP response, CLI exit code, retry decision, dead letter, or startup error message.
Known failures include domain, parsing, authorization, integration, I/O, persistence, configuration, and workflow failures.
Preferred order:
better-result, when available and appropriate.type Result<T, E extends Error> =
| { readonly _tag: "ok"; readonly value: T }
| { readonly _tag: "err"; readonly error: E };
Prefer:
Promise<Result<User, UserLookupError>>
not:
Promise<User> // rejects for ordinary lookup/storage failures
Promise rejection is equivalent to throwing. Catch unclassified third-party rejection inside the owning Adapter and translate it into a known tagged error before it crosses the Adapter boundary. Rejection may escape application code only for a defect.
Throw or panic only when a defect makes correct execution impossible, not merely because the current caller has no recovery strategy. Defects include:
notYetImplemented pathsKnown configuration failures are values; 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.
Expected failures should use custom tagged errors, generally extending:
ErrorTaggedError from better-resultSchema.TaggedErrorClass in Effect codebasesCustom errors should include:
cause: unknownExample:
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.
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:
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.
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 inputmakeX(...) / createX(...) for smart constructors from already-typed piecesisX(value): value is X for true predicatesassertX(...) rarely, mostly at tests/framework boundariesAvoid validateX when the function returns a refined value. It parsed something.
Use schema libraries as boundary parsers, not as ad-hoc validators sprinkled through core logic.
Preference:
Schema parsing should produce refined/domain types and typed custom errors where practical.
Use branded/refined types when they prevent realistic misuse or invalid construction, especially for:
UserId, OrgId, WorkflowIdEmailAddress, NonEmptyString, UrlPositiveInt, Cents, PercentageMilliseconds, Bytes, UsdCentsConstruct 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.
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;
Domain Module, Application Service Module, and Adapter Module name responsibilities, not required folders, suffixes, or TypeScript constructs. A module may be a function, object, class, file, or package with a cohesive public interface. Use the roles at any scale; do not create three layers when the behavior does not need them.
The normal dependency and call flow for an operation with application policy or effects is:
external input -> inbound Adapter -> Application Service -> Domain Module
|
+-> application-owned port
-> outbound Adapter -> external system
An inbound Adapter may call a Domain Module directly only for a pure operation with no authorization, application policy, persistence, external calls, or effect sequencing:
external input -> inbound Adapter -> Domain Module
The composition root constructs concrete Adapters and supplies them to Application Services. Dependencies point inward: Domain Modules know neither services nor Adapters; Application Services know application-owned port contracts, not concrete technologies; Adapters depend on those contracts and translate at the edge.
Classify code by the responsibility that would make it change:
Split an abstraction when it owns more than one of these reasons to change. Do not split code merely to satisfy the taxonomy: a pure operation may need only a Domain Module, while a simple boundary may call an Application Service with no new domain type.
For a new feature or a local refactor:
Apply these responsibilities inside the project's existing layout and framework vocabulary. Migrate mixed code only across the feature's required semantic surface; otherwise contain the old convention at an Adapter seam rather than forcing a broad rewrite.
For example, in password reset: EmailAddress and ResetToken are Domain Modules; PasswordReset is the Application Service; an HTTP route is an inbound Adapter; Postgres and email-provider implementations are outbound Adapters; bootstrap performs the wiring.
A deep module hides substantial behavior, invariants, policy, sequencing, or translation behind a cohesive, low-burden interface. Low-burden does not necessarily mean few functions.
Avoid shallow abstractions that merely forward calls, mirror tables, rename another API, or expose implementation steps.
Use the deletion test:
A Domain Module is a pure, type-centric abstract data type in the OCaml tradition. It centers one primary domain type or tightly related type family and owns what values mean and which operations are legal.
Use one when the code has a meaningful domain distinction, invariant, calculation, decision, or lifecycle. Keep a primitive or local pure function when introducing a domain abstraction would prevent no realistic misuse and centralize no meaningful rule.
A Domain Module should:
It may define pure permission decisions over parsed domain values. It should not authenticate callers, gather authorization context, enforce permissions while carrying out an application operation, choose effect order, query storage, call a network, or expose transport/persistence DTOs. Callers use its operations instead of recreating its checks or branding values with casts.
Example:
// email-address.ts
/** A parsed, normalized email address. */
export type EmailAddress = Brand<string, "EmailAddress">;
/** Parse an email address from untrusted input. */
export function parse(input: string): Result<EmailAddress, InvalidEmailAddress>;
/** Render an email address as a string. */
export function toString(email: EmailAddress): string;
/** Compare two email addresses for equality. */
export function equals(left: EmailAddress, right: EmailAddress): boolean;
Domain Modules may use plain functions, immutable value classes, or static-style classes when cohesive. If using classes:
parse / make / smart constructorsAn Application Service Module owns one cohesive application operation or capability, such as PasswordReset, Invitations, or SubscriptionLifecycle. It applies application policy and sequences effects through narrow, application-owned ports while delegating intrinsic business rules to Domain Modules.
Use one when an operation must coordinate authorization, domain decisions, persistence, external calls, transactions, messages, time, IDs, or telemetry—or when the same operation must be callable from multiple entrypoints. A direct Domain Module call is enough when no application policy or effect orchestration exists.
An Application Service should:
It should not parse protocol envelopes, render responses, execute SQL, translate vendor DTOs, or duplicate Domain Module invariants. Prefer constructor injection for dependency-bearing classes; in Effect codebases, use services/tags/layers. Avoid dependency bags passed into every call.
There is no arbitrary method limit. Split methods that represent unrelated capabilities, change for different reasons, or require unrelated dependencies. Avoid vague names such as Manager, Processor, Helper, or generic UserService unless established by the project.
An Adapter Module owns one boundary's translation and technology mechanics. Use one whenever application code crosses a framework, protocol, serialization, process, persistence, runtime, or third-party boundary.
There are two directions:
An Adapter should own schema/DTO translation, framework lifecycle, external error classification, and safe diagnostics for its boundary. It may retry a short-lived technical failure only when the operation is safely repeatable and the retry does not change the port's meaning. It should not decide business eligibility, authorization policy, legal state transitions, or application-operation ordering. Keep raw external types inside the Adapter or composition root.
A port is not an Adapter. A port is the application-owned contract that states what an operation needs; an outbound Adapter is one replaceable implementation. Do not add an Adapter that only forwards the same shape to another internal module without hiding real translation or mechanics.
The composition root parses environment and configuration, acquires resources, constructs concrete Adapters, and injects them into Application Services. Keep framework bindings and concrete wiring here; do not turn the composition root into a place for domain rules, application policy, or reusable boundary translation.
Define ports beside the Application Service that needs them and in the application's language, not the provider's language. Depend on the smallest meaningful capability the operation uses; let a cohesive concrete Adapter be wider. Port inputs, outputs, and errors must be application/domain types rather than raw rows, SDK objects, or framework values.
Because TypeScript is structurally typed, this works well:
type UsersForPasswordReset = {
findActiveByEmail(email: EmailAddress): Promise<Result<ActiveUser, UserLookupError>>;
};
export class PasswordReset {
constructor(private readonly users: UsersForPasswordReset) {}
}
A wider adapter can satisfy it:
export class PostgresUsers {
findActiveByEmail(...) { ... }
findById(...) { ... }
updateProfile(...) { ... }
}
This avoids both mega-repositories and one-method adapter sprawl.
Before creating a new adapter or service, agents must audit existing adapters/services.
Prefer, in order:
Do not require an ADR for a routine feature-level Adapter or Application Service. Create an ADR when the new module introduces a lasting architectural boundary, shared pattern, provider strategy, or deliberate exception to these standards. The ADR should explain:
Avoid repository-per-table by default.
Repository-like adapters are acceptable when they represent a cohesive domain persistence capability. They should expose meaningful domain operations and return parsed domain types / typed errors, not raw rows and ORM errors.
Treat raw database rows and ORM models as infrastructure DTOs. Parse them before application/core logic. Keep SQL/ORM details inside infrastructure adapters or persistence modules.
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:
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 described above. 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.
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:
Retrying should not rely on “probably safe” side effects.
Add an end-to-end test whenever the behavior can be exercised through its real public entrypoint in the normal test environment without unreliable third parties or unreasonable setup, runtime, or cost. Add lower-level tests when they provide extra coverage for important cases.
Prefer confidence-oriented tests:
Never use vi.mock or jest.mock for module mocking. Use real seams:
Prefer tests that assert observable input/output behavior:
Avoid spy-driven tests like expect(sendEmail).toHaveBeenCalledWith(...) unless the interaction itself is the only observable behavior.
For persistence behavior, prefer SQLite/local DB-backed tests over hand-rolled in-memory fakes when SQL/schema/transaction behavior matters.
Use fast-check where properties are clearer than examples, especially for:
Use arbitraries for mock/test data generation. Prefer exporting arbitraries near the domain module they support:
src/billing/
invoice-number.ts
invoice-number.test.ts
invoice-number.arbitrary.ts
Tests should not bypass parsers, smart constructors, or invariants.
Use strict TypeScript settings where practical:
strict: truenoUncheckedIndexedAccess: trueexactOptionalPropertyTypes: truenoImplicitOverride: truenoFallthroughCasesInSwitch: truePrefer immutable values:
type CreateUserInput = {
readonly email: EmailAddress;
readonly roles: ReadonlyArray<Role>;
};
Mutation is acceptable inside localized imperative shell code, performance-sensitive internals, builders, or adapters when hidden behind a precise interface.
any, and non-null assertionsAvoid:
any!)as Typeas const is fine.
Rare exceptions are allowed for highly generic helpers, branding internals, interop boundaries, or combinators where TypeScript cannot express the invariant.
Any non-as const cast requires a Rust-like safety comment:
// SAFETY: TypeScript cannot express the brand. parseEmailAddress checked the normalized string before branding. Callers cannot construct EmailAddress except through this parser.
return normalized as EmailAddress;
Rare any also requires a targeted oxlint ignore and justification:
// oxlint-disable-next-line no-explicit-any -- SAFETY: This helper preserves arbitrary function parameters; TypeScript cannot express this variadic constraint without any.
type Fn = (...args: any[]) => unknown;
Do not use !. Branch, parse, or refine instead.
Prefer direct imports from the file that owns the abstraction. Avoid barrel files / index.ts re-export layers by default.
For domain modules, namespace imports often preserve the module shape:
import * as EmailAddress from "./email-address";
EmailAddress.parse(input);
Use named imports for classes and focused shared helpers:
import { PasswordReset } from "./password-reset";
Use import type / export type for type-only imports and exports.
Export only what callers should use. Keep internal helpers unexported unless intentionally shared. Do not export internals just for tests.
Avoid TypeScript namespace unless there is a compelling interop reason.
Avoid vague files:
utils.ts
helpers.ts
common.ts
misc.ts
Use precise names:
email-address.ts
billing-period.ts
string-case.ts
array.ts
Tiny ubiquitous generic helpers/types may share one explicit module when no more precise owner exists. Appropriate contents include:
casesHandledshouldNeverHappennotYetImplementedRedactedTags, ExtractTag, and ExcludeTagResult helpers when the project uses neither Effect nor better-resultKeep only helpers justified by the target project. Keep domain and application policy with their owning modules.
No arbitrary file-size limits. Prefer cohesion and discoverability over small files for their own sake. Split when a file has multiple unrelated reasons to change or callers must understand unrelated concepts.
Comments should explain invariants, trade-offs, non-obvious domain rules, and safety justifications. Avoid comments that narrate obvious code.
Every exported symbol from a JavaScript or TypeScript module requires JSDoc. Public methods and properties of an exported class also require JSDoc. Private and otherwise internal code requires documentation only when its complexity warrants it. Put documentation on the original declaration; re-exports do not need duplicate documentation.
Do not use @inheritDoc, @inherit, or similar inheritance tags. Write the required documentation explicitly on each symbol or member.
Use standard JSDoc syntax:
/**
* Parse an email address from untrusted input.
*
* @param input - The untrusted string to parse.
* @returns A parsed email address, or `InvalidEmailAddress` when the input is invalid.
*/
export function parse(input: string): Result<EmailAddress, InvalidEmailAddress>;
For generics:
/**
* Map the success value of a result.
*
* @template T - The original success type.
* @template U - The mapped success type.
* @template E - The error type.
* @param result - The result to map.
* @param fn - The function applied to the success value.
* @returns A result with the mapped success value, or the original error.
*/
export function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E>;
Use @throws only for unrecoverable defects, framework-required behavior, or temporary notYetImplemented paths. Do not document expected typed errors as throws.
For complex exported object types, document fields when helpful:
/** Input required to create a user. */
export type CreateUserInput = {
/** The actor creating the user. */
readonly actor: AdminUser;
/** The parsed email address for the new user. */
readonly email: EmailAddress;
};
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.
Before coding:
Partial<T> in core/application logic.fast-check arbitraries for generated test data when practical.