opencode-effect-enforcer 0.2.3 → 0.2.4

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 (47) hide show
  1. package/README.md +12 -10
  2. package/docs/effect-4.0.0-rc.112.md +316 -0
  3. package/guidance/effect-first-development.md +30 -17
  4. package/guidance/progressive-disclosure-guidance.md +13 -0
  5. package/package.json +3 -2
  6. package/patterns/avoid-direct-tag-checks.md +8 -2
  7. package/patterns/avoid-react-hooks.md +18 -37
  8. package/patterns/effect-run-in-body.md +1 -1
  9. package/patterns/require-effect-concurrency.md +11 -0
  10. package/patterns/use-console-service.md +6 -1
  11. package/skills/effect-ai-language-model/SKILL.md +10 -16
  12. package/skills/effect-ai-prompt/SKILL.md +36 -2
  13. package/skills/effect-ai-provider/SKILL.md +13 -0
  14. package/skills/effect-ai-streaming/SKILL.md +81 -108
  15. package/skills/effect-ai-tool/SKILL.md +50 -87
  16. package/skills/effect-atom-rpc/SKILL.md +9 -2
  17. package/skills/effect-atom-state/SKILL.md +5 -0
  18. package/skills/effect-cache/SKILL.md +32 -0
  19. package/skills/effect-cli/SKILL.md +22 -3
  20. package/skills/effect-concurrency-testing/SKILL.md +7 -9
  21. package/skills/effect-domain-modeling/SKILL.md +208 -1169
  22. package/skills/effect-domain-predicates/SKILL.md +5 -6
  23. package/skills/effect-error-handling/SKILL.md +5 -4
  24. package/skills/effect-http-api/SKILL.md +12 -1
  25. package/skills/effect-http-client/SKILL.md +1 -1
  26. package/skills/effect-http-server/SKILL.md +14 -3
  27. package/skills/effect-layer-design/SKILL.md +22 -56
  28. package/skills/effect-mcp-server/SKILL.md +1 -1
  29. package/skills/effect-pattern-matching/SKILL.md +44 -11
  30. package/skills/effect-platform-abstraction/SKILL.md +1 -1
  31. package/skills/effect-platform-layers/SKILL.md +1 -1
  32. package/skills/effect-rpc-api/SKILL.md +8 -1
  33. package/skills/effect-rpc-client/SKILL.md +20 -6
  34. package/skills/effect-rpc-cluster/SKILL.md +44 -14
  35. package/skills/effect-rpc-server/SKILL.md +32 -5
  36. package/skills/effect-scheduling/SKILL.md +1 -1
  37. package/skills/effect-schema-composition/SKILL.md +69 -15
  38. package/skills/effect-schema-v4/SKILL.md +43 -1
  39. package/skills/effect-scope/SKILL.md +30 -0
  40. package/skills/effect-service-implementation/SKILL.md +10 -4
  41. package/skills/effect-socket/SKILL.md +5 -5
  42. package/skills/effect-sql/SKILL.md +22 -0
  43. package/skills/effect-stream/SKILL.md +32 -1
  44. package/skills/effect-testing/SKILL.md +39 -31
  45. package/skills/effect-workflow/SKILL.md +6 -0
  46. package/patterns/vm-in-wrong-file.md +0 -51
  47. package/skills/effect-react-vm/SKILL.md +0 -675
@@ -91,7 +91,7 @@ const GetCurrentTime = Tool.make('GetCurrentTime', {
91
91
  success: Schema.Number
92
92
  });
93
93
 
94
- const result = Tool.Success<typeof GetCurrentTime>;
94
+ type Result = Tool.Success<typeof GetCurrentTime>;
95
95
  ```
96
96
 
97
97
  **Key Pattern: Tool.make**
@@ -234,13 +234,13 @@ const QueryDatabase = Tool.make('QueryDatabase', {
234
234
  dependencies: [Database]
235
235
  });
236
236
 
237
- type Requirements = Tool.Requirements<typeof QueryDatabase>;
237
+ type Requirements = Tool.HandlerServices<typeof QueryDatabase>;
238
238
  ```
239
239
 
240
240
  **Key Pattern: dependencies**
241
241
 
242
242
  - Array of service tags
243
- - Requirements extracted at type level
243
+ - `Tool.HandlerServices<T>` includes declared dependencies, parameter-decoding services, and result-encoding services
244
244
  - Must be provided when creating handlers
245
245
 
246
246
  ## Creating Toolkits
@@ -392,12 +392,12 @@ const toolkitLayer = toolkit.toLayer({
392
392
  });
