opencode-effect-enforcer 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +3 -3
  2. package/docs/effect-4.0.0-changelog.md +3213 -0
  3. package/docs/effect-4.0.0.md +110 -0
  4. package/guidance/effect-first-development.md +15 -305
  5. package/guidance/progressive-disclosure-guidance.md +18 -24
  6. package/package.json +2 -2
  7. package/patterns/avoid-any.md +2 -2
  8. package/patterns/avoid-direct-json.md +6 -6
  9. package/patterns/avoid-native-fetch.md +8 -6
  10. package/patterns/avoid-node-imports.md +2 -2
  11. package/patterns/avoid-non-null-assertion.md +2 -2
  12. package/patterns/avoid-object-type.md +2 -2
  13. package/patterns/avoid-platform-coupling.md +1 -1
  14. package/patterns/avoid-process-env.md +3 -4
  15. package/patterns/avoid-ts-ignore.md +1 -1
  16. package/patterns/context-tag-extends.md +11 -8
  17. package/patterns/effect-promise-vs-trypromise.md +6 -7
  18. package/patterns/prefer-arr-sort.md +2 -2
  19. package/patterns/prefer-effect-fn.md +21 -65
  20. package/patterns/prefer-schema-class.md +4 -4
  21. package/patterns/throw-in-effect-gen.md +1 -1
  22. package/patterns/use-clock-service.md +4 -0
  23. package/patterns/use-command-executor-service.md +2 -2
  24. package/patterns/use-http-client-service.md +8 -6
  25. package/patterns/use-random-service.md +6 -7
  26. package/skills/effect-ai-chat/SKILL.md +13 -7
  27. package/skills/effect-ai-language-model/SKILL.md +50 -21
  28. package/skills/effect-ai-prompt/SKILL.md +25 -14
  29. package/skills/effect-ai-provider/SKILL.md +50 -22
  30. package/skills/effect-ai-streaming/SKILL.md +27 -12
  31. package/skills/effect-ai-tool/SKILL.md +37 -28
  32. package/skills/effect-atom-rpc/SKILL.md +57 -36
  33. package/skills/effect-atom-state/SKILL.md +57 -19
  34. package/skills/effect-batching/SKILL.md +5 -3
  35. package/skills/effect-cache/SKILL.md +19 -7
  36. package/skills/effect-cli/SKILL.md +17 -8
  37. package/skills/effect-command-executor/SKILL.md +115 -64
  38. package/skills/effect-concurrency-testing/SKILL.md +26 -6
  39. package/skills/effect-config/SKILL.md +53 -2
  40. package/skills/effect-context-witness/SKILL.md +6 -6
  41. package/skills/effect-domain-modeling/SKILL.md +8 -1
  42. package/skills/effect-error-handling/SKILL.md +15 -2
  43. package/skills/effect-fiber/SKILL.md +20 -25
  44. package/skills/effect-filesystem/SKILL.md +69 -57
  45. package/skills/effect-http-api/SKILL.md +72 -22
  46. package/skills/effect-http-client/SKILL.md +25 -21
  47. package/skills/effect-http-server/SKILL.md +51 -21
  48. package/skills/effect-incremental-migration/SKILL.md +17 -8
  49. package/skills/effect-layer-design/SKILL.md +8 -0
  50. package/skills/effect-managed-runtime/SKILL.md +6 -0
  51. package/skills/effect-mcp-server/SKILL.md +64 -24
  52. package/skills/effect-observability/SKILL.md +61 -15
  53. package/skills/effect-parallelization/SKILL.md +24 -7
  54. package/skills/effect-path/SKILL.md +8 -2
  55. package/skills/effect-platform-abstraction/SKILL.md +88 -66
  56. package/skills/effect-platform-layers/SKILL.md +68 -67
  57. package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
  58. package/skills/effect-react-composition/SKILL.md +19 -6
  59. package/skills/effect-rpc-api/SKILL.md +24 -24
  60. package/skills/effect-rpc-client/SKILL.md +33 -28
  61. package/skills/effect-rpc-cluster/SKILL.md +122 -78
  62. package/skills/effect-rpc-server/SKILL.md +56 -20
  63. package/skills/effect-scheduling/SKILL.md +29 -1
  64. package/skills/effect-schema-composition/SKILL.md +31 -13
  65. package/skills/effect-schema-v4/SKILL.md +94 -10
  66. package/skills/effect-scope/SKILL.md +13 -5
  67. package/skills/effect-service-implementation/SKILL.md +1 -1
  68. package/skills/effect-socket/SKILL.md +52 -8
  69. package/skills/effect-sql/SKILL.md +67 -33
  70. package/skills/effect-stream/SKILL.md +50 -5
  71. package/skills/effect-testing/SKILL.md +91 -2
  72. package/skills/effect-workflow/SKILL.md +76 -39
  73. package/src/guidance.ts +0 -1
@@ -9,7 +9,11 @@ Use this skill when implementing tools for AI language models using the Effect A
9
9
 
10
10
  ## Effect AI Documentation Access
11
11
 
