title: Effect Service Conventions model: gpt-6-sol effort: max input: incremental tools:
Review changed TypeScript for the conventions below. They apply when a pull request creates, moves, refactors, or consumes an Effect service, or adds server behavior. Authors read the same rules in docs/internals/effect-services.md; keep the two in step. Review only the lines the PR changed; older code in the same file that predates these conventions is not a finding. Do not demand repository-wide cleanup.
import * as Effect from "effect/Effect", import * as Layer from "effect/Layer". Flag named imports from the bare "effect" package.WorkspacePaths.WorkspacePaths, WorkspacePaths.make, WorkspacePaths.layer. Flag aliases such as import { layer as workspacePathsLayer } that erase the namespace.@t3tools/contracts and for modules used only for a pure helper, error, schema, config value, or type. Do not request import type * as Contracts.export * as TokenStore from "./tokenStore.ts" over individually renamed make and layer exports.apps/server/src/ws.ts, an HTTP route handler, or an MCP tool handler. A handler decodes input, calls one service method, and maps the service's errors to the transport's error. Filesystem, Git, process, or persistence work, folder naming, multi-step command dispatch, retries, or rollback inside a handler belongs in a service method; ask for it to move to the domain's existing service, or a new one when none owns the domain. Existing inline handlers are legacy; flag only changed lines.Context.Service tag with its interface inline, make, then layer.Context.Service. Do not add a standalone FooShape interface; refer to the inferred type as Foo["Service"].make when the module owns construction, and export it only when another module imports it; knip fails CI on an unused export, so do not ask for an export nothing uses. Do not write make = Effect.succeed(...) only to force Layer.effect; use Layer.succeed, Layer.scoped, or whichever constructor matches.make and layer in a module named for its implementation (BunPtyAdapter.ts). Keep implementation-specific names when one abstract port module holds several implementations (makeCloudflaredRelayClient, layerCloudflared in RelayClient.ts). infra/relay/src/db.ts may keep its inline Layer.succeed(RelayDb, db).yield* Foo.Foo, and make/layer types expose those requirements. Flag a factory that takes Foo["Service"] (or an object of Effect-returning methods) as a parameter when that value is a service dependency. Passing service instances explicitly in tests is fine; passing pure configuration, immutable domain values, or deliberate callback strategies is not service injection.Layer.succeed whose implementation calls runtime-backed or imperative APIs.ManagedRuntime.make, runPromise, and runPromiseExit belong at application or framework boundaries: React, native callbacks, CLI, HTTP adapters. Flag them in domain services, repositories, persistence, and service constructors. A named imperative adapter may bridge an Effect service into a Promise API but must not become a dependency of another Effect service.Schema.TaggedError and structured attributes: operation or stage, resource path or entity identifier, normalized category or status. Derive message from those attributes only. Never derive it from cause, cause.message, or a stringified defect, and do not add a detail field that copies cause.message.cause so the chain and stack survive. Make cause required if every construction wraps a failure. Pure validation or domain errors created without an underlying failure need no cause.cause; expose normalized categories, lengths, counts, and safe URL protocol or hostname where useful.operation, reason, kind, or phase literal. Split into separate error classes when a discriminator drives caller control flow or the user-facing message; a discriminator used only for diagnostics may stay a field. Caller-visible messages exposed through HTTP, RPC, persisted state, or UI are behavior and must survive a structural refactor.(...args) => new SomeError({ ...args }). Construct the error at the failure boundary. Keep a mapper only when it performs real normalization, passes through domain errors, or adds reusable context; when such a mapper belongs to the target error type, prefer a static factory on that class.export const isFoo = Schema.is(Foo). Flag a private Schema.is constant wrapped by a function with the same signature.Effect.catchTags({ ... }), including for a single tag; do not use catchTag or catchIf with a schema predicate for that. Effect.catch is fine when the whole error channel is handled; catchIf is fine for structural predicates such as a platform error code.Layer.effect, universal namespace imports, generic make/layer names for abstract-port implementations, or separate error classes for diagnostic-only fields.Report only violations introduced by changed lines. Post each as a precise inline comment on the smallest relevant range and state the expected fix. A clear convention violation may fail the check; optional style preferences and untouched legacy code may not.
When there are no findings, make the entire final response exactly All clear on one line with nothing else.