393
393
 
394
394
  const program = Effect.gen(function* () {
395
- const handlers = yield* toolkitLayer;
395
+ const handlers = yield* toolkit;
396
396
  const resultStream = yield* handlers.handle('GetWeather', { location: 'NYC' });
397
397
  return resultStream;
398
- }).pipe(Effect.provide(WeatherServiceLive));
398
+ }).pipe(Effect.provide(toolkitLayer), Effect.provide(WeatherServiceLive));
399
399
 
400
- declare const WeatherServiceLive: Layer<WeatherService>;
400
+ declare const WeatherServiceLive: Layer.Layer<WeatherService>;
401
401
  ```
402
402
 
403
403
  **Key Pattern: Handler Context**
@@ -666,7 +666,7 @@ Approved calls execute on the next model call; denied calls are converted to fai
666
666
  import { Effect, Stream } from 'effect';
667
667
 
668
668
  const program = Effect.gen(function* () {
669
- const toolkit = yield* MyToolkitLayer;
669
+ const toolkit = yield* MyToolkit;
670
670
 
671
671
  const resultStream = yield* toolkit.handle('GetWeather', {
672
672
  location: 'San Francisco'
@@ -823,7 +823,7 @@ import { Effect, Match } from 'effect';
823
823
 
824
824
  const executeTool = (toolName: string, params: unknown) =>
825
825
  Effect.gen(function* () {
826
- const toolkit = yield* MyToolkitLayer;
826
+ const toolkit = yield* MyToolkit;
827
827
 
828
828
  const handler = Match.value(toolName).pipe(
829
829
  Match.when('GetWeather', () =>
@@ -843,10 +843,12 @@ const executeTool = (toolName: string, params: unknown) =>
843
843
 
844
844
  ## Complete Example
845
845
 
846
+ <!-- typecheck -->
846
847
  ```typescript
847
848
  import * as Tool from 'effect/unstable/ai/Tool';
848
849
  import * as Toolkit from 'effect/unstable/ai/Toolkit';
849
- import { Effect, Schema, Layer, Stream } from 'effect';
850
+ import * as Schema from 'effect/Schema';
851
+ import { Clock, Context, Effect, Layer, Stream } from 'effect';
850
852
 
851
853
  class UserNotFound extends Schema.TaggedError<UserNotFound>()(
852
854
  'UserNotFound',
@@ -855,40 +857,27 @@ class UserNotFound extends Schema.TaggedError<UserNotFound>()(
855
857
  }
856
858
  ) {}
857
859
 
858
- class Database extends Context.Service<
859
- Database,
860
- {
861
- readonly query: (sql: string) => Effect.Effect<unknown>;
862
- }
863
- >()('Database') {}
860
+ class User extends Schema.Class<User>('User')({
861
+ id: Schema.String,
862
+ name: Schema.String,
863
+ email: Schema.String
864
+ }) {}
865
+
866
+ class UserQuery extends Schema.Class<UserQuery>('UserQuery')({
867
+ userId: Schema.String
868
+ }) {}
869
+
870
+ class Users extends Context.Service<Users, {
871
+ readonly get: (id: string) => Effect.Effect<User, UserNotFound>;
872
+ }>()('app/Users') {}
864
873
 
865
874
  const GetUser = Tool.make('GetUser', {
866
875
  description: 'Retrieve user information by ID',
867
- parameters: Schema.Struct({
868
- userId: Schema.String
869
- }),
870
- success: Schema.Struct({
871
- id: Schema.String,
872
- name: Schema.String,
873
- email: Schema.String
874
- }),
875
- failure: Schema.instanceOf(UserNotFound),
876
+ parameters: UserQuery,
877
+ success: User,
878
+ failure: UserNotFound,
876
879
  failureMode: 'error',
877
- dependencies: [Database]
878
- });
879
-
880
- const CreateUser = Tool.make('CreateUser', {
881
- description: 'Create a new user',
882
- parameters: Schema.Struct({
883
- name: Schema.String,
884
- email: Schema.String
885
- }),
886
- success: Schema.Struct({
887
- id: Schema.String,
888
- name: Schema.String,
889
- email: Schema.String
890
- }),
891
- dependencies: [Database]
880
+ dependencies: [Users]
892
881
  });
893
882
 