12
- For comprehensive Effect AI documentation, view the Effect v4 repository at `packages/ai/`
12
+ For core AI APIs inspect `packages/effect/src/ai/` at `effect@4.0.0` in the
13
+ Effect source reference; provider implementations live under `packages/ai/`.
14
+ Keep all Effect-family packages on the same version. These APIs are tagged
15
+ `@stability unstable`, so minor releases may break them despite stable-looking
16
+ import paths.
13
17
 
14
18
  Reference this for:
15
19
 
@@ -83,7 +87,7 @@ Use this pattern when the surrounding framework wants an async callback surface
83
87
  ### Basic Tool Definition
84
88
 
85
89
  ```typescript
86
- import * as Tool from 'effect/unstable/ai/Tool';
90
+ import * as Tool from 'effect/ai/Tool';
87
91
  import * as Schema from 'effect/Schema';
88
92
 
89
93
  const GetCurrentTime = Tool.make('GetCurrentTime', {
@@ -107,7 +111,7 @@ type Result = Tool.Success<typeof GetCurrentTime>;
107
111
  If you have an existing domain schema you want to use as a tool, create the tool with `Tool.make` and reference the schema directly:
108
112
 
109
113
  ```typescript
110
- import * as Tool from 'effect/unstable/ai/Tool';
114
+ import * as Tool from 'effect/ai/Tool';
111
115
  import * as Schema from 'effect/Schema';
112
116
 
