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
@@ -33,29 +33,31 @@ good url = pipe(
33
33
 
34
34
  ```haskell
35
35
  -- Composable request building
36
- request :: Effect HttpClientRequest HttpBodyError
36
+ request :: HttpClientRequest
37
37
  request = pipe(
38
- HttpClientRequest.post("/api/users"),
39
- HttpClientRequest.bodyJson({ name: "Alice" })
38
+ HttpClientRequest.get("https://api.example.com/users"),
39
+ HttpClientRequest.setHeader("accept", "application/json")
40
40
  )
41
41
 
42
42
  -- With retry, timeout, tracing
43
43
  resilient :: Effect Response HttpError (HttpClient | Scope)
44
44
  resilient = pipe(
45
- request,
46
- Effect.flatMap(HttpClient.execute),
45
+ HttpClient.execute(request),
47
46
  Effect.retry(Schedule.recurs(3)),
48
47
  Effect.timeout(Duration.seconds(10))
49
48
  )
50
49
 
51
50
  -- Platform layer at entry point
52
51
  main = program.pipe(
53
- Effect.provide(BunHttpClient.layer) -- or NodeHttpClient.layer
52
+ Effect.provide(BunHttpClient.layer) -- or NodeHttpClient.layerUndici
54
53
  )
55
54
  ```
56
55
 
57
56
  Native `fetch` produces untyped Promise rejections. Use `HttpClientRequest`, `HttpClientResponse`, and `HttpClient` from Effect for typed errors, composable request building, and testability via layer substitution.
58
57
 
58
+ Import the HTTP modules from `effect/http`. Retry only proven-idempotent requests;
59
+ do not copy a retry policy onto ordinary non-idempotent POST/PATCH operations.
60
+
59
61
  Exception: an explicit low-level platform implementation that cannot use Effect HTTP. Keep it isolated, lift it with `Effect.tryPromise`, propagate the supplied `AbortSignal`, classify status before decoding, and decode unknown bodies with `Schema`.
60
62
 
61
63
  References: EF-9b, Checklist #42 in effect-first-development.md
@@ -73,12 +73,12 @@ good = do
73
73
  | `node:fs/promises` | `FileSystem.FileSystem` |
74
74
  | `node:path` | `Path.Path` |
75
75
  | `node:os` | `FileSystem.makeTempFileScoped` / `Path.Path` / platform layer |
76
- | `node:child_process` | `ChildProcessSpawner` + `ChildProcess` from `effect/unstable/process` |
76
+ | `node:child_process` | `ChildProcessSpawner` + `ChildProcess` from `effect/process` |
77
77
  | `node:http` | `HttpClient.HttpClient` |
78
78
  | `node:https` | `HttpClient.HttpClient` |
79
79
  | `node:stream` | `Stream` from effect |
80
80
  | `node:readline` | `Terminal.Terminal` |
81
- | `node:crypto` | `Crypto` from `effect/unstable/crypto` or a wrapped Effect service |
81
+ | `node:crypto` | `Crypto.Crypto` from `effect/Crypto` or a wrapped Effect service |
82
82
 
83
83
  **Exceptions:**
84
84
 
@@ -37,8 +37,8 @@ safe :: User → Maybe Email
37
37
  safe user = user?.contact?.email ?? Nothing
38
38
 
39
39
  -- With Schema for external data
40
- validated :: Unknown → Either ParseError User
41
- validated = Schema.decode userSchema
40
+ validated :: Unknown → Effect User SchemaError
41
+ validated = Schema.decodeUnknownEffect User
42
42
  ```
43
43
 
44
44
  The `!` operator is "trust me, this isn't null"—if wrong, runtime crash. Use `?.`, `??`, `Option`, or type guards for safe null handling.
@@ -39,8 +39,8 @@ good :: User → Effect ()
39
39
  good user = doSomething user -- clear structure, IDE support
40
40
 
41
41
  -- For unknown shapes
42
- decode :: Unknown → Either ParseError User
43
- decode = Schema.decode userSchema
42
+ decode :: Unknown → Effect User SchemaError
43
+ decode = Schema.decodeUnknownEffect User
44
44
  ```
45
45
 
46
46
  `Object` and `{}` provide no type safety—they accept any non-null value. Use explicit types, `Record<K,V>`, `unknown`, or Schema for validation.
@@ -38,6 +38,6 @@ good = Layer.provide(platformLayer) -- no platform coupling
38
38
  good = -- runtime provides ChildProcessSpawner, FileSystem, etc.
39
39
  ```
40
40
 
41
- Binding packages wrap external systems (CLIs, APIs, databases) and must be platform-agnostic. They should depend on abstract services from `effect` and its unstable namespaces, such as `FileSystem`, `Path`, `HttpClient`, and `ChildProcessSpawner`, but never on `@effect/platform-bun` or `@effect/platform-node` concrete implementations.
41
+ Binding packages wrap external systems (CLIs, APIs, databases) and must be platform-agnostic. They should depend on abstract services from `effect` and its area entrypoints, such as `FileSystem`, `Path`, `HttpClient` from `effect/http`, and `ChildProcessSpawner` from `effect/process`, rather than hardwiring `@effect/platform-bun` or `@effect/platform-node` concrete implementations.
42
42
 
43
43
  Platform-specific layers (`BunServices.layer`, `NodeServices.layer`) belong in the runtime or CLI entry point, not in bindings. The runtime provides concrete implementations of abstract Effect services there.
@@ -29,13 +29,12 @@ Config.Redacted :: String -> Config Redacted -- for sensitive values
29
29
  bad :: Effect String
30
30
  bad = Effect.sync \_ -> process.env.API_KEY -- raw side effect
31
31
 
32
- good :: Effect String ConfigError
32
+ good :: Effect (Redacted String) ConfigError
33
33
  good = Config.Redacted "API_KEY" -- typed, redacted
34
34
 
35
- better :: Effect String ConfigError
35
+ better :: Effect Int ConfigError
36
36
  better = Config.Int("PORT")
37
- & Config.withDefault "3000"
38
- & Config.map Number.parse -- with transformation
37
+ & Config.withDefault 3000 -- defaults use the decoded type
39
38
  ```
40
39
 
41
40
  `process.env` is a raw side effect with no type safety. Use `Config.*` for validated, composable, testable configuration.
@@ -32,7 +32,7 @@ bad = do
32
32
 
33
33
  good :: Effect a
34
34
  good = do
35
- x ← Schema.decode schema raw -- prove correctness
35
+ x ← Schema.decodeUnknownEffect schema raw -- prove correctness
36
36
  ```
37
37
 
38
38
  Suppressing errors masks bugs that surface at runtime. Fix the underlying type issue instead.
@@ -31,7 +31,7 @@ Use `Context.Service` for service definitions. Replace these legacy spellings:
31
31
  | Legacy API | Era | Replacement |
32
32
  | --------------------------- | --------- | -------------------------- |
33
33
  | `Context.Tag` / `GenericTag` | pre-v4 | `Context.Service` |
34
- | `Effect.Service` | early v4 | `Context.Service` |
34
+ | `Effect.Service` | v3 | `Context.Service` |
35
35
  | `ServiceMap.Service` / `.*` | prerelease | `Context.Service` / `Context.*` |
36
36
 
37
37
  ```haskell
@@ -47,21 +47,24 @@ class ParallelClient extends Effect.Service<ParallelClient>()(...)
47
47
  class MyService extends ServiceMap.Service<MyService>()("@app/MyService", { ... })
48
48
 
49
49
  -- Fix: Context.Service
50
- class ParallelClient extends Context.Service<ParallelClient>()(
50
+ class ParallelClient extends Context.Service<ParallelClient, Interface>()(
51
51
  "@parallel/ParallelClient"
52
52
  )
53
53
  ```
54
54
 
55
+ <!-- typecheck -->
55
56
  ```typescript
56
- // Concrete service (single implementation)
57
- export class ParallelClient extends Context.Service<ParallelClient>()(
57
+ import { Context, Effect, Layer } from 'effect';
58
+
59
+ class ParallelClient extends Context.Service<ParallelClient, {
60
+ readonly ping: Effect.Effect<string>;
61
+ }>()(
58
62
  '@parallel/ParallelClient'
59
63
  ) {}
60
64
 
61
- // Interface-style service (multiple implementations, config, infrastructure)
62
- export class Clipboard extends Context.Service<Clipboard>()(
63
- '@Clipboard/Clipboard'
64
- ) {}
65
+ const layer = Layer.succeed(ParallelClient, {
66
+ ping: Effect.succeed('pong')
67
+ });
65
68
  ```
66
69
 
67
70
  When migrating from `ServiceMap`, also update the corresponding module accessors:
@@ -16,7 +16,7 @@ suggestSkills:
16
16
 
17
17
  ```haskell
18
18
  -- Transformation
19
- promise :: IO (Promise a) → Effect a ∅ -- rejection = defect (uncatchable)
19
+ promise :: IO (Promise a) → Effect a ∅ -- rejection = defect, outside typed E
20
20
  tryPromise :: IO (Promise a) → Effect a E -- rejection = typed error (catchable)
21
21
  ```
22
22
 
@@ -24,7 +24,7 @@ tryPromise :: IO (Promise a) → Effect a E -- rejection = typed error (c
24
24
  -- Pattern
25
25
  bad :: Effect User ∅
26
26
  bad = Effect.promise \_ → fetchUser id
27
- -- rejection becomes Defect: can't catch, crashes fiber
27
+ -- rejection becomes a defect: catchTag does not handle it
28
28
 
29
29
  good :: Effect User FetchError
30
30
  good = Effect.tryPromise
@@ -37,11 +37,10 @@ good = Effect.tryPromise
37
37
  handle :: Effect User FetchError → Effect User ∅
38
38
  handle = catchTag "FetchError" \e → defaultUser
39
39
 
40
- -- Defects bypass all handlers
41
- defect :: Effect a ∅ → Effect a E
42
- defect = id -- can't recover from defects
40
+ -- Defects require explicit cause/defect handling, not ordinary typed recovery
41
+ inspectDefect = Effect.catchDefect
43
42
  ```
44
43
 
45
- `Effect.promise` converts rejections to uncatchable defects. Use `Effect.tryPromise` for typed, recoverable errors in the E channel.
44
+ `Effect.promise` converts rejections to defects outside the typed error channel. `Effect.catchDefect` and cause-level handlers can observe them, but expected rejection belongs in `Effect.tryPromise` with a typed error.
46
45
 
47
- Any reference to `Effect.promise` is flagged — not just `yield* Effect.promise(...)`. Piping, passing, or returning `Effect.promise` propagates the same defect-conversion problem to the consumer and is equally wrong.
46
+ Any reference to `Effect.promise` is flagged — not just `yield* Effect.promise(...)`. Review callbacks, returned helpers, and direct calls alike. An explicit boundary whose rejection genuinely represents a defect may intentionally use `Effect.promise`; explain that contract rather than relabeling expected failures as defects.
@@ -44,9 +44,9 @@ sorted = Arr.sort byNameThenAge
44
44
 
45
45
  -- Reverse
46
46
  descending :: [User] -> [User]
47
- descending = Arr.sort (Order.reverse byName)
47
+ descending = Arr.sort (Order.flip byName)
48
48
  ```
49
49
 
50
50
  Native `.sort()` mutates the array in place and uses an untyped comparator. `Arr.sort` from `effect/Array` returns a new sorted array using a composable, typed `Order`.
51
51
 
52
- References: EF-38 in effect-first-development.md
52
+ References: EF-5 in effect-first-development.md
@@ -84,74 +84,30 @@ Key details:
84
84
 
85
85
  ## Complete Before/After
86
86
 
87
+ <!-- typecheck -->
87
88
  ```typescript
88
- // BEFORE — plain arrow functions, no tracing
89
- export class UserRepository extends Context.Service<UserRepository>()(
90
- '@services/UserRepository',
91
- {
92
- make: Effect.gen(function* () {
93
- const db = yield* DatabaseClient;
89
+ import { Context, Effect, Layer } from 'effect';
90
+ import * as Schema from 'effect/Schema';
94
91
 
95
- return {
96
- findById: (id: string): Effect.Effect<User, UserNotFound> =>
97
- Effect.gen(function* () {
98
- const row = yield* db.query(
99
- 'SELECT * FROM users WHERE id = ?',
100
- id
101
- );
102
- if (!row)
103
- return yield* new UserNotFound({
104
- id,
105
- message: `Not found: ${id}`
106
- });
107
- return row as User;
108
- }),
109
-
110
- create: (
111
- data: CreateUserData
112
- ): Effect.Effect<User, DuplicateUser> =>
113
- Effect.gen(function* () {
114
- return yield* db.insert('users', data);
115
- })
116
- };
117
- })
118
- }
119
- ) {}
120
-
121
- // AFTER — Effect.fn, every method gets a traced span
122
- export class UserRepository extends Context.Service<UserRepository>()(
123
- '@services/UserRepository',
124
- {
125
- make: Effect.gen(function* () {
126
- const db = yield* DatabaseClient;
127
-
128
- const findById = Effect.fn('UserRepository.findById')(
129
- (id: string): Effect.Effect<User, UserNotFound> =>
130
- Effect.gen(function* () {
131
- const row = yield* db.query(
132
- 'SELECT * FROM users WHERE id = ?',
133
- id
134
- );
135
- if (!row)
136
- return yield* new UserNotFound({
137
- id,
138
- message: `Not found: ${id}`
139
- });
140
- return row as User;
141
- })
142
- );
143
-
144
- const create = Effect.fn('UserRepository.create')(
145
- (data: CreateUserData): Effect.Effect<User, DuplicateUser> =>
146
- Effect.gen(function* () {
147
- return yield* db.insert('users', data);
148
- })
149
- );
150
-
151
- return { findById, create };
152
- })
153
- }
92
+ class User extends Schema.Class<User>('User')({ id: Schema.String }) {}
93
+ class UserNotFound extends Schema.TaggedError<UserNotFound>()(
94
+ 'UserNotFound', { id: Schema.String }
154
95
  ) {}
96
+ class Database extends Context.Service<Database, {
97
+ readonly findUser: (id: string) => Effect.Effect<User, UserNotFound>;
98
+ }>()('app/Database') {}
99
+ class UserRepository extends Context.Service<UserRepository, {
100
+ readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
101
+ }>()('app/UserRepository') {}
102
+
103
+ const layer = Layer.effect(UserRepository, Effect.gen(function* () {
104
+ const db = yield* Database;
105
+ // Before: (id: string) => Effect.gen(function* () { ... })
106
+ const findById = Effect.fn('UserRepository.findById')(function* (id: string) {
107
+ return yield* db.findUser(id);
108
+ });
109
+ return UserRepository.of({ findById });
110
+ }));
155
111
  ```
156
112
 
157
113
  ## When NOT to use Effect.fn
@@ -16,7 +16,7 @@ suggestSkills:
16
16
 
17
17
  ```haskell
18
18
  -- Transformation
19
- Schema.Struct :: { fields } -> Schema { fields } -- anonymous, no constructor
19
+ Schema.Struct :: { fields } -> Schema { fields } -- structural value with make helpers
20
20
  Schema.Class :: String -> { fields } -> Class -- named, constructable, extensible
21
21
 
22
22
  -- Pattern
@@ -25,7 +25,7 @@ bad = Schema.Struct({
25
25
  id: Schema.String,
26
26
  name: Schema.String
27
27
  })
28
- -- anonymous type, no constructor, no instanceof
28
+ -- structural type, make helpers, no class identity / instanceof
29
29
 
30
30
  good :: Schema
31
31
  good = class User extends Schema.Class<User>("User")({
@@ -49,6 +49,6 @@ extend = class Admin extends User.extend<Admin>("Admin")({
49
49
  }) {}
50
50
  ```
51
51
 
52
- `Schema.Struct` produces an anonymous schema without a constructor or `instanceof` support. `Schema.Class` provides a named type, constructor, extensibility, and optional annotation support when docs or introspection benefit from it. Prefer `Schema.Class` for decoded domain/API shapes, union members, and values that need identity. `Schema.Struct` remains appropriate for local structural composition, configuration internals, and schemas where class identity adds no value, so review this informational finding in context.
52
+ Every schema, including `Schema.Struct`, has `make`, `makeOption`, and `makeEffect` construction helpers. `Schema.Class` additionally provides a named class, `new` / `instanceof` identity, and class extension. Prefer it for decoded domain/API shapes, union members, and values that need identity. `Schema.Struct` remains appropriate for local structural composition, configuration internals, and schemas where class identity adds no value, so review this informational finding in context.
53
53
 
54
- References: EF-3, EF-33 in effect-first-development.md
54
+ References: EF-3 in effect-first-development.md
@@ -30,7 +30,7 @@ suggestSkills:
30
30
 
31
31
  ```haskell
32
32
  -- Transformation
33
- throw :: Error -> ⊥ -- untyped, uncatchable by Effect
33
+ throw :: Error -> ⊥ -- defect, outside the typed error channel
34
34
  yield* Effect.fail :: TaggedError -> E ⊥ E -- typed, catchable via catchTag
35
35
  ```
36
36
 
@@ -43,3 +43,7 @@ test = do
43
43
  ```
44
44
 
45
45
  Direct `Date` usage is non-deterministic. Use `DateTime.now` or `Clock.currentTimeMillis` for testable time operations via `TestClock`.
46
+
47
+ Use wall-clock time for timestamps and `Clock.monotonicTimeNanos` for elapsed
48
+ durations; wall-clock adjustments must not
49
+ change a measured interval.
@@ -35,7 +35,7 @@ ChildProcessSpawner :: Effect a ChildProcessSpawner -- typed I/O, scoped lifeti
35
35
  ```
36
36
 
37
37
  ```haskell
38
- -- Pattern (Effect v4 — effect/unstable/process)
38
+ -- Pattern (Effect v4 — effect/process)
39
39
  bad :: () → IO String
40
40
  bad = exec "git" ["status"] (cb) -- callback, untyped, no cancellation
41
41
 
@@ -46,7 +46,7 @@ good = do
46
46
  -- typed output, error channel, scoped lifetime
47
47
  ```
48
48
 
49
- Direct `child_process` imports give you callback APIs, manual lifecycle, and no error channel. Use `ChildProcessSpawner` and `ChildProcess` from `effect/unstable/process` for typed errors, scoped resource lifetime, and platform-agnostic process spawning.
49
+ Direct `child_process` imports give you callback APIs, manual lifecycle, and no error channel. Use `ChildProcessSpawner` and `ChildProcess` from `effect/process` for typed errors, scoped resource lifetime, and platform-agnostic process spawning.
50
50
 
51
51
  **Exceptions:**
52
52
 
@@ -49,26 +49,28 @@ good url = pipe
49
49
 
50
50
  ```haskell
51
51
  -- Composable request building
52
- request :: Effect HttpClientRequest HttpBodyError
52
+ request :: HttpClientRequest
53
53
  request = pipe
54
- (HttpClientRequest.post "/api/users")
55
- (HttpClientRequest.bodyJson { name: "Alice" })
54
+ (HttpClientRequest.get "https://api.example.com/users")
55
+ (HttpClientRequest.setHeader "accept" "application/json")
56
56
 
57
57
  -- With retry, timeout, tracing
58
58
  resilient :: Effect Response (HttpClient | HttpBodyError)
59
59
  resilient = pipe
60
- request
61
- (Effect.flatMap HttpClient.execute)
60
+ (HttpClient.execute request)
62
61
  (Effect.retry (Schedule.recurs 3))
63
62
  (Effect.timeout (Duration.seconds 10))
64
63
 
65
64
  -- Provide platform layer at entry point
66
65
  main = program
67
- & provide BunHttpClient.layer -- or NodeHttpClient.layer
66
+ & provide BunHttpClient.layer -- or NodeHttpClient.layerUndici
68
67
  ```
69
68
 
70
69
  Direct `http` / `https` imports give you callback APIs, manual TLS plumbing, and no error channel. Use Effect's `HttpClient`, `HttpClientRequest`, and `HttpClientResponse` for typed errors, composable request building, declarative retry/timeout, and testability via layer substitution.
71
70
 
71
+ Import these modules from `effect/http`. The retry example assumes an idempotent
72
+ GET; only retry mutations when their idempotency is established by the contract.
73
+
72
74
  **Exceptions:**
73
75
 
74
76
  - Platform-specific layers that implement `HttpClient.HttpClient`
@@ -34,12 +34,11 @@ bad = floor (Math.random * 100) -- R = ∅, untestable
34
34
  good :: Effect Int Random
35
35
  good = Random.nextIntBetween 0 100 -- R ⊃ Random, deterministic in tests
36
36
 
37
- -- In tests
38
- test :: Effect () TestRandom
39
- test = do
40
- TestRandom.feedInts [42, 7, 13] -- deterministic sequence
41
- result ← good
42
- assert (result == 42)
37
+ -- Repeatable sequence in tests
38
+ seeded = good & Random.withSeed "example-seed"
43
39
  ```
44
40
 
45
- `Math.random()` is non-deterministic. Use `Random` service for reproducible randomness via `TestRandom.feed*` in tests.
41
+ `Math.random()` bypasses Effect's random service. Use `Random.withSeed` for
42
+ repeatable sequences in tests, or provide a controlled `Random.Random` service
43
+ when the exact generated values matter. `Random.nextIntBetween(min, max)` includes
44
+ both endpoints by default; pass `{ halfOpen: true }` to exclude the upper bound.
@@ -8,14 +8,17 @@ You are an Effect TypeScript expert specializing in the `Chat` module for statef
8
8
  ## Effect Source Reference
9
9
 
10
10
  The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
11
- Browse and read files there directly to look up APIs, types, and implementations.
11
+ Use `git show effect@4.0.0:<path>` in that checkout for this skill's baseline;
12
+ main may be ahead. Keep `effect` and `@effect/*` packages on the same version.
13
+ The `effect/ai` APIs are tagged `@stability unstable`: minor releases may break
14
+ them even though the import path no longer contains `unstable`.
12
15
 
13
16
  Reference this for:
14
17
 
15
- - Chat module source: `packages/effect/src/unstable/ai/Chat.ts`
18
+ - Chat module source: `packages/effect/src/ai/Chat.ts`
16
19
  - Chat usage examples: `ai-docs/src/71_ai/30_chat.ts`
17
20
  - Tool integration examples: `ai-docs/src/71_ai/20_tools.ts`
18
- - Prompt construction: `packages/effect/src/unstable/ai/Prompt.ts`
21
+ - Prompt construction: `packages/effect/src/ai/Prompt.ts`
19
22
 
20
23
  ## Core Imports
21
24
 
@@ -28,7 +31,7 @@ import {
28
31
  Tool,
29
32
  Toolkit,
30
33
  AiError
31
- } from 'effect/unstable/ai';
34
+ } from 'effect/ai';
32
35
  ```
33
36
 
34
37
  ## What Chat Provides
@@ -122,7 +125,10 @@ yield* session.generateText({ prompt: [] });
122
125
 
123
126
  ## Streaming Text
124
127
 
125
- `streamText` returns a `Stream` of `Response.StreamPart` values. History is updated when the stream finalizes. Consume the stream to completion if the full assistant response should become history; if the stream is interrupted early, only the parts emitted before finalization are recorded.
128
+ `streamText` returns a `Stream` of `Response.StreamPart` values. History is updated
129
+ when the stream finalizes by folding the parts received so far. Consume to
130
+ completion to preserve the full response: interrupted text/reasoning sequences
131
+ without an end marker are not folded into history.
126
132
 
127
133
  ```ts
128
134
  yield*
@@ -215,7 +221,7 @@ const restored = yield* Chat.fromExport(data);
215
221
  For automatic persistence (save after every generation), use `Chat.Persistence`:
216
222
 
217
223
  ```ts
218
- import { Persistence } from 'effect/unstable/persistence';
224
+ import { Persistence } from 'effect/persistence';
219
225
 
220
226
  // Create a persistence layer and provide a BackingPersistence implementation
221
227
  const PersistenceLayer = Chat.layerPersisted({ storeId: 'my-chats' }).pipe(
@@ -459,7 +465,7 @@ Each `Chat` instance uses an internal semaphore with 1 permit, ensuring that onl
459
465
 
460
466
  1. **Always provide `LanguageModel.LanguageModel`** — `generateText`, `streamText`, and `generateObject` all require it in context. Provide via `Effect.provide(modelLayer)`.
461
467
  2. **Use `prompt: []` in agentic loops** — After the initial prompt, pass an empty prompt to let the model respond based on accumulated history including tool results.
462
- 3. **Import from `effect/unstable/ai`** — Chat, Prompt, Tool, Toolkit, LanguageModel, and AiError all come from this path.
468
+ 3. **Import from `effect/ai`** — Chat, Prompt, Tool, Toolkit, LanguageModel, and AiError all come from this path.
463
469
  4. **One session = one conversation** — Create separate `Chat` instances for independent conversations. Don't share a session across unrelated threads.
464
470
  5. **Export before shutdown** — Use `exportJson` to persist state. Restore with `Chat.fromJson`.
465
471
  6. **Provide toolkit handlers** — When using tools, the toolkit's handler layer must be provided (e.g., `Layer.provide(ToolsLayer)`).