@opsydyn/oxlint-effect
v1.1.0
Published
Oxlint plugin rules for Effect TypeScript code shape constraints.
Readme
linteffect Oxlint plugin
Oxlint plugin rules for Effect TypeScript code-shape constraints.
Install
bun add -d oxlint @opsydyn/oxlint-effectConfigure
import { defineConfig } from "oxlint";
import { recommended } from "@opsydyn/oxlint-effect";
export default defineConfig({
plugins: ["typescript"],
jsPlugins: [...recommended.jsPlugins],
rules: recommended.rules,
});recommended.jsPlugins is exported as a readonly tuple. Spreading it creates
the mutable array shape expected by Oxlint's ExternalPluginEntry[] config
type.
Configure Type-Aware Linting
typeAware is an opt-in configuration bridge to Oxc's type-aware
linting. It preserves
the package's recommended syntax-only linteffect/* rules and enables Oxc's
type-aware engine, but it does not select any typescript/* rules or enable
typeCheck compiler diagnostics.
Install the type-aware engine in the consuming project:
bun add -d oxlint oxlint-tsgolint @opsydyn/oxlint-effectThe current engine requires TypeScript 7. Keep oxlint, oxlint-tsgolint,
TypeScript, and the consumer's project configuration compatible. In a
monorepo, build dependent packages so their declarations can be resolved
before type-aware linting runs.
import { defineConfig } from "oxlint";
import { typeAware } from "@opsydyn/oxlint-effect";
export default defineConfig({
options: typeAware.options,
jsPlugins: [...typeAware.jsPlugins],
plugins: ["typescript", "unicorn", "oxc"],
rules: {
...typeAware.rules,
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "error",
},
});options.typeAware must be at the root of the resolved Oxlint configuration;
do not place it in an override or nested configuration object. The package
uses only that option. The two typescript/* entries above are selected by the
consumer; typeAware.rules itself does not include them. Consumers may also
separately opt into compiler diagnostics:
oxlint --type-awareexport default defineConfig({
options: {
typeAware: true,
typeCheck: true,
},
});The CLI enables the same type-aware engine without this package preset. The
second configuration is consumer-owned and is not part of typeAware.
The package's custom Effect rules remain syntax-only. Oxc's JavaScript plugin API does not support custom type-aware rules, so semantic Effect rules remain deferred until Oxc provides supported typed-plugin access.
Configure One Rule Group
Every named preset in the following table is exported as a config-shaped preset
with the same shape as recommended; use the same mutable-array workaround
for each one. For example, the ddd preset combines Domain Modeling and Error
Modeling rules:
import { defineConfig } from "oxlint";
import { ddd } from "@opsydyn/oxlint-effect";
export default defineConfig({
plugins: ["typescript"],
jsPlugins: [...ddd.jsPlugins],
rules: ddd.rules,
});Named group presets:
| Preset | Rule Group |
| --- | --- |
| reactAndRuntimeBoundaries | React and Runtime Boundaries |
| effectComposition | Effect Composition |
| concurrencySafety | Concurrency Safety |
| resourceLifetime | Resource Lifetime |
| pipelineShapeAndSequencing | Pipeline Shape and Sequencing |
| branchingAndLocalControlFlow | Branching and Local Control Flow |
| optionMatchAndDataNormalization | Option, Match, and Data Normalization |
| atomStateAndPlatformBoundaries | Atom, State, and Platform Boundaries |
| domainModeling | Domain Modeling |
| errorModeling | Error Modeling |
| ddd | Domain Modeling and Error Modeling |
| effectFlow | Effect Flow |
| pureTransformation | Pure Transformation |
| behaviorDecoration | Behavior Decoration |
| styleSeparation | Style Separation |
| serviceAndLayerArchitecture | Service and Layer Architecture |
| platformAndBoundaryHygiene | Platform and Boundary Hygiene |
| testingObservabilityAndQa | Testing, Observability, and QA |
Each preset also has a rule-only export with a Rules suffix. Use those when
you want to compose multiple groups. typeAware is the only new mode in this
slice; compose it with rule-only exports when a type-aware configuration needs
additional Effect policy:
import { defineConfig } from "oxlint";
import {
typeAware,
domainModelingRules,
errorModelingRules,
} from "@opsydyn/oxlint-effect";
export default defineConfig({
options: typeAware.options,
jsPlugins: [...typeAware.jsPlugins],
plugins: ["typescript", "unicorn", "oxc"],
rules: {
...typeAware.rules,
...domainModelingRules,
...errorModelingRules,
},
});For local development inside this repository, point jsPlugins at the TypeScript source:
import { defineConfig } from "oxlint";
import { allRules } from "./src/index";
export default defineConfig({
plugins: ["typescript"],
jsPlugins: [{ name: "linteffect", specifier: "./src/index.ts" }],
rules: allRules,
});Rule Groups
The recommended config enables the broadly applicable rules as errors. Strict groups can add more opinionated checks where a team has adopted the associated workflow. The rules are heuristic: they flag code shapes that tend to hide Effect flow, domain meaning, or runtime boundaries.
React and Runtime Boundaries
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-react-state | React state hooks such as useState, useReducer, and useEffect. | Keeps React UI state in the atom/runtime model instead of bypassing it. |
| linteffect/no-runtime-runfork | Runtime.runFork(...). | Detached fibers hide ownership, interruption, and lifecycle boundaries. |
| linteffect/no-run-effect-outside-boundary | Direct Effect.runPromise, Effect.runSync, Effect.runFork, and related Effect.run* calls. | Keeps Effect execution owned by app, CLI, worker, route, or test boundaries. |
| linteffect/no-or-die-outside-boundary | Effect.orDie(...), Effect.orDieWith(...), and pipe arguments such as Effect.orDie. | Prevents recoverable typed failures from being converted to defects inside domain logic. |
| linteffect/prevent-dynamic-imports | Dynamic import(...). | Static imports keep dependency boundaries visible. |
| linteffect/no-render-side-effects | Match.value(...).pipe(...) used as a render-time statement. | Prevents side effects from running during render. |
| linteffect/no-inline-runtime-provide | Inline Effect.provide(...) inside local runtime/generator chains. | Keeps dependency assembly at service or application boundaries. |
Effect Composition
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-effect-as | Direct Effect.as(...) wrappers. | Makes value flow explicit instead of discarding meaning behind a placeholder. |
| linteffect/no-effect-do | Effect.Do. | Avoids builder-style hidden sequencing. |
| linteffect/no-effect-bind | Effect.bind(...). | Prefers direct Effect.gen or pipeline flow over builder state. |
| linteffect/no-effect-async | Effect.async(...). | Manual callback bridges are easy to leak or resume incorrectly. |
| linteffect/no-effect-ignore | Effect.ignore(...) and pipe arguments such as Effect.ignore. | Makes ignored failures explicit at boundaries instead of burying failure ownership. |
| linteffect/no-effect-never | Effect.never. | Infinite effects should have explicit lifecycle and teardown ownership. |
| linteffect/no-effect-fn-generator | Effect.fn(function* ...). | Avoids wrapper generators that obscure sequencing. |
| linteffect/no-nested-effect-gen | Effect.gen nested inside another Effect.gen. | Keeps generator-based effects linear. |
| linteffect/no-yield-without-star-in-effect-gen | Plain yield inside Effect.gen. | Requires yield* so generator steps delegate to the Effect interpreter. |
| linteffect/no-async-effect-combinator-callback | async callbacks passed to common Effect combinators. | Prevents Promise-returning callbacks from bypassing Effect error, interruption, and tracing semantics. |
| linteffect/no-throw-in-effect-logic | throw inside Effect.gen or common Effect combinator callbacks. | Keeps failures in typed Effect error channels. |
| linteffect/no-try-catch-in-effect-logic | try/catch inside Effect.gen or common Effect combinator callbacks. | Uses Effect error combinators instead of local imperative recovery. |
| linteffect/no-promise-api-in-effect-logic | Promise.all, Promise.race, .then, .catch, and related Promise APIs inside Effect logic. | Keeps scheduling, cancellation, tracing, and failures inside Effect. |
| linteffect/no-swallowed-catch-all | Effect.catchAll handlers that recover with Effect.succeed, Effect.void, Effect.asVoid, or Effect.ignore. | Avoids hiding failures without telemetry, re-fail, or explicit typed recovery. |
| linteffect/no-manual-effect-channels | Manual Effect.Effect<...> and Layer.Layer<...> channel types. | Lets Effect infer channels from real composition. |
| linteffect/no-effect-type-alias | Type aliases around Effect.Effect<...>. | Keeps service surfaces concrete and discoverable. |
| linteffect/no-public-generic-effect-error | Exported APIs returning Effect.Effect<_, Error, _>. | Public Effect APIs should expose tagged, recoverable domain errors instead of generic Error. |
Effect Flow
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-piped-yield-in-gen | Two or more yield* effect.pipe(...) steps inside one Effect.gen. | Keeps decorated effects named before the workflow so generator bodies read as a clear story. |
| linteffect/no-gen-for-mapping | Tiny Effect.gen blocks that yield once and return a pure transform. | Simple value mapping belongs in Effect.map or a named pure transformation, not workflow syntax. |
| linteffect/prefer-gen-for-workflow | Pipelines with three or more sequencing combinators such as Effect.flatMap, Effect.andThen, Effect.tap, or Effect.zipRight. | Long sequencing pipelines read like imperative workflow; Effect.gen makes the happy path explicit. |
| linteffect/no-business-logic-in-pipe | Strict: .pipe(Effect.flatMap(...)) callbacks with branching, service retrieval, or multiple Effect steps. | Keeps workflow decisions in Effect.gen and reserves pipe for behavior around a completed effect. |
Pure Transformation
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-large-anonymous-flow | flow(...) expressions with five or more transformation steps. | Large pure pipelines need a domain name so the transformation is reusable and reviewable. |
| linteffect/no-effect-in-flow | Effect.*, yield, await, async callbacks, console calls, Promise, or runtime access inside flow(...). | flow() should stay pure; effectful workflow, retries, logging, and dependency access belong in Effect code. |
| linteffect/prefer-named-flow | Non-trivial flow(...) expressions passed inline as callback/combinator arguments. | Naming the transformation makes DTO mapping and business calculations explicit instead of anonymous callback logic. |
| linteffect/prefer-flow-for-pure-pipeline | Strict: pure nested call towers three calls deep or more. | Names a reusable transformation pipeline instead of burying data flow in nested calls. |
Behavior Decoration
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/prefer-pipe-for-behavior | Static behavior decorators such as Effect.retry(Effect.succeed(...), policy). | Retry, timeout, spans, logging, recovery, DI, and value transforms should read as behavior around an existing effect. |
| linteffect/prefer-decorated-effect-before-gen | Two or more decorated yield* service.pipe(Effect.retry(...)) / yield* effect.pipe(Effect.withSpan(...)) steps inside one Effect.gen. | Generator bodies should tell the workflow story; behavior policy should be named before the workflow. |
| linteffect/no-workflow-in-behavior-pipe | Pipes that mix behavior decorators with multiple workflow sequencing operators or embedded control flow. | .pipe() should answer how an effect behaves, not bury multi-step workflow that belongs in Effect.gen. |
Style Separation
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-mixed-pillar-function | Functions that mix three or more style pillars: workflow, pure transformation, behavior decoration, and Layer construction. | Each function should have one obvious style so the domain story, policies, transformations, and wiring stay separately reviewable. |
| linteffect/no-clever-effect-expression | Deep or wrapper-heavy expressions combining multiple style pillars, such as pipe(Effect.map(Effect.gen(...), flow(...)), ((x) => x)). | Dense expression towers hide intent and make Effect code harder to debug or refactor. |
| linteffect/prefer-extracted-concept | Multi-statement anonymous callbacks passed into Effect combinators. | Inline callback bodies with several steps usually represent a named transformation, policy, or workflow concept. |
Service and Layer Architecture
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/prefer-effect-service | Context.Tag(...) and Context.GenericTag(...) service definitions. | Modern Effect.Service gives services generated accessors, consistent default layers, and clearer dependency ownership. |
| linteffect/no-layer-provide-in-service-definition | Layer.provide(...) nested inside an Effect.Service options object. | Service definitions should declare implementation and dependencies; layer assembly belongs at application, test, or composition boundaries. |
| linteffect/require-service-accessors | Effect.Service classes whose options omit accessors: true. | Static accessors keep service APIs consistent and avoid hand-written dependency plumbing. |
| linteffect/require-service-dependencies | Effect.Service implementations that yield* SomeService without a dependencies option. | Service dependency graphs should be declared where the service is defined. |
| linteffect/no-namespace-effect-import | import * as ... from "effect" and other Effect package namespace imports. | Direct named imports keep the Effect surface explicit and easier to scan. |
| linteffect/no-manual-service-object-export | Exported *Service object literals with function-valued members. | Public service APIs should use Effect.Service for accessors, default layers, and dependency ownership. |
| linteffect/no-layer-merge-in-request-handler | Layer.merge* or Layer.provide inside request/route/handler functions. | Request handlers should run programs, not assemble the application dependency graph. |
| linteffect/no-service-method-returning-promise | Methods returned from Effect.Service implementations that return Promise. | Service APIs should preserve Effect cancellation, tracing, typed failures, and dependency semantics. |
| linteffect/prefer-layer-pipe | Nested Layer.provide(...) call towers. | Layer assembly should read as a left-to-right composition pipeline. |
| linteffect/no-inline-layer-provide-in-program | Effect.provide(...) or Layer.provide(...) buried inside Effect.gen program bodies. | Programs should describe workflow; application layer provisioning belongs at composition boundaries. |
| linteffect/prefer-layer-mergeall-for-infrastructure | Nested Layer.merge(...) chains. | Infrastructure groups should use Layer.mergeAll(...) so dependency groups stay visible. |
| linteffect/no-service-layer-scatter | Three or more separate *Layer/*Live constants with inline Layer.provide or Effect.provide. | Service and infrastructure layers should be grouped by concern instead of scattered one constant at a time. |
Platform and Boundary Hygiene
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-node-fs-in-effect-code | fs, node:fs, fs/promises, and node:fs/promises imports and module-scope require() calls in Effect modules. | Effect code should stay portable and move Node filesystem access behind a platform boundary. |
| linteffect/no-json-parse-without-schema | JSON.parse(...) in Effect modules without an explicit Effect Schema import. | External JSON must be decoded through a schema at the boundary rather than trusted as an unvalidated value. |
| linteffect/no-date-now-in-effect | Date.now() within supported Effect construction boundaries. | Time should be supplied through Effect's Clock services so workflows remain deterministic and testable. |
| linteffect/no-new-date-in-domain-logic | new Date(...) in Effect-importing modules outside configured runtime boundaries. | Domain code should receive time through Clock or a modeled input rather than constructing a wall-clock value directly. |
| linteffect/no-node-platform-in-shared-code | Node built-in imports, including node:* and bare built-in module names, outside configured boundary paths. | Shared modules should remain portable and obtain platform capabilities through services or explicit application boundaries. |
| linteffect/no-process-env-direct-read | Direct and computed process.env reads outside configured boundary and configuration paths. | Environment values should be decoded once in a configuration service or Layer rather than read as ambient state. |
| linteffect/no-hidden-effect-execution | Direct Effect.run* calls in Effect modules outside configured boundary paths. | Reusable code should return Effects and leave runtime execution ownership at an application boundary. |
| linteffect/no-boundary-try-catch-without-effect-map | try/catch blocks in configured boundaries with no direct Effect.try, error mapping, recovery, or Effect.run* call. | Boundary failure handling should stay in Effect's typed error channel rather than becoming imperative control flow. |
Testing, Observability, and QA
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-console-in-effect-flow | console.* inside direct Effect.gen, Effect.sync, Effect.try, Effect.tryPromise, or Effect.fn callbacks, and Effect.Service implementations. | Logging through Effect preserves the runtime's observability context. |
| linteffect/no-effect-log-without-structured-context | String-only Effect.logError and Effect.logWarning calls in direct error-handler callbacks or Effect.Service implementations. | Failure logs need an error, structured fields, or local Effect.annotateLogs(...) context for correlation. |
| linteffect/require-span-on-public-service-method | Exported functions or function-valued variables with an explicit Effect.Effect return (on the function or variable declaration), plus Effect.Service methods directly returning an Effect, when any direct Effect return lacks Effect.withSpan(...). | Public operations need visible trace boundaries. |
| linteffect/no-runpromise-in-non-async-test-body | Discarded direct Effect.runPromise(...) calls in *.test.*, *.spec.*, and __tests__ files. | Tests must await or return runtime execution so the framework observes completion. |
| linteffect/require-effect-flip-for-error-test | Direct await expect(Effect.runPromise(effect)).rejects... assertions in conventional test files. | An expected typed Effect failure is clearer when Effect.flip yields the error as a value for structural assertions. |
| linteffect/no-test-mock-layer-when-default-available | A direct Layer.succeed(...) or Layer.effect(...) sibling of SomeService.Default in the same Layer.provide(...) call. | An explicit default composition and a sibling replacement layer can hide which service contract the test exercises. |
These rules are deliberately syntax-only. They require an Effect ecosystem import;
they do not resolve aliases, infer Effect return types, or follow values through
variables. require-span-on-public-service-method accepts either data-first
Effect.withSpan(program, "operation") or .pipe(Effect.withSpan("operation")).
The three test-shape rules are strict opt-in checks: use
testingObservabilityAndQa when a repository follows this test style; they are
not in recommended. For an expected typed failure, the preferred pattern from
EffectPatterns service-test guidance
is to apply Effect.flip,
execute the flipped Effect, and assert the returned error's _tag, message,
and structured fields. Effect.flip moves the expected typed failure into the
success channel, making the assertion explicit. This rule deliberately matches
only a direct expect(Effect.runPromise(...)).rejects shape; ordinary
JavaScript rejection assertions, aliases, helpers, and other promise chains are
out of scope.
Configure path-sensitive rules independently when a repository uses different application and configuration boundaries:
import { defineConfig } from "oxlint";
import { platformAndBoundaryHygiene } from "@opsydyn/oxlint-effect";
export default defineConfig({
plugins: ["typescript"],
jsPlugins: [...platformAndBoundaryHygiene.jsPlugins],
rules: {
...platformAndBoundaryHygiene.rules,
"linteffect/no-node-platform-in-shared-code": [
"error",
{ boundaryPaths: ["apps/**", "server/**"] },
],
"linteffect/no-process-env-direct-read": [
"error",
{
boundaryPaths: ["apps/**", "server/**"],
configPaths: ["packages/config/**"],
},
],
"linteffect/no-hidden-effect-execution": [
"error",
{ boundaryPaths: ["apps/**", "server/**"] },
],
"linteffect/no-new-date-in-domain-logic": [
"error",
{ boundaryPaths: ["apps/**", "server/**"] },
],
"linteffect/no-boundary-try-catch-without-effect-map": [
"error",
{ boundaryPaths: ["apps/**", "server/**"] },
],
},
});Concurrency Safety
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-unbounded-effect-all | Effect.all(items.map(...)) without an explicit concurrency option. | Prevents load-dependent runaway parallelism and makes throughput ownership explicit. |
| linteffect/no-fire-and-forget-fork | Bare Effect.fork(...) expression statements. | Detached fibers hide failure, interruption, and lifecycle ownership. |
| linteffect/no-fork-in-loop | Effect.fork(...) inside for, for...of, for...in, while, or do...while loops. | Avoids loop-spawned unbounded fibers; use bounded Effect.all / Effect.forEach or scoped supervision. |
| linteffect/no-race-without-cleanup | Effect.race(...) / Effect.raceAll(...) without Effect.ensuring, scoped, or acquire/release cleanup. | Racing effects need explicit loser cleanup so losing work and resources do not leak. |
| linteffect/no-unobserved-fiber | const fiber = Effect.fork(...) when the fiber is never passed to Fiber.join, Fiber.await, or Fiber.interrupt. | Forked fibers should have observed failure and interruption ownership. |
| linteffect/no-unbounded-concurrent-retry | Effect.retry(...) nested inside unbounded mapped Effect.all(...) or unbounded Effect.forEach(...). | Prevents retry storms by requiring bounded concurrency or a queue/backoff policy. |
| linteffect/no-blocking-call-in-effect | Sync fs / crypto / zlib calls inside Effect.sync or Effect.gen. | Blocking calls stall the runtime worker and should live behind async/platform boundaries. |
| linteffect/no-promise-concurrency-in-effect | Promise.all, Promise.allSettled, Promise.race, or Promise.any inside Effect logic. | Keeps concurrency, interruption, tracing, and typed failures inside Effect. |
| linteffect/no-shared-mutable-state-across-fibers | Outer let / var state mutated from Effect.fork, Effect.all, or Effect.forEach work. | Shared mutable state across fibers creates nondeterministic races; use Effect concurrency primitives. |
| linteffect/no-timeout-with-noninterruptible-promise | Effect.timeout(Effect.promise(...)) or Effect.timeout(Effect.tryPromise(...)) without a signal-aware callback. | Timeout should interrupt the underlying async operation, not only the Effect wrapper. |
| linteffect/no-uninterruptible-concurrent-region | Effect.uninterruptible(...) wrapping fork, race, all, forEach, or queue-taking work. | Broad uninterruptible concurrent regions block cancellation and can strand work during shutdown. |
| linteffect/no-unbounded-queue-or-pubsub | Queue.unbounded() and PubSub.unbounded(). | Unbounded buffers hide backpressure and can fail under load; capacity should be owned explicitly. |
| linteffect/no-global-mutable-concurrency-state | Module-level mutable state or mutable containers touched from concurrent Effect work. | Global mutable state under concurrency behaves like shared memory; move ownership into Effect primitives or layers. |
| linteffect/no-yield-with-held-semaphore-permit | Direct semaphore withPermit / withPermits work whose effect contains sleep, await, Promise interop, queue waiting, or concurrent Effect work. | Strict-only protection against holding a permit while interruptible or concurrent work occupies the critical section. |
| linteffect/no-yield-with-held-mutable-ref | Effectful SynchronizedRef modifiers (modifyEffect, modifySomeEffect, updateEffect, updateAndGetEffect) whose callback suspends or starts concurrent work. | Strict-only protection that keeps internal reference coordination short and synchronous. |
| linteffect/no-unscoped-background-fiber | Direct Effect.forkDaemon(...) without a direct Effect.supervised(...) child-effect marker. | Strict-only protection that requires daemon work to expose supervision or use scoped ownership instead of silently outliving the caller. |
| linteffect/no-manual-deferred-coordination | A local Deferred.make(...) / Deferred.unsafeMake(...) latch whose matching Deferred.await(...) has no timeout, race, interruption, scope, or finalizer protection. | Strict-only protection against unbounded waits and implicit completion ownership in ad hoc coordination. |
| linteffect/no-acquire-without-scoped-release | Resource-like open / connect / create / start / listen / subscribe / acquire calls for client, connection, pool, database, file, socket, stream, server, subscription, or handle values inside concurrent Effect work without scoped release evidence. | Keeps resources acquired by concurrent work tied to acquireRelease, acquireUseRelease, Effect.scoped, or a matching finalizer. |
Resource Lifetime
These rules use a central syntax-only resource vocabulary (client, connection,
conn, pool, db, database, file, socket, stream, server,
subscription, and handle). They require an Effect ecosystem import and
support the same boundaryPaths option as the other lifecycle rules.
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-manual-resource-close | Resource-like .close(), .destroy(), .dispose(), or .cleanup() calls outside release or finalizer callbacks. | Keeps cleanup owned by Effect scopes instead of ad hoc imperative code. |
| linteffect/no-unbound-scope | Scope.make() without Effect.scoped, Layer.scoped, explicit Scope.close, or a matching acquire/release callback. | Prevents scopes and their finalizers from being leaked. |
| linteffect/no-resource-succeed-escape | Effect.succeed(resourceLike) for client, connection, pool, file, socket, stream, server, subscription, or handle-shaped values. | Keeps live resource lifetimes inside scoped Effect ownership; this heuristic is focused-only rather than recommended. |
| linteffect/no-resource-without-acquire-release | Runtime: resource-like open / connect / create / start / listen / subscribe / acquire calls without a release owner. | Makes resource ownership explicit across failure, interruption, and shutdown. |
| linteffect/no-request-scoped-long-lived-resource | Strict: resource acquisition or construction inside request, route, endpoint, controller, or handler functions. | Keeps long-lived clients and pools in application Layers instead of multiplying them per request. |
| linteffect/no-global-resource-singleton | Strict: module-level new Client, new Pool, new Database, and similar resource-like constructors. | Keeps construction, testing, and shutdown ownership in services and Layers. |
| linteffect/no-run-with-open-resource | Runtime: Effect.run* in a lexical scope containing an unowned resource creation. | Prevents runtime execution from finishing while an imperative resource remains open. |
| linteffect/no-nested-acquire-release | Strict: Effect.acquireRelease / acquireUseRelease nesting deeper than two levels. | Encourages named Layers and manageable release boundaries instead of opaque release stacks. |
| linteffect/no-missing-layer-provision-at-run | Strict: Effect.run* on a program that yields a service tag without a local Effect.provide or Layer.provide. | Makes runtime dependency provisioning visible at the application boundary. |
Scope.global remains a deferred candidate because the supported Effect API does
not currently expose it; it is not part of the v1 export surface.
Pipeline Shape and Sequencing
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-nested-effect-call | Deeply nested Effect.xx(Effect.yy(...)) calls. | Flattens sequencing into readable pipelines. |
| linteffect/no-effect-ladder | Nested Effect combinator ladders in assignments or returns. | Avoids control flow hidden inside expression towers. |
| linteffect/no-flatmap-ladder | Nested Effect.flatMap and map plus flatten ladders. | Encourages one clear bind point after context is built. |
| linteffect/no-pipe-ladder | Nested pipe(...) or method .pipe(...) chains. | Keeps pipelines flat and scan-friendly. |
| linteffect/no-call-tower | Effect calls passed directly into other Effect calls. | Makes intermediate effects named or piped. |
| linteffect/no-effect-orElse-ladder | Effect.orElse wrapped around sequencing chains. | Keeps error handling at an explicit decision point. |
| linteffect/no-effect-wrapper-alias | Const/function aliases that only wrap Effect calls. | Discourages wrapper choreography with no domain meaning. |
| linteffect/warn-effect-sync-wrapper | Effect.sync(() => someCall()) around non-console calls. | Avoids hiding side effects behind vague sync wrappers. |
| linteffect/no-effect-side-effect-wrapper | Effect.as or Effect.zipRight around side-effecting operands. | Prevents side effects from being disguised as discarded values. |
| linteffect/no-effect-all-step-sequencing | Sequential side effects hidden in Effect.all(..., { concurrency: 1 }). | Reserves Effect.all for aggregation, not imperative step lists. |
| linteffect/no-effect-succeed-variable | Effect.succeed(variable) used as a branch placeholder. | Encourages selecting plain values before entering Effect flow. |
Branching and Local Control Flow
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-if-statement | Imperative if statements in Effect files. | Pushes branching toward typed Match/Option/Either decisions. |
| linteffect/no-switch-statement | Imperative switch statements in Effect files. | Encourages exhaustive domain matching. |
| linteffect/no-ternary | Ternary expressions in Effect files. | Keeps decisions explicit and named. |
| linteffect/no-try-catch | try/catch. | Keeps failures in typed Effect error channels. |
| linteffect/no-arrow-ladder | Nested arrow IIFEs. | Avoids local wrapper control flow. |
| linteffect/no-iife-wrapper | Immediately invoked function wrappers. | Moves decisions into named values or pipelines. |
| linteffect/no-return-in-arrow | return inside block-bodied arrow callbacks. | Prefers expression callbacks for simple pipeline steps. |
| linteffect/no-return-in-callback | return inside inline function callbacks. | Reduces hidden local control flow. |
| linteffect/no-return-null | return null in Effect files. | Uses Option.none or typed failures instead of null sentinels. |
| linteffect/no-branch-in-object | Match/Option/Either decisions inside object literals. | Computes decisions first, then builds data from named values. |
Option, Match, and Data Normalization
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-option-as | Option.as(...). | Makes selection explicit with Option.map or Option.match. |
| linteffect/no-match-void-branch | Match branches returning Effect.void. | Avoids no-op branches that hide guard-style control flow. |
| linteffect/no-match-effect-branch | Multi-step sequencing inside Match or Option branches. | Selects data in Match/Option, then runs one Effect pipeline. |
| linteffect/no-model-overlay-cast | as assertions on decoded model flow. | Avoids hiding schema drift with unchecked overlays. |
| linteffect/no-unknown-boolean-coercion-helper | Local unknown-to-boolean checks paired with null fallback matching. | Moves boolean normalization to the schema boundary. |
| linteffect/no-fromnullable-nullish-coalesce | Option.fromNullable(value ?? null) or ?? undefined. | Passes nullable sources directly without rewrapping noise. |
| linteffect/no-option-boolean-normalization | Repeated Option.match boolean normalization. | Normalizes once at the boundary and reads typed booleans later. |
| linteffect/no-string-sentinel-return | Effect.succeed("token") sentinel returns. | Uses domain values, Option/Either, or tagged unions for decisions. |
| linteffect/no-string-sentinel-const | String constants used as state/status tokens. | Avoids ad hoc string state machines. |
Atom, State, and Platform Boundaries
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-effect-sync-console | console.* inside Effect.sync. | Uses Effect.log* or a real logging boundary. |
| linteffect/no-atom-registry-effect-sync | Atom or atom registry operations wrapped in Effect.sync. | Keeps atom operations in the atom flow. |
| linteffect/no-family-collection-read | Atom.family projections that read broad collection atoms. | Keeps keyed atoms keyed instead of coupling to whole collections. |
| linteffect/no-naked-object-state-update | Raw object spreading, Object.assign, JSON rebuilds, and similar state shortcuts. | Preserves explicit model transitions and schema boundaries. |
| linteffect/no-wrapgraphql-catchall | Effect.catchAll after wrapGraphqlCall or applyResponse. | Handles GraphQL envelope errors at the response mapping boundary. |
Domain Modeling
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-raw-domain-id-alias | type UserId = string and similar raw ID aliases. | Branded IDs prevent swapped identifiers across boundaries. |
| linteffect/no-boolean-domain-flag | Boolean behavior flags such as shouldNotifyCustomer. | Replaces hidden modes with commands, tagged unions, or explicit functions. |
| linteffect/no-magic-domain-string | Raw string comparisons such as status === "approved". | Makes domain vocabularies typed and exhaustive. |
| linteffect/no-raw-domain-primitive-params | Domain-looking functions with several raw string/number params. | Introduces branded values or command objects for meaningful inputs. |
| linteffect/no-raw-time-domain-field | Time-looking fields typed as number or Date. | Models durations and clock boundaries explicitly. |
| linteffect/no-overloaded-options-object | opts, options, or config typed as any or object. | Uses Schema decoding or named config models instead of loose bags. |
| linteffect/no-domain-logic-in-conditional | Multi-clause business rules embedded in boolean expressions. | Extracts named predicates or validation Effects that can be tested. |
| linteffect/no-implicit-state-machine-object | Multiple boolean lifecycle flags on one object. | Models impossible states away with tagged unions. |
| linteffect/no-adhoc-domain-error | Effect.fail("...") and throw new Error("...") in domain code. | Uses structured tagged errors for recovery and observability. |
| linteffect/no-domain-meaning-by-folder-only | Admin/public/internal meaning encoded only in names around raw IDs. | Represents context in types, commands, policies, or services. |
Error Modeling
Public Effect operations should expose one structured, recoverable error
channel. The ddd preset includes the complete Domain Modeling and Error
Modeling groups; DDD-only rules remain opt-in to keep recommended compatible
with existing projects.
| Rule | Catches | Why |
| --- | --- | --- |
| linteffect/no-error-as-public-effect-error | Exported functions returning Effect.Effect<_, Error, _>. | Generic Error hides recovery semantics and domain context. |
| linteffect/no-unknown-public-error-channel | Exported functions returning Effect.Effect<_, unknown, _>. | Callers cannot recover by tag or type from an unknown channel. |
| linteffect/no-mixed-effect-error-shapes | Public error unions mixing Error, unknown, string, number, or boolean shapes. | A single tagged error union keeps recovery and observability predictable. |
| linteffect/no-effect-fail-error-message | Effect.fail(error.message), string concatenation, or templates that stringify an error. | Preserves the original error tag, cause, and context instead of collapsing it into a string. |
| linteffect/no-catchall-generic-rethrow | catchAll handlers that create new Error(...) inside Effect.fail(...). | Keeps recovery typed and prevents generic rethrows from erasing the original failure. |
| linteffect/no-log-only-error-handling | catchAll or tapError handlers that only call Effect.log*. | Logging is observability, not failure ownership; map or re-fail after logging. |
| linteffect/no-early-catchall-null | Non-boundary catchAll recovery with Effect.succeed(null), undefined, or a fallback value. | Lets higher layers own recovery instead of leaking untyped absence from domain logic. |
| linteffect/no-expected-state-as-error | Effect.fail("NotFound"), "Missing", "Empty", or "None". | Models expected states as Option, Either, or tagged data instead of overloading failure. |
| linteffect/no-exception-domain-error | throw new *Error inside Effect workflows. | Keeps domain failures in typed Effect channels with supervision and structured recovery. |
| linteffect/no-empty-error-tag | Strict: _tag-only error types and Data.TaggedError classes with empty payloads. | Requires enough structured context for recovery, diagnosis, and domain-level observability. |
