From 35c8470e096aadc253e77278ae07bc779f514b07 Mon Sep 17 00:00:00 2001 From: EFLKumo <96906415+EFLKumo@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:35:43 +0800 Subject: [PATCH] EFFECT.md: Effect 4 API shapes verified against repos/effect Agents kept re-checking the same v4 signatures (renames from Effect 3, Schedule, CLI flags, MCP, vitest, vscode-jsonrpc failure modes). Record them once and point to the file from CLAUDE.md. --- CLAUDE.md | 3 ++ EFFECT.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 EFFECT.md diff --git a/CLAUDE.md b/CLAUDE.md index f88ea7c..b71e844 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,9 @@ test/fixtures/workspace.ts temp workspace layer running the fixture server Rule: outside `src/lsp/`, `vscode-*` may only be imported as types. +`EFFECT.md` lists the Effect 4 API shapes already verified against `repos/effect` (renames from Effect 3, Schedule, +CLI, MCP, vitest). Read it before grepping the Effect source, and add to it when you verify something new. + ## Design decisions worth knowing - Positions handed to agents are 1-based; `Where` accepts `column` or a `symbol` name to find on the line. diff --git a/EFFECT.md b/EFFECT.md new file mode 100644 index 0000000..dea2bf8 --- /dev/null +++ b/EFFECT.md @@ -0,0 +1,85 @@ +# Effect 4 notes for this repo + +Effect `4.0.0-rc.115` (pinned in `package.json`; `repos/effect` is the same version, check +`repos/effect/packages/effect/package.json` if either is bumped). The API differs from Effect 3 +in places, and the type stripper gives no hints, so agents kept re-verifying the same shapes. +This file records what has been verified against the source so the next agent does not have to. + +When something is not listed here, grep the source, not `node_modules`: +`grep -n "^export const NAME" repos/effect/packages/effect/src/MODULE.ts` shows the signature +and implementation; `repos/effect/packages/effect/test/` has usage examples. Add what you learn. + +## Renames from Effect 3 (the things that bite) + +- `Effect.async` → `Effect.callback`; `Effect.catchAll` → `Effect.catch`; `Effect.catchAllCause` → `Effect.catchCause`. +- Every `unsafeX` is `xUnsafe`: `Deferred.doneUnsafe`, `Deferred.isDoneUnsafe`, `PubSub.publishUnsafe`, + `SubscriptionRef.getUnsafe`, `Latch.openUnsafe`, `Scope.makeUnsafe`. +- `Context.Tag` classes are `Context.Service()("id")`; layers are built with `Layer.effect(Tag, make)`. +- `Schema.Union([A, B])` and `Schema.Literals(["a", "b"])` take an array. Filters are `.check(Schema.isInt(), + Schema.isGreaterThanOrEqualTo(0))`, not `.pipe(Schema.int())`. `.annotate({ description, message })`. + `Schema.TaggedError()("Tag", fields)`; `Schema.NumberFromString`; `Schema.fromJsonString(Schema.Json)`. +- `Duration.isGreaterThan` / `isLessThan` (not `greaterThan`); `Duration.format(d)` prints `1s 500ms`. +- `Effect.timeout` / `timeoutOption` / `timeoutOrElse`; `Stream.runHead` returns an `Option`. +- `Effect.forkScoped(effect, { startImmediately?, uninterruptible? })`; `Effect.forkIn(scope)`; `Effect.forkDetach`. +- `Effect.tap` accepts an effect or a function; `Effect.when(condition)` returns an `Option`. +- `Effect.fn("name")(function* (...) {})` for traced generator functions (used everywhere in `src/lsp/`). + +## Schedule (reshaped in v4) + +- Constructors: `exponential(base, factor)`, `spaced`, `fixed`, `recurs(n)`, `forever`, `jittered`, `cron`. +- Limits: `Schedule.upTo({ times, duration })` (replaces `intersect(recurs(n))`); `Schedule.while(meta => boolean | Effect)` + replaces `whileInput` / `whileOutput` / `recurWhile`; `Schedule.modifyDelay(meta => Duration.Input)`; `Schedule.tap(meta => ...)`; + `Schedule.passthrough`. Combinators: `Schedule.max(a, b)` recurs while both do, with the slower delay (Effect 3 + `intersect`); `Schedule.min` recurs while either does, with the faster delay (`union`); `Schedule.concat` runs one + then the other (`andThen`). There is no `resetAfter`: write a step with its own state instead (below). +- `Metadata` given to those callbacks: `{ input, output, duration, attempt, start, now, elapsed, elapsedSincePrevious }`. + `elapsedSincePrevious` is the time between consecutive steps (between crashes, for a restart policy). +- Custom schedules: `Schedule.fromStepWithMetadata(Effect.sync(() => (meta: InputMetadata) => ...))`, where the step + returns `Effect.succeed([output, delay])` to continue or `Cause.done(output)` to stop. `Effect.sync` runs once per `retry`, + so closure state (a crash streak) is per run. See `restarts` in `src/lsp/LanguageServer.ts`. +- `Effect.retry` takes `{ while, until, times, schedule }`, a `Schedule`, or a builder `($) => ...`. `Effect.retryOrElse(schedule, + (lastError, output) => ...)` runs the fallback once the schedule stops. The effect under retry/repeat sees + `Schedule.CurrentMetadata` as a service. + +## Concurrency primitives (as expected, with v4 names) + +- `Deferred.make()`, `await`, `succeed`, `fail`, `done(exit)`, `isDone`; the `*Unsafe` variants are synchronous and return + `boolean` (whether this call completed it). Waiters of `Deferred.await` resume synchronously inside `doneUnsafe`. +- `PubSub.unbounded({ replay? })`, `publish`, `publishUnsafe`, `subscribe`, `Stream.fromPubSub`, `Stream.fromSubscription`. +- `SubscriptionRef.make/get/set/update/changes`; `changes` emits the current value first (replay 1). +- `Latch.make(open?)` with `.open`, `.close`, `.await`, `.release`, `.whenOpen(effect)`. +- `Semaphore.make(n)`, `.withPermits(n)(effect)`; `FiberSet.makeRuntime()` / `makeRuntimePromise` to run effects from callbacks. +- `ScopedCache.makeWith({ capacity, timeToLive: (exit) => Duration, lookup })`, `get`, `has`, `invalidate`, `keys`, `values`, `entries`. +- `Cause.done(value)` is the halt signal used by `Pull`, `Schedule` and `Stream` internals; `Cause.hasInterruptsOnly(cause)`. + +## `effect/unstable/cli` + +- `Command.make(name, { flag: Flag..., arg: Argument... }, Effect.fn(function* (config) {}))`, `Command.withDescription`, + `Command.withExamples([{ command, description }])`, `Command.run({ version })`. +- `Flag.String/Int/Boolean/Directory(name, { mustExist })/KeyValuePair/Literals(name, [...])`; `Flag.withSchema(schema)` + (decodes from the flag's type, so `Schema.NumberFromString` for a string flag), `withDescription`, `withDefault`, `withAlias`, + `withMetavar("never|always|N")`, `optional` (→ `Option`), `map`, `orElse`. `Argument.String`, `Argument.variadic`. +- Help does not print default values; put them in the description. Schema failures print `Invalid value for flag --x: "v". + Expected: Schema validation failed: `. + +## `effect/unstable/ai` MCP (see `src/Server.ts`, `src/Tools.ts`, `src/Resources.ts`) + +- `McpServer.layerStdio(info)` / `layerHttp({ ...info, path })` served with `HttpRouter.serve`; `McpServer.registerToolkit(toolkit)` + after `Toolkit.toLayer(handlers)`; `McpServer.registerResource({ uri | template, name, description, mimeType, content, completion })`. +- `Tool.make(name, { description, parameters, success, failure, dependencies }).annotate(Tool.Readonly, ...)`. +- `McpServer.McpServer` service exposes `notifications["notifications/message" | "notifications/resources/updated"]`. + +## `@effect/vitest` + +- `it.effect` / `it.live` tests run inside a `Scope`; `it.layer(layer, { excludeTestServices: true })("name", (it) => ...)` shares the + layer across the block and, with that option, uses the real clock (`Stream.debounce` and timeouts hang under `TestClock`). +- Real processes and `Effect.sleep` need real time, so every fixture-server test uses `excludeTestServices` or `it.live`. + +## `vscode-jsonrpc` (not Effect, but verified the same way) + +- Loss of the peer: `connection.onClose` fires on transport close and on `dispose()`; pending requests reject with + `ResponseError` codes `ConnectionInactive` (-32096), `PendingResponseRejected` (-32097) or `MessageWriteError` (-32099). +- Writing to a dead child's stdin rejects a promise nobody handles (EPIPE crashes the process). `LanguageServer.ts` writes through + a `PassThrough` piped into stdin and listens for stdin errors instead. +- `connection.onRequest((method, params) => ...)` registers a catch-all ("star") handler; handlers registered per type are + consulted first.