113
117
  const UserResult = Schema.Struct({
@@ -139,7 +143,7 @@ type Success = Tool.Success<typeof GetUserTool>;
139
143
  ### Tool with Parameters
140
144
 
141
145
  ```typescript
142
- import * as Tool from 'effect/unstable/ai/Tool';
146
+ import * as Tool from 'effect/ai/Tool';
143
147
  import { Schema } from 'effect';
144
148
 
145
149
  const GetWeather = Tool.make('GetWeather', {
@@ -169,7 +173,7 @@ type Success = Tool.Success<typeof GetWeather>;
169
173
  ### Tool with Failure Handling
170
174
 
171
175
  ```typescript
172
- import * as Tool from 'effect/unstable/ai/Tool';
176
+ import * as Tool from 'effect/ai/Tool';
173
177
  import { Schema } from 'effect';
174
178
 
175
179
  class UserNotFound extends Schema.TaggedError<UserNotFound>()(
@@ -196,10 +200,7 @@ const FindUser = Tool.make('FindUser', {
196
200
  name: Schema.String,
197
201
  email: Schema.String
198
202
  }),
199
- failure: Schema.Union([
200
- Schema.instanceOf(UserNotFound),
201
- Schema.instanceOf(DatabaseError)
202
- ]),
203
+ failure: Schema.Union([UserNotFound, DatabaseError]),
203
204
  failureMode: 'error'
204
205
  });
205
206
 
@@ -222,12 +223,14 @@ annotation and `Tool.FailureOrigin` type. Keep validation, declared handler, and
222
223
  internal failures distinct when presenting or reporting them.
223
224
 
224
225
  - `"error"` (default): Failures go to Effect error channel
225
- - `"return"`: Failures returned as tool result (captured, not thrown)
226
+ - `"return"`: Parameter and handler failures become failed tool results. Result
227
+ encoding can still fail the returned Stream with `AiError`; return mode does
228
+ not make stream consumption infallible.
226
229
 
227
230
  ### Tool with Service Dependencies
228
231
 
229
232
  ```typescript
230
- import * as Tool from 'effect/unstable/ai/Tool';
233
+ import * as Tool from 'effect/ai/Tool';
231
234
  import * as Context from 'effect/Context';
232
235
  import { Schema } from 'effect';
233
236
 
@@ -261,8 +264,8 @@ type Requirements = Tool.HandlerServices<typeof QueryDatabase>;
261
264
  ### Basic Toolkit
262
265
 
263
266
  ```typescript
264
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
265
- import * as Tool from 'effect/unstable/ai/Tool';
267
+ import * as Toolkit from 'effect/ai/Toolkit';
268
+ import * as Tool from 'effect/ai/Tool';
266
269
  import { Effect, Schema } from 'effect';
267
270
 
268
271
  const GetCurrentTime = Tool.make('GetCurrentTime', {
@@ -473,7 +476,7 @@ Keep static descriptions on the tool for invariant behavior. Put runtime-specifi
473
476
  ### Merging Toolkits
474
477
 
475
478
  ```typescript
476
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
479
+ import * as Toolkit from 'effect/ai/Toolkit';
477
480
 
478
481
  const mathToolkit = Toolkit.make(
479
482
  Tool.make('add', {
@@ -510,7 +513,7 @@ type AllTools = Toolkit.Tools<typeof combined>;
510
513
  ### Basic Provider Tool
511
514
 
512
515
  ```typescript
513
- import * as Tool from 'effect/unstable/ai/Tool';
516
+ import * as Tool from 'effect/ai/Tool';
514
517
  import { Schema } from 'effect';
515
518
 
516
519
  const AnthropicBash = Tool.providerDefined({
@@ -557,7 +560,7 @@ const nativeTools = Toolkit.make(
557
560
  ### Provider Tool with Handler
558
561
 
559
562
  ```typescript
560
- import * as Tool from 'effect/unstable/ai/Tool';
563
+ import * as Tool from 'effect/ai/Tool';
561
564
  import { Schema } from 'effect';
562
565
 
563
566
  const WebSearch = Tool.providerDefined({
@@ -614,7 +617,7 @@ declare const performSearch: (query: string) => Effect.Effect<
614
617
  ### Understanding ToolCallPart and ToolResultPart
615
618
 
616
619
  ```typescript
617
- import * as Prompt from 'effect/unstable/ai/Prompt';
620
+ import * as Prompt from 'effect/ai/Prompt';
618
621
 
619
622
  const toolCallPart = Prompt.makePart('tool-call', {
620
623
  id: 'call_123',
@@ -725,8 +728,14 @@ const toolkitLayer = LongRunningToolkit.toLayer({
725
728
 
726
729
  **Key Pattern: toolkit.handle**
727
730
 
728
- - `toolkit.handle(name, params, toolCallId?)` accepts `Tool.ParametersEncoded<Tool>` and returns `Effect<Stream<HandlerResult<Tool>>>`
731
+ - `toolkit.handle(name, params, toolCallId?, options?)` accepts
732
+ `Tool.ParametersEncoded<T>` and optional schema parse options. Its full shape is
733
+ `Effect<Stream<HandlerResult<T>, Tool.HandlerError<T> | AiError, Tool.HandlerServices<T>>, AiError, Tool.HandlerServices<T>>`.
729
734
  - `handle` decodes the encoded input with the tool's parameter schema before invoking the handler; the handler still receives `Tool.Parameters<Tool>`
735
+ - Provide handler services to the outer Effect as well as the Stream. A
736
+ service-dependent parameter decoder runs before the Stream exists; providing
737
+ services only around `Stream.runCollect` is too late. Consuming the Stream
738
+ inside the same provided `Effect.gen` keeps both stages wired.
730
739
  - The optional call ID is forwarded to the handler as `context.toolCallId`
731
740
  - `preliminary: true`: progress update; do not persist as final history
732
741
  - `preliminary: false`: final result to send/persist
@@ -760,7 +769,7 @@ Passing the decoded `{ times: 3 }` directly to `handle` is now a type error. Use
760
769
  ### Tool Annotations
761
770
 
762
771
  ```typescript
763
- import * as Tool from 'effect/unstable/ai/Tool';
772
+ import * as Tool from 'effect/ai/Tool';
764
773
  import { Schema } from 'effect';
765
774
 
766
775
  const ReadOnlyQuery = Tool.make('query', {
@@ -786,7 +795,7 @@ MCP emits the first four annotations as tool hints. They are hints, not authoriz
786
795
  ### JSON Schema Generation
787
796
 
788
797
  ```typescript
789
- import * as Tool from 'effect/unstable/ai/Tool';
798
+ import * as Tool from 'effect/ai/Tool';
790
799
 
791
800
  const tool = Tool.make('example', {
792
801
  parameters: Schema.Struct({
@@ -815,7 +824,7 @@ const jsonSchema = Tool.getJsonSchema(tool);
815
824
  ### Tool Guards
816
825
 
817
826
  ```typescript
818
- import * as Tool from 'effect/unstable/ai/Tool';
827
+ import * as Tool from 'effect/ai/Tool';
819
828
 
820
829
  const userTool = Tool.make('example');
821
830
  const providerTool = Tool.providerDefined({
@@ -858,8 +867,8 @@ const executeTool = (toolName: string, params: unknown) =>
858
867
 
859
868
  <!-- typecheck -->
860
869
  ```typescript
861
- import * as Tool from 'effect/unstable/ai/Tool';
862
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
870
+ import * as Tool from 'effect/ai/Tool';
871
+ import * as Toolkit from 'effect/ai/Toolkit';
863
872
  import * as Schema from 'effect/Schema';
864
873
  import { Clock, Context, Effect, Layer, Stream } from 'effect';
865
874
 
@@ -946,9 +955,9 @@ manual `handle` calls accept their encoded representation.
946
955
  **CRITICAL**: Always use namespace imports:
947
956
 
948
957
  ```typescript
949
- import * as Tool from 'effect/unstable/ai/Tool';
950
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
951
- import * as Prompt from 'effect/unstable/ai/Prompt';
958
+ import * as Tool from 'effect/ai/Tool';
959
+ import * as Toolkit from 'effect/ai/Toolkit';
960
+ import * as Prompt from 'effect/ai/Prompt';
952
961
  import { Schema, Effect, Data, Context, Layer } from 'effect';
953
962
 
954
963
  const myTool = Tool.make('example');
@@ -958,8 +967,8 @@ const myToolkit = Toolkit.make(myTool);
958
967
  **NEVER** do this:
959
968
 
960
969
  ```typescript
961
- import { make } from 'effect/unstable/ai/Tool';
962
- import { make as makeToolkit } from 'effect/unstable/ai/Toolkit';
970
+ import { make } from 'effect/ai/Tool';
971
+ import { make as makeToolkit } from 'effect/ai/Toolkit';
963
972
  ```
964
973
 
965
974
  ## Quality Checklist
@@ -3,7 +3,11 @@ name: effect-atom-rpc
3
3
  description: Build reactive Atom-aware RPC clients for React/Atom UIs using AtomRpc. Provides cached query atoms, invalidating mutation atoms, SSR hydration, and reactivity-key invalidation on top of an RpcGroup.
4
4
  ---
5
5
 
6
- You are an Effect TypeScript expert specializing in `effect/unstable/reactivity/AtomRpc` — the reactive RPC client for Atom-driven frontends.
6
+ You are an Effect TypeScript expert specializing in `effect/reactivity/AtomRpc` — the reactive RPC client for Atom-driven frontends.
7
+
8
+ Baseline: `effect@4.0.0`; inspect that tag in the Effect source reference.
9
+ Core reactivity APIs are `@stability unstable` and may break in minor releases.
10
+ Use matching Effect-family versions; `@effect/atom-react@4.0.0` requires React 19.
7
11
 
8
12
  ## When to use this skill
9
13
 
@@ -35,13 +39,13 @@ handle them when consuming the stream. An explicit zero `timeToLive` disables
35
39
  default idle retention, so unmount/remount may dispose and refetch. Omission uses
36
40
  the registry default. HttpApi calls accept per-call `sseOptions`.
37
41
 
38
- - `packages/effect/src/unstable/reactivity/AtomRpc.ts` — the whole API (~270 lines)
42
+ - `packages/effect/src/reactivity/AtomRpc.ts` — the whole API
39
43
  - `packages/effect/test/reactivity/AtomRpc.test.ts` — minimal usage + serialization test
40
- - `packages/effect/src/unstable/reactivity/AtomHttpApi.ts` — the cousin pattern for `HttpApi` (same idea, same options)
41
- - `packages/effect/src/unstable/reactivity/AsyncResult.ts` — the `AsyncResult` type returned by `query`
42
- - `packages/effect/src/unstable/reactivity/Atom.ts` — `runtime`, `family`, `serializable`, `keepAlive`, `setIdleTTL`
43
- - `packages/effect/src/unstable/reactivity/Hydration.ts` — SSR dehydrate/hydrate helpers
44
- - `packages/effect/src/unstable/reactivity/Reactivity.ts` — the `Reactivity` service that powers reactivity keys
44
+ - `packages/effect/src/reactivity/AtomHttpApi.ts` — the cousin pattern for `HttpApi` (same idea, same options)
45
+ - `packages/effect/src/reactivity/AsyncResult.ts` — the `AsyncResult` type returned by `query`
46
+ - `packages/effect/src/reactivity/Atom.ts` — `runtime`, `family`, `serializable`, `keepAlive`, `setIdleTTL`
47
+ - `packages/effect/src/reactivity/Hydration.ts` — SSR dehydrate/hydrate helpers
48
+ - `packages/effect/src/reactivity/Reactivity.ts` — the `Reactivity` service that powers reactivity keys
45
49
 
46
50
  ## Imports
47
51
 
@@ -52,9 +56,9 @@ import {
52
56
  AtomRpc,
53
57
  Hydration,
54
58
  Reactivity
55
- } from 'effect/unstable/reactivity';
56
- import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
57
- import { FetchHttpClient } from 'effect/unstable/http';
59
+ } from 'effect/reactivity';
60
+ import { RpcClient, RpcSerialization } from 'effect/rpc';
61
+ import { FetchHttpClient } from 'effect/http';
58
62
  import { useAtomValue, useAtomSet } from '@effect/atom-react';
59
63
  ```
60
64
 
@@ -77,9 +81,9 @@ The `runtime` is what powers `query` and `mutation` — internally it builds a `
77
81
 
78
82
  ```ts
79
83
  import { Layer } from 'effect';
80
- import { AtomRpc } from 'effect/unstable/reactivity';
81
- import { FetchHttpClient } from 'effect/unstable/http';
82
- import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
84
+ import { AtomRpc } from 'effect/reactivity';
85
+ import { FetchHttpClient } from 'effect/http';
86
+ import { RpcClient, RpcSerialization } from 'effect/rpc';
83
87
  import { UsersGroup } from '../shared/users-rpc.ts';
84
88
 
85
89
  export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
@@ -124,7 +128,7 @@ AtomRpc.Service<UsersClient>()('UsersClient', {
124
128
  disableTracing: false,
125
129
  generateRequestId: customRequestIdFactory, // for deterministic IDs in tests
126
130
  makeEffect: undefined, // override the default RpcClient.make
127
- runtime: Atom.runtime // override the runtime factory (e.g., Atom.runtime.withReactivity)
131
+ runtime: Atom.runtime // override with another factory, e.g. Atom.context()
128
132
  });
129
133
  ```
130
134
 
@@ -149,6 +153,11 @@ client.query(tag, payload, options?) =>
149
153
 
150
154
  Query atoms are **cached by request key** — calling `query('GetUser', { id: '1' })` from many components returns the *same* atom, so the rpc fires once and shares the result.
151
155
 
156
+ The key also includes headers, reactivity keys, TTL, and serialization key.
157
+ Use matching options to share a cache entry. Union-of-tag arguments retain the
158
+ selected RPCs' payload/result types; they no longer collapse to `never`. Narrow
159
+ the tag when application logic needs to pair a particular payload with its result.
160
+
152
161
  Effect v4 query wrappers preserve serialization and retention metadata while adding reactivity. It is therefore safe to combine `reactivityKeys`, `serializationKey`, and `timeToLive`; hydration identity and idle retention are not discarded by the reactive wrapper.
153
162
 
154
163
  ```ts
@@ -184,7 +193,11 @@ client.query(tag, payload, {
184
193
  ```
185
194
 
186
195
  - **`reactivityKeys`** — keys this atom listens to. When a `mutation` (or any `Reactivity.invalidate`) fires with overlapping keys, this atom re-fetches.
187
- - **`timeToLive`** — `Duration.Input | "infinity"`. With a finite duration, the atom is removed from the cache after that long without subscribers (`Atom.setIdleTTL`). With `Duration.infinity`, the atom is `Atom.keepAlive`'d (never evicted). Without `timeToLive`, the atom resets when the last subscriber unmounts.
196
+ - **`timeToLive`** — idle retention via `Atom.setIdleTTL`. A finite duration permits
197
+ disposal after inactivity; `Duration.infinity` uses `Atom.keepAlive` until the
198
+ registry is reset/disposed. Omission uses the registry default, while explicit
199
+ zero bypasses default idle retention. Freshness is separate: use `Atom.swr` for
200
+ stale-while-revalidate behavior (see `effect-atom-state`).
188
201
  - **`serializationKey`** — for non-stream queries, a stable key opts the atom into `Atom.serializable` keyed by `AtomRpc:${tag}:${serializationKey}`. Without a `serializationKey`, the query atom is cached for the current registry but is not serializable/hydratable, so SSR clients will fetch again. Stream rpcs cannot be serializable.
189
202
 
190
203
  ### Stream rpcs
@@ -291,25 +304,27 @@ Hydration uses `Atom.serializable` (which `query` opts into when you pass `seria
291
304
 
292
305
  ```ts
293
306
  // --- server: render then dehydrate ---
294
- import { AtomRegistry, Hydration } from 'effect/unstable/reactivity';
295
-
296
- const registry = AtomRegistry.make();
297
- const userAtom = UsersClient.query('GetUser', { id: 'u1' }, {
298
- serializationKey: 'u1'
299
- });
307
+ import { Effect } from 'effect';
308
+ import { AtomRegistry, Hydration } from 'effect/reactivity';
300
309
 
301
- const unmount = registry.mount(userAtom);
302
- yield* Effect.yieldNow;
303
- yield* Effect.yieldNow;
304
-
305
- const dehydrated = Hydration.toValues(Hydration.dehydrate(registry));
306
- unmount();
310
+ const dehydrated = yield* Effect.scoped(Effect.gen(function* () {
311
+ const registry = yield* Effect.acquireRelease(
312
+ Effect.sync(() => AtomRegistry.make()),
313
+ (registry) => Effect.sync(() => registry.dispose())
314
+ );
315
+ const userAtom = UsersClient.query('GetUser', { id: 'u1' }, {
316
+ serializationKey: 'u1'
317
+ });
318
+ yield* AtomRegistry.mount(registry, userAtom);
319
+ yield* AtomRegistry.getResult(registry, userAtom);
320
+ return Hydration.toValues(Hydration.dehydrate(registry));
321
+ }));
307
322
  // dehydrated is a JSON-safe array of { key, value } pairs to inline in the HTML
308
323
  ```
309
324
 
310
325
  ```tsx
311
326
  // --- client: hydrate before rendering ---
312
- import { AtomRegistry, Hydration } from 'effect/unstable/reactivity';
327
+ import { AtomRegistry, Hydration } from 'effect/reactivity';
313
328
  import { RegistryContext } from '@effect/atom-react';
314
329
 
315
330
  const registry = AtomRegistry.make();
@@ -327,7 +342,7 @@ Stream rpcs are not serializable — only non-stream `query` atoms with a stable
327
342
  `AtomRpc.Service` accepts a `runtime` factory option. The default is `Atom.runtime`. Override it when you want all the atoms produced by this client to share a custom reactivity scope, registry, or annotation:
328
343
 
329
344
  ```ts
330
- const myRuntime = Atom.runtime.withReactivity(['shared-namespace']);
345
+ const myRuntime = Atom.context();
331
346
 
332
347
  export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
333
348
  group: UsersGroup,
@@ -336,12 +351,17 @@ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
336
351
  }) {}
337
352
  ```
338
353
 
354
+ `withReactivity(keys)` decorates an atom; it is not a runtime factory. Use
355
+ `reactivityKeys` on queries/mutations for invalidation. Runtime factories use
356
+ registry-scoped layer memoization by default; provide the same registry to
357
+ components that should share state.
358
+
339
359
  ## End-to-end example
340
360
 
341
361
  ```ts
342
362
  // --- shared/users-rpc.ts ---
343
363
  import { Schema } from 'effect';
344
- import { Rpc, RpcGroup } from 'effect/unstable/rpc';
364
+ import { Rpc, RpcGroup } from 'effect/rpc';
345
365
 
346
366
  export class User extends Schema.Class<User>('User')({
347
367
  id: Schema.String,
@@ -375,9 +395,9 @@ export const UsersGroup = RpcGroup.make(GetUser, ListUsers, CreateUser);
375
395
  ```ts
376
396
  // --- frontend/clients/users-client.ts ---
377
397
  import { Layer } from 'effect';
378
- import { FetchHttpClient } from 'effect/unstable/http';
379
- import { AtomRpc } from 'effect/unstable/reactivity';
380
- import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
398
+ import { FetchHttpClient } from 'effect/http';
399
+ import { AtomRpc } from 'effect/reactivity';
400
+ import { RpcClient, RpcSerialization } from 'effect/rpc';
381
401
  import { UsersGroup } from '../../shared/users-rpc.ts';
382
402
 
383
403
  export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
@@ -393,7 +413,7 @@ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
393
413
  ```tsx
394
414
  // --- frontend/components/UserList.tsx ---
395
415
  import { useAtomValue, useAtomSet } from '@effect/atom-react';
396
- import { AsyncResult } from 'effect/unstable/reactivity';
416
+ import { AsyncResult } from 'effect/reactivity';
397
417
  import { UsersClient } from '../clients/users-client';
398
418
 
399
419
  export function UserList() {
@@ -455,7 +475,7 @@ export function UserDetail({ id }: { id: string }) {
455
475
  If your backend exposes an `HttpApi` instead of an `RpcGroup`, `AtomHttpApi.Service<Self>()(id, options)` is the same idea applied to `HttpApiGroup`/`HttpApiEndpoint`:
456
476
 
457
477
  ```ts
458
- import { AtomHttpApi } from 'effect/unstable/reactivity';
478
+ import { AtomHttpApi } from 'effect/reactivity';
459
479
 
460
480
  class ApiClient extends AtomHttpApi.Service<ApiClient>()('ApiClient', {
461
481
  api: MyHttpApi,
@@ -494,7 +514,8 @@ The `query`/`mutation` arguments differ (you pass `groupName, endpointName, requ
494
514
  - One `AtomRpc.Service` class per logical client; module-singleton, never per-component.
495
515
  - Always pass `reactivityKeys` to mutations so dependent queries refresh.
496
516
  - Pass `serializationKey` to every query you want SSR-hydratable.
497
- - Pick `timeToLive` deliberately: short (`'10 seconds'`) for hot data, long (`Duration.infinity`) for reference data, omit for "tear down on unmount".
517
+ - Pick idle `timeToLive` deliberately; omit for registry defaults, zero to bypass
518
+ default idle retention, or `Duration.infinity` to retain until registry disposal.
498
519
  - Use `AsyncResult.builder(...).onWaiting(...).onError(...).onSuccess(...)` (or `AsyncResult.matchWithWaiting`) to render — never check `.waiting` and `.error` ad-hoc. Reserve `.onFailure((cause) => ...)` for whole-`Cause` fallbacks after typed error branches.
499
520
  - For protocol layers that depend on auth tokens or other reactive state, use the `(get) => Layer` form of `protocol` so the client rebuilds when those atoms change.
500
521
  - For typed error recovery, handle declared RPC errors, RPC middleware wire errors, and `RpcClientError`; branch on `_tag` only when the relevant errors are tagged.
@@ -7,20 +7,20 @@ description: Implement reactive state management with Effect Atom for React appl
7
7
 
8
8
  Effect Atom is a reactive state management library for Effect that seamlessly integrates with React.
9
9
 
10
- `@effect/atom-react` supports React `>=19.0.0 <20.0.0` (the peer range
11
- was relaxed). This does not add React 18 support. Keep the adapter aligned with
12
- the Effect release; core atoms still live in `effect/unstable/reactivity`, and
10
+ `@effect/atom-react@4.0.0` requires React `>=19.0.0 <20.0.0`.
11
+ Keep the adapter at the same version as `effect`; core atoms live in `effect/reactivity`, and
13
12
  React bindings live in `@effect/atom-react` (`packages/atom/react` upstream).
13
+ Core reactivity APIs carry `@stability unstable` and may break in minor releases.
14
14
 
15
15
  ## Effect Source Reference
16
16
 
17
17
  The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
18
- Browse and read files there directly to look up APIs, types, and implementations.
18
+ Inspect `git show effect@4.0.0:<path>` there for this baseline; main may be ahead.
19
19
 
20
20
  Reference this for:
21
21
 
22
- - Atom reactivity: `packages/effect/src/unstable/reactivity/`
23
- - AsyncResult source: `packages/effect/src/unstable/reactivity/AsyncResult.ts`
22
+ - Atom reactivity: `packages/effect/src/reactivity/`
23
+ - AsyncResult source: `packages/effect/src/reactivity/AsyncResult.ts`
24
24
  - Effect source: `packages/effect/src/`
25
25
 
26
26
  ## Core Concepts
@@ -36,7 +36,7 @@ explicit for state that must survive SSR dehydration.
36
36
  Atoms work **by reference** - they are stable containers for reactive state:
37
37
 
38
38
  ```typescript
39
- import * as Atom from 'effect/unstable/reactivity/Atom';
39
+ import * as Atom from 'effect/reactivity/Atom';
40
40
 
41
41
  // Atoms are created once and referenced throughout the app
42
42
  export const counterAtom = Atom.make(0);
@@ -47,10 +47,13 @@ export const counterAtom = Atom.make(0);
47
47
 
48
48
  ### Automatic Cleanup
49
49
 
50
- Atoms automatically reset when no subscribers remain (unless marked with `keepAlive`):
50
+ Registry nodes become eligible for disposal when unused by subscribers and
51
+ dependent atoms. Disposal follows the atom's idle TTL or registry default; it is
52
+ not necessarily synchronous with the last React unmount. `keepAlive` retains the
53
+ node until its registry is reset/disposed:
51
54
 
52
55
  ```typescript
53
- // Resets when last subscriber unmounts
56
+ // Disposed after becoming unused, according to the registry's idle policy
54
57
  export const temporaryState = Atom.make(initialValue);
55
58
 
56
59
  // Persists across component lifecycles
@@ -61,10 +64,15 @@ export const persistentState = Atom.make(initialValue).pipe(Atom.keepAlive);
61
64
 
62
65
  Atom values are computed on-demand when subscribers access them.
63
66
 
67
+ Tracked dependencies remain retained while a node is stale and are reconciled
68
+ on its next build. Register cleanup with `get.addFinalizer` or scoped effects so
69
+ superseded builds release resources. Observed failed builds can recover when a
70
+ dependency changes; avoid manual reset workarounds for stale-node propagation.
71
+
64
72
  ## Pattern: Basic Atoms
65
73
 
66
74
  ```typescript
67
- import * as Atom from 'effect/unstable/reactivity/Atom';
75
+ import * as Atom from 'effect/reactivity/Atom';
68
76
 
69
77
  // Simple atom
70
78
  export const count = Atom.make(0);
@@ -132,19 +140,29 @@ export const userAtoms = Atom.family((userId: string) =>
132
140
  Atom.make<User | null>(null).pipe(Atom.keepAlive)
133
141
  );
134
142
 
135
- // Usage - always returns the same atom for a given ID
143
+ // Reuses the cached live atom for a given ID
136
144
  const userAtom = userAtoms(userId);
137
145
  ```
138
146
 
147
+ Family caches use weak references where supported. Identity is reused while the
148
+ cached atom is live; it is not a permanent store of every key ever requested.
149
+ An old atom's finalizer does not evict a newer cached atom for the same key.
150
+
139
151
  ## Pattern: Atom.fn for Async Actions
140
152
 
141
153
  Use `Atom.fn` with `Effect.fnUntraced` for async operations:
142
154
 
143
155
  - Reading gives `AsyncResult<Success, Error>` with automatic `.waiting` flag
144
156
  - Triggering via `useAtomSet` runs the effect
157
+ - The callback takes one application argument plus an `Atom.FnContext`; bundle
158
+ multiple inputs into an object. The second argument is not another payload.
159
+ - By default a new write replaces the current run. Use `{ concurrent: true }`
160
+ deliberately for overlapping Effect executions. Synchronous successes and
161
+ failures are retained in concurrent mode; it still exposes one `AsyncResult`,
162
+ not a per-invocation result log.
145
163
 
146
164
  ```typescript
147
- import * as Atom from "effect/unstable/reactivity/Atom"
165
+ import * as Atom from "effect/reactivity/Atom"
148
166
  import { useAtomValue, useAtomSet } from "@effect/atom-react"
149
167
  import { Effect, Exit } from "effect"
150
168
 
@@ -278,7 +296,7 @@ Atom.runtime.addGlobalLayer(
278
296
  Atoms can return `AsyncResult` types for explicit error handling:
279
297
 
280
298
  ```tsx
281
- import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
299
+ import * as AsyncResult from 'effect/reactivity/AsyncResult';
282
300
 
283
301
  export const userData = Atom.make<AsyncResult.AsyncResult<User, Error>>(
284
302
  AsyncResult.initial()
@@ -354,7 +372,7 @@ Use `Atom.kvs` for persisted state:
354
372
 
355
373
  ```typescript
356
374
  import { BrowserKeyValueStore as BrowserKvs } from '@effect/platform-browser';
357
- import * as Atom from 'effect/unstable/reactivity/Atom';
375
+ import * as Atom from 'effect/reactivity/Atom';
358
376
  import * as Schema from 'effect/Schema';
359
377
 
360
378
  export const userSettings = Atom.kvs({
@@ -452,7 +470,7 @@ export const wsConnection = Atom.make(
452
470
  2. **Never Manual Void Wrappers**: Don't wrap Effects in void functions—you lose `waiting` control
453
471
  3. **Reference Stability**: Use `Atom.family` for dynamically generated atom sets
454
472
  4. **Lazy Evaluation**: Values computed on-demand when accessed
455
- 5. **Automatic Cleanup**: Atoms reset when unused (unless `keepAlive`)
473
+ 5. **Automatic Cleanup**: Unused atoms follow idle retention; `keepAlive` lasts until registry reset/disposal
456
474
  6. **Derive, Don't Coordinate**: Use computed atoms to derive state
457
475
  7. **Result Types**: Handle errors explicitly with AsyncResult.match
458
476
  8. **Services in Runtime**: Wrap layers once, use in multiple atoms
@@ -467,7 +485,7 @@ export const wsConnection = Atom.make(
467
485
  Use `Atom.fn` with `Effect.fnUntraced` which automatically provides `AsyncResult` with `.waiting` flag:
468
486
 
469
487
  ```typescript
470
- import * as Atom from "effect/unstable/reactivity/Atom"
488
+ import * as Atom from "effect/reactivity/Atom"
471
489
  import { useAtomValue, useAtomSet } from "@effect/atom-react"
472
490
  import { Effect } from "effect"
473
491
 
@@ -496,7 +514,7 @@ function UserProfile() {
496
514
 
497
515
  ```typescript
498
516
  export const updateItem = runtime.fn(
499
- Effect.fnUntraced(function* (id: string, updates: Partial<Item>) {
517
+ Effect.fnUntraced(function* ({ id, updates }: { id: string; updates: Partial<Item> }) {
500
518
  const current = yield* Atom.get(itemsAtom);
501
519
 
502
520
  // Optimistic update
@@ -569,12 +587,32 @@ Atom.batch(() => {
569
587
  registry.set(ageAtom, 30);
570
588
  registry.set(statusAtom, 'active');
571
589
  });
572
- // Subscribers notified once, not three times
590
+ // Dependents observe the final batched state
573
591
  ```
574
592
 
575
593
  Outside Effect/Atom contexts, use an `AtomRegistry` (`registry.set(...)`) inside the batch. Inside an atom or write context, use that context (`ctx.set(...)`) in the same pattern. `Atom.batch` only batches notifications; it does not introduce a free `set` function.
576
594
 
577
- Use when multiple atoms must update atomically to avoid intermediate renders.
595
+ Batch synchronous writes to avoid intermediate notifications. Writes made by
596
+ commit listeners are processed in subsequent commit work rather than dropped,
597
+ so do not promise exactly one callback when listeners themselves write.
598
+ Batching is not rollback: writes made before a thrown exception are still
599
+ committed and notified, then the first failure is rethrown. Async work after an
600
+ `await` is outside the batch.
601
+
602
+ ## Stale-while-revalidate reads
603
+
604
+ Wrap an `AsyncResult` query atom with `Atom.swr({ staleTime: '30 seconds' })`.
605
+ Reads return the current result and defer stale-source refresh until after the
606
+ read. The scheduled refresh is skipped if the source becomes fresh or the
607
+ wrapper is disposed; a one-shot unmounted read does not keep background work
608
+ alive. Mount/subscribe for a continuing query lifetime.
609
+
610
+ `revalidateOnMount` controls initial stale refreshes. `revalidateOnFocus: true`
611
+ respects `staleTime`, while `'always'` forces a refresh. Manual refresh always
612
+ forwards to the source. `staleTime` is a freshness window, distinct from idle TTL.
613
+ Create the wrapper once (module scope, `Atom.family`, or `useMemo`) rather than
614
+ on every render. `swr` returns `WithoutSerializable<R>`; retain the serializable
615
+ source atom for hydration or explicitly serialize the wrapper under its own key.
578
616
 
579
617
  ## AsyncResult.builder
580
618
 
@@ -14,7 +14,7 @@ Reference this for:
14
14
 
15
15
  - `Request` and `Request.Class` definitions (`packages/effect/src/Request.ts`)
16
16
  - `RequestResolver` constructors and combinators (`packages/effect/src/RequestResolver.ts`)
17
- - `SqlResolver` for SQL-specific batching (`packages/effect/src/unstable/sql/SqlResolver.ts`)
17
+ - `SqlResolver` for SQL-specific batching (`packages/effect/src/sql/SqlResolver.ts`)
18
18
  - Batching tutorial (`ai-docs/src/05_batching/10_request-resolver.ts`)
19
19
 
20
20
  ## The N+1 Problem
@@ -440,10 +440,12 @@ class Users extends Context.Service<
440
440
 
441
441
  ## SQL Integration with SqlResolver
442
442
 
443
- `SqlResolver` (from `effect/unstable/sql`) provides schema-validated, batched SQL resolvers. Import:
443
+ `SqlResolver` (from `effect/sql`) provides schema-validated, batched SQL resolvers.
444
+ It remains marked `@stability unstable` even though its import path has no
445
+ `unstable` segment; check its release-specific contract when upgrading. Import:
444
446
 
445
447
  ```typescript
446
- import { SqlResolver } from 'effect/unstable/sql';
448
+ import { SqlResolver } from 'effect/sql';
447
449
  ```
448
450
 
449
451
  ### SqlResolver.ordered