894
883
  const GetCurrentTime = Tool.make('GetCurrentTime', {
@@ -896,58 +885,26 @@ const GetCurrentTime = Tool.make('GetCurrentTime', {
896
885
  success: Schema.Number
897
886
  });
898
887
 
899
- const UserToolkit = Toolkit.make(GetUser, CreateUser, GetCurrentTime);
888
+ const UserToolkit = Toolkit.make(GetUser, GetCurrentTime);
900
889
 
901
890
  const UserToolkitLive = UserToolkit.toLayer({
902
- GetUser: ({ userId }) =>
903
- Effect.gen(function* () {
904
- const db = yield* Database;
905
- const user = yield* db.query(
906
- `SELECT * FROM users WHERE id = ?`,
907
- userId
908
- );
909
-
910
- if (!user) {
911
- return yield* Effect.fail(new UserNotFound({ userId }));
912
- }
913
-
914
- return user as { id: string; name: string; email: string };
915
- }),
916
-
917
- CreateUser: ({ name, email }) =>
918
- Effect.gen(function* () {
919
- const db = yield* Database;
920
- const id = crypto.randomUUID();
921
-
922
- yield* db.query(
923
- `INSERT INTO users (id, name, email) VALUES (?, ?, ?)`,
924
- id,
925
- name,
926
- email
927
- );
928
-
929
- return { id, name, email };
930
- }),
931
-
932
- GetCurrentTime: () => Effect.succeed(Date.now())
891
+ GetUser: Effect.fn('Tools.GetUser')(function* ({ userId }) {
892
+ const users = yield* Users;
893
+ return yield* users.get(userId);
894
+ }),
895
+ GetCurrentTime: Effect.fn('Tools.GetCurrentTime')(() => Clock.currentTimeMillis)
933
896
  });
934
897
 
935
- const DatabaseLive = Layer.succeed(Database, {
936
- query: (sql: string, ...params: ReadonlyArray<unknown>) =>
937
- Effect.logInfo(`Query: ${sql}`).pipe(Effect.as({}))
938
- });
898
+ // A complete, typed demo implementation; production supplies a repository adapter.
899
+ const exampleUser = new User({ id: 'user-123', name: 'Alice', email: 'alice@example.com' });
900
+ const UsersTest = Layer.succeed(Users, Users.of({
901
+ get: Effect.fn('Users.get')((userId: string): Effect.Effect<User, UserNotFound> => userId === exampleUser.id
902
+ ? Effect.succeed(exampleUser)
903
+ : Effect.fail(new UserNotFound({ userId })))
904
+ }));
939
905
 
940
906
  const program = Effect.gen(function* () {
941
- const toolkit = yield* UserToolkitLive;
942
-
943
- const createStream = yield* toolkit.handle('CreateUser', {
944
- name: 'Alice',
945
- email: 'alice@example.com'
946
- });
947
-
948
- yield* createStream.pipe(
949
- Stream.runForEach((result) => Effect.log('Created user:', result.result))
950
- );
907
+ const toolkit = yield* UserToolkit;
951
908
 
952
909
  const getStream = yield* toolkit.handle('GetUser', {
953
910
  userId: 'user-123'
@@ -962,9 +919,15 @@ const program = Effect.gen(function* () {
962
919
  yield* timeStream.pipe(
963
920
  Stream.runForEach((result) => Effect.log('Current time:', result.result))
964
921
  );
965
- }).pipe(Effect.provide(DatabaseLive));
922
+ }).pipe(Effect.provide(UserToolkitLive), Effect.provide(UsersTest));
966
923
  ```
967
924
 
925
+ Yield the **Toolkit**, then provide its handler **Layer**. A Layer is not a
926
+ yieldable toolkit instance. Encode tagged failures with the error schema itself
927
+ rather than `Schema.instanceOf(ErrorClass)`, which only checks an existing
928
+ instance and cannot reconstruct it from JSON. Handlers get decoded class values;
929
+ manual `handle` calls accept their encoded representation.
930
+
968
931
  ## Import Patterns
969
932
 
970
933
  **CRITICAL**: Always use namespace imports:
@@ -1005,7 +968,7 @@ import { make as makeToolkit } from 'effect/unstable/ai/Toolkit';
1005
968
  - [ ] Tool.Readonly annotation for read-only tools
1006
969
  - [ ] Tool.Destructive annotation for mutating operations
1007
970
  - [ ] Tool.Idempotent annotation for safe retries
1008
- - [ ] Custom annotations via Tool.annotate
971
+ - [ ] Custom annotations via the tool's `.annotate` method
1009
972
  - [ ] Provider-defined tools for native provider features
1010
973
  - [ ] Toolkit.make with all tools (preferred in v4) or Toolkit.merge for combining tool collections
1011
974
  - [ ] Error handling with catchTag in handlers
@@ -18,7 +18,14 @@ You are an Effect TypeScript expert specializing in `effect/unstable/reactivity/
18
18
  Use this skill when building React (or Atom-based) frontends that consume an existing `RpcGroup`.
19
19
 
20
20
  For the underlying RPC definitions, see the `effect-rpc-cluster` skill.
21
- For Atom fundamentals (`Atom.make`, `family`, `keepAlive`, `AsyncResult`, hydration), see the `effect-atom-state` and `effect-react-vm` skills.
21
+ For Atom fundamentals (`Atom.make`, `family`, `keepAlive`, `AsyncResult`, hydration), see the `effect-atom-state` skill.
22
+
23
+ The underlying RPC protocols require `codecFor` in rc.112. Built-in protocol
24
+ layers supply it; forward it when implementing a custom transport. You can pair
25
+ `RpcSerialization.layerSchemaBinary` on client/server without changing query or
26
+ mutation call sites; see `effect-rpc-client` for frame limits and compatibility.
27
+ Workflow proxy discard RPCs now return execution IDs, so their mutation success
28
+ type is `string`. This is distinct from the client's `{ discard: true }` option.
22
29
 
23
30
  ## Effect Source Reference
24
31
 
@@ -485,4 +492,4 @@ The `query`/`mutation` arguments differ (you pass `groupName, endpointName, requ
485
492
  - 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.
486
493
  - 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.
487
494
  - For typed error recovery, handle declared RPC errors, RPC middleware wire errors, and `RpcClientError`; branch on `_tag` only when the relevant errors are tagged.
488
- - Cross-link to `effect-atom-state` and `effect-react-vm` skills for atom-side patterns; this skill covers only the RPC bridge.
495
+ - Cross-link to `effect-atom-state` for atom-side patterns; this skill covers only the RPC bridge.
@@ -7,6 +7,11 @@ 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
+ At rc.112, `@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
13
+ React bindings live in `@effect/atom-react` (`packages/atom/react` upstream).
14
+
10
15
  ## Effect Source Reference
11
16
 
12
17
  The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
@@ -50,6 +50,7 @@ Choosing the right tool:
50
50
  | Cache one value, no key | `Effect.cached` / `cachedWithTTL` / `cachedInvalidateWithTTL` (section 10) |
51
51
  | Cache values by key | `Cache` |
52
52
  | Cached value owns resources (connection, file handle, subprocess) | `ScopedCache` |
53
+ | Keyed resource retained until each caller's scope releases it | `RcMap` / `LayerMap` |
53
54
  | Batch + deduplicate request-shaped fetches | see the effect-batching skill (`RequestResolver.withCache` / `asCache`) |
54
55
  | Pool of N interchangeable resources checked out per use | `effect/Pool` (not keyed caching) |
55
56
  | Scope/finalizer fundamentals | see the effect-scope skill |
@@ -362,6 +363,37 @@ The full method surface (`get`, `getOption`, `getSuccess`, `set`, `has`, `invali
362
363
 
363
364
  ## 9. Services in Lookups (requireServicesAt)
364
365
 
366
+ ### Retain an existing keyed resource (rc.112)
367
+
368
+ `RcMap.getOption(map, key)` atomically retains a cached entry for the caller's
369
+ `Scope` before awaiting it. It returns `Option.none()` if the entry is missing or
370
+ the map is closed and never starts a lookup for a missing key. An existing
371
+ in-flight entry is awaited; cached/in-flight failures still fail with `E`.
372
+ This is not a non-blocking success-only peek and does not turn failures into absence.
373
+
374
+ Use `LayerMap.contextEffectOption(key)` for the same behavior over keyed layer
375
+ contexts. Its service-class accessor additionally requires the LayerMap service.
376
+ `has` followed by `get` is not an atomic substitute: the entry can disappear
377
+ between calls and `get` may allocate a replacement. Keep the retained resource's
378
+ use inside the caller scope. See `effect-scope` for `Pool.use` when resources are
379
+ interchangeable rather than keyed.
380
+
381
+ <!-- typecheck -->
382
+ ```ts
383
+ import { Duration, Effect, Option, RcMap } from 'effect';
384
+
385
+ const program = Effect.gen(function* () {
386
+ const resources = yield* RcMap.make({
387
+ lookup: (key: string) => Effect.succeed(`resource:${key}`),
388
+ idleTimeToLive: Duration.minutes(1)
389
+ });
390
+ const missing = yield* RcMap.getOption(resources, 'database');
391
+ yield* Effect.scoped(RcMap.get(resources, 'database'));
392
+ const retained = yield* RcMap.getOption(resources, 'database');
393
+ return { missing: Option.isNone(missing), retained };
394
+ }).pipe(Effect.scoped);
395
+ ```
396
+
365
397
  Lookups can use services. `requireServicesAt` controls *where* the type system demands them:
366
398
 
367
399
  - **default (`'construction'` behavior)**: `Cache.make` itself requires `R`; services are captured when the cache is built, and the cache type is `Cache<Key, A, E, never>` — `get` needs nothing.
@@ -201,20 +201,39 @@ Hidden flags parse normally, but generated help, shell completions, and typo sug
201
201
 
202
202
  Bare boolean flags are required. `--verbose` produces `true`, `--no-verbose` produces `false`, and omission produces `CliError.MissingOption`. Add `Flag.withDefault(false)` for ordinary opt-in switch behavior, or use `Flag.optional` / a config or prompt fallback when absence has separate meaning.
203
203
 
204
- ### Prompt Defaults and Prefixes
204
+ ### Prompt Defaults and Themes
205
205
 
206
+ <!-- typecheck -->
206
207
  ```typescript
208
+ import { Effect } from 'effect';
207
209
  import { Prompt } from 'effect/unstable/cli';
208
210
 
209
211
  Prompt.integer({ message: 'Count', default: 42 });
210
212
  Prompt.file({ message: 'Pick file', default: '/workspace/config.json' });
211
213
 
212
- // The default prefix is "?"; an empty string omits it.
213
- Prompt.text({ message: 'Name', prefix: '>' });
214
+ // A local override merges over the context theme.
215
+ const name = Prompt.text({ message: 'Name', theme: { prefix: '>' } });
216
+
217
+ // Provide once to theme all prompts in a command or application.
218
+ const themed = name.pipe(
219
+ Effect.provideService(Prompt.Theme, Prompt.makeTheme({ tick: '✓' }))
220
+ );
214
221
  ```
215
222
 
216
223
  Integer prompt defaults are editable and Enter submits the default if unchanged. `Prompt.file` resolves/selects the default as the initial path.
217
224
 
225
+ In rc.112, per-prompt `prefix` is replaced by `theme?: Partial<Prompt.Theme>`.
226
+ `Prompt.Theme` is a context reference with platform defaults; `Prompt.makeTheme`
227
+ builds a complete theme. Local fields override the context theme. Theme fields
228
+ include prompt symbols, `passwordMask`, and ANSI color values. The default
229
+ prefix is `?`; `theme: { prefix: '' }` omits it.
230
+
231
+ `autoComplete` and `file` now insert `j`/`k` into filter text. Navigate with arrow
232
+ keys (or Ctrl-P/Ctrl-K for up, Ctrl-N for down). Wizard command output redacts
233
+ password prompt values. Completion generators preserve quoted/spaced/Unicode
234
+ choices across Bash (including 3.2), Fish, and Zsh; let the generator escape
235
+ choice values rather than pre-escaping your domain values.
236
+
218
237
  ## Commands
219
238
 
220
239
  ### Creating Commands
@@ -45,7 +45,7 @@ it.effect('fiber polling with yieldNow', () =>
45
45
 
46
46
  yield* latch.open;
47
47
 
48
- expect(yield* fiber.await).toEqual(Exit.void);
48
+ expect(yield* Fiber.await(fiber)).toEqual(Exit.void);
49
49
  })
50
50
  );
51
51
  ```
@@ -212,12 +212,12 @@ it.effect('should publish user created event', () =>
212
212
  type: 'created',
213
213
  userId: 'user-123'
214
214
  });
215
- })
215
+ }).pipe(
216
+ Effect.provide(UserServiceLive),
217
+ Effect.provide(Layer.succeed(EventBus, pubsub))
218
+ )
216
219
  );
217
- }).pipe(
218
- Effect.provide(UserServiceLive),
219
- Effect.provide(Layer.succeed(EventBus, pubsub))
220
- )
220
+ })
221
221
  );
222
222
  ```
223
223
 
@@ -333,9 +333,7 @@ it.effect('subscriptions are interruptible', () =>
333
333
 
334
334
  const result = yield* Fiber.await(fiber);
335
335
 
336
- expect(Exit.isFailure(result) && Pull.isDoneCause(result.cause)).toBe(
337
- true
338
- );
336
+ expect(Exit.hasInterrupts(result)).toBe(true);
339
337
  })
340
338
  );
341
339
  ```