opencode-effect-enforcer 0.2.8 → 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 (72) 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 +8 -6
  5. package/guidance/progressive-disclosure-guidance.md +15 -7
  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 +1 -1
  19. package/patterns/prefer-effect-fn.md +21 -65
  20. package/patterns/prefer-schema-class.md +3 -3
  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
@@ -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
@@ -15,6 +15,9 @@ the yielded service and `get` / `contextEffect` / `contextEffectOption`: an entr
15
15
  can fail when reacquired after successful preloading. Invalidating an active
16
16
  RcMap/LayerMap entry releases it after its last borrower closes; a replacement
17
17
  entry remains independently owned.
18
+ Preload keys with zero `idleTimeToLive` are skipped, including the default TTL.
19
+ Set a non-zero TTL when construction must eagerly acquire and validate them;
20
+ otherwise acquisition failures surface on first use.
18
21
 
19
22
  The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
20
23
  Browse and read files there directly to look up APIs, types, and implementations.
@@ -47,7 +50,7 @@ interface Cache<Key, A, E = never, R = never> {
47
50
  How to think about it:
48
51
 
49
52
  - **Entries store the lookup `Exit`** — successes *and failures* are cached. A failed lookup keeps failing from cache until the entry expires, is invalidated, or is refreshed.
50
- - **Concurrent `get`s of the same missing key share one lookup.** The first caller runs the lookup on its own fiber; the rest await the same `Deferred`.
53
+ - **Concurrent `get`s of the same missing key share one lookup fiber.** Each caller waits independently; interrupting one waiter does not cancel work still needed by another. The last departing waiter interrupts a pending lookup.
51
54
  - **Insertion-ordered map = LRU.** Reads move the entry to the back; when capacity is exceeded the oldest entries are evicted.
52
55
  - **TTL is computed per entry** from the lookup `Exit` and the key, against the fiber's `Clock` — `TestClock` works. Expiry is lazy: entries are removed when next touched.
53
56
  - **`ScopedCache`** is the same model where each entry additionally owns a `Scope`: resources acquired during the lookup live exactly as long as the entry is cached.
@@ -191,7 +194,7 @@ If an older in-flight lookup is interrupted after `set` installs a newer value,
191
194
  yield* Cache.invalidate(cache, 'k'); // remove one key; no-op if absent
192
195
  yield* Cache.invalidateAll(cache); // clear everything
193
196
 
194
- // Conditional: removes only a *resolved successful* value matching the predicate.
197
+ // Conditional: awaits a pending entry, then removes a successful value matching the predicate.
195
198
  // Returns false for missing, expired, failed, or non-matching entries.
196
199
  const removed: boolean = yield* Cache.invalidateWhen(cache, 'k', (v) => v.stale);
197
200
  ```
@@ -205,7 +208,7 @@ const fresh = yield* Cache.refresh(cache, 'k');
205
208
  - Always invokes the lookup, even for an unexpired entry, and resets the TTL from the new exit.
206
209
  - For an **existing** key, the old entry keeps serving `get` callers until the new lookup completes — built-in stale-while-revalidate.
207
210
  - For a **missing** key, a pending entry is inserted immediately; concurrent `get`s wait on it.
208
- - Concurrent `refresh` calls are **not deduplicated** — each runs the lookup independently (only `get` dedups).
211
+ - `Cache.refresh` calls are **not deduplicated** — each runs the lookup independently. `ScopedCache.refresh` of a missing key delegates to `get` and shares that pending lookup; refreshes of existing keys run independently.
209
212
 
210
213
  ---
211
214
 
@@ -234,6 +237,11 @@ yield* Cache.get(cache, 'd'); // evicts 'b'
234
237
  - `Duration.infinity` (the default) means no expiry. `Duration.zero` means the entry expires immediately — effectively "do not cache this result".
235
238
  - `refresh` and re-`get`-after-expiry both restart the TTL clock; plain `get` hits do not extend it (no sliding expiration).
236
239
 
240
+ `Cache.get` does not retain a zero-TTL result after completion. A synchronous
241
+ zero-TTL lookup does not occupy capacity and evict a live entry; a pending lookup
242
+ still occupies capacity while it is shared. `ScopedCache` has the distinct lazy
243
+ resource-release behavior described in section 8.
244
+
237
245
  Per-key and per-result TTL via `makeWith`:
238
246
 
239
247
  ```ts
@@ -274,7 +282,7 @@ const robust = yield* Cache.makeWith(fetchUser, {
274
282
  });
275
283
  ```
276
284
 
277
- **Interruption poisons entries the same way**: the lookup runs on the fiber of the caller that triggered the miss. If that fiber is interrupted mid-lookup, the entry's deferred completes with the interrupt exit — concurrent waiters fail with it, and with an infinite TTL later `get`s keep replaying the interrupt instead of retrying. The exit-aware TTL above also fixes this, because an interrupt is a non-success exit (`Exit.isSuccess(exit) === false`) and gets a zero/short TTL.
285
+ **Interruption is not cached.** Missing-key lookups run in daemon fibers, shared by their waiters. One caller's interruption leaves the lookup alive while another caller is waiting. When the last waiter leaves before completion, the lookup is interrupted and its entry removed; a later `get` starts again. `ScopedCache` also closes that lookup's entry scope. Children forked with `forkChild` inside the lookup end with the lookup fiber, not with the requesting caller; use the entry scope for resource-lifetime background work.
278
286
 
279
287
  `getSuccess`, `values`, and `entries` skip failed entries; `getOption` and `get` propagate the cached error; `invalidateWhen` returns `false` for failed entries (the predicate only sees successes).
280
288
 
@@ -341,6 +349,10 @@ The entry's scope is closed — releasing everything the lookup acquired — whe
341
349
  4. **replaced** (`set` over an existing key; `refresh` closes the old entry's scope after the new lookup completes),
342
350
  5. **orphaned by cache close** (the owning scope closes).
343
351
 
352
+ A pending shared lookup also closes its entry scope when its last waiter leaves.
353
+ Interrupting an existing-key `refresh` closes the replacement scope and leaves
354
+ the previous entry intact.
355
+
344
356
  ```ts
345
357
  const tracker: Array<string> = [];
346
358
  const cache = yield* ScopedCache.make({
@@ -434,7 +446,7 @@ const cacheB = yield* Cache.make({
434
446
  const row = yield* Cache.get(cacheB, 1).pipe(Effect.provideService(Db, dbImpl));
435
447
  ```
436
448
 
437
- At runtime the lookup always sees the construction-time context merged with the caller's context (the caller's services win on conflicts). Tracing is connected: the lookup runs on the calling fiber, so spans created inside the lookup are children of the caller's current span. Remember the lookup runs **once per miss** — only the first caller's context matters for a given entry.
449
+ At runtime the lookup sees the construction-time context merged with the initiating caller's context (the caller's services win on conflicts). The shared lookup fiber inherits that caller's tracing context. Remember the lookup runs **once per miss** — later waiters do not replace the context for a pending entry.
438
450
 
439
451
  The same option exists on `ScopedCache.make`/`makeWith`.
440
452
 
@@ -597,9 +609,9 @@ it.effect('expires entries after the TTL', () =>
597
609
  2. **v4 renames type parameters, it does not reorder them** — v3's `Cache<Key, Value, Error>` becomes `Cache<Key, A, E, R>`: same value-before-error order with a services parameter appended. Only the pre-v3 `@effect/io` era used `Cache<Key, Error, Value>`.
598
610
  3. **`Cache.makeWith(lookup, options)` vs `ScopedCache.makeWith({ lookup, ...options })`** — `Cache.makeWith` takes the lookup as a separate first argument; `ScopedCache.makeWith` (and both `make`s) take one options object containing `lookup`.
599
611
  4. **Failures are cached with the default infinite TTL** — one transient lookup error fails that key forever. Use `makeWith` with an exit-aware TTL (`Exit.isSuccess(exit) ? ttl : Duration.zero`).
600
- 5. **Interrupting the fiber that started a lookup poisons the entry** — the lookup runs on the first caller's fiber; if it is interrupted, the interrupt exit is cached and replayed to waiters and later `get`s. The exit-aware TTL in (4) also covers interrupts.
612
+ 5. **Confusing caller cancellation with lookup cancellation** — a shared missing-key lookup survives while any waiter remains. The last departing waiter interrupts pending work; interrupted entries are removed, and `ScopedCache` closes their scopes. Failure TTL policy is still needed for ordinary failures.
601
613
  6. **`timeToLive` in `make` is a `Duration.Input` value, not a function** — the `(exit, key) => Duration.Input` form only exists on `makeWith`; passing a function to `make` won't compile.
602
- 7. **`refresh` is not deduplicated** — concurrent `refresh` calls each run the lookup; only `get` shares in-flight lookups. Serialize refreshes yourself if the lookup is expensive.
614
+ 7. **Assuming all refreshes deduplicate** — `Cache.refresh` and existing-key `ScopedCache.refresh` run independently. Only missing-key `ScopedCache.refresh` delegates to the shared `get` path. Serialize expensive refreshes if needed.
603
615
  8. **Don't mutate key objects after first use** — plain objects compare structurally in v4 (equal-content literals do hit), but `Equal`/`Hash` cache their results per object, so a mutated key misbehaves silently. Prefer primitives or immutable `Data.Class`/`Schema.Class` keys.
604
616
  9. **Don't `Effect.acquireRelease` inside a plain `Cache` lookup** — `Scope` leaks into `R` and finalizers attach to whatever outer scope is around, not to the entry: nothing is released on eviction/invalidation. Use `ScopedCache`, which provides a per-entry scope.
605
617
  10. **ScopedCache values die with their entry** — after `invalidate`/eviction/expiry-purge the value's finalizers have run. Don't hold the value beyond the entry's lifetime; use `effect/Pool` for checkout semantics.
@@ -20,8 +20,11 @@ scalars containing colon-whitespace or a trailing colon: quote those values.
20
20
 
21
21
  ## Import Pattern
22
22
 
23
+ The CLI APIs are marked `@stability unstable` even at the stable package release.
24
+ Inspect `effect@4.0.0:packages/effect/src/cli/` for this baseline's contracts.
25
+
23
26
  ```typescript
24
- import { Argument, Command, Flag, Prompt } from 'effect/unstable/cli';
27
+ import { Argument, Command, Flag, Prompt } from 'effect/cli';
25
28
  ```
26
29
 
27
30
  Platform services and runtime for the entry point:
@@ -37,7 +40,7 @@ Positional arguments are parsed in order. Use `Flag.Boolean` for toggles or `Arg
37
40
  ### Constructors
38
41
 
39
42
  ```typescript
40
- import { Argument } from 'effect/unstable/cli';
43
+ import { Argument } from 'effect/cli';
41
44
 
42
45
  Argument.String('name'); // string
43
46
  Argument.Int('count'); // number (integer)
@@ -64,7 +67,7 @@ Argument.FileSchema('config', MySchema); // reads and validates file via Schema
64
67
  ### Combinators
65
68
 
66
69
  ```typescript
67
- import { Argument } from 'effect/unstable/cli';
70
+ import { Argument } from 'effect/cli';
68
71
 
69
72
  // Description for help text
70
73
  Argument.String('file').pipe(Argument.withDescription('Input file'));
@@ -121,10 +124,16 @@ Argument.Int('count').pipe(
121
124
 
122
125
  Flags are named options with `--name` or `-alias` syntax.
123
126
 
127
+ Negative numeric tokens such as `-3`, `-3.70`, `-.5`, and `-1e-3` are values,
128
+ not short-option bundles: `--lon -3.70` works with `Flag.Finite('lon')`.
129
+ Use `--` to pass trailing operands that otherwise look like options; a lone
130
+ `-` is also retained as a value. Numeric lexing does not replace the chosen
131
+ primitive's validation.
132
+
124
133
  ### Constructors
125
134
 
126
135
  ```typescript
127
- import { Flag } from 'effect/unstable/cli';
136
+ import { Flag } from 'effect/cli';
128
137
 
129
138
  Flag.Boolean('verbose'); // required: --verbose / --no-verbose; omission fails
130
139
  Flag.String('config'); // --config value
@@ -152,7 +161,7 @@ Flag.KeyValuePair('env'); // --env FOO=bar → Record<string, string>
152
161
  ### Combinators
153
162
 
154
163
  ```typescript
155
- import { Flag } from 'effect/unstable/cli';
164
+ import { Flag } from 'effect/cli';
156
165
 
157
166
  // Alias
158
167
  Flag.Boolean('verbose').pipe(
@@ -217,7 +226,7 @@ Bare boolean flags are required. `--verbose` produces `true`, `--no-verbose` pro
217
226
  <!-- typecheck -->
218
227
  ```typescript
219
228
  import { Effect } from 'effect';
220
- import { Prompt } from 'effect/unstable/cli';
229
+ import { Prompt } from 'effect/cli';
221
230
 
222
231
  Prompt.Int({ message: 'Count', default: 42 });
223
232
  Prompt.File({ message: 'Pick file', default: '/workspace/config.json' });
@@ -253,7 +262,7 @@ choice values rather than pre-escaping your domain values.
253
262
 
254
263
  ```typescript
255
264
  import { Console, Effect } from 'effect';
256
- import { Argument, Command, Flag } from 'effect/unstable/cli';
265
+ import { Argument, Command, Flag } from 'effect/cli';
257
266
 
258
267
  // Simple command (no config, no handler)
259
268
  const version = Command.make('version');
@@ -509,7 +518,7 @@ Command.provideSync(MyService, (config) => makeMyService(config.env));
509
518
  ```typescript
510
519
  import { NodeRuntime, NodeServices } from '@effect/platform-node';
511
520
  import { Effect } from 'effect';
512
- import { Command, Flag } from 'effect/unstable/cli';
521
+ import { Command, Flag } from 'effect/cli';
513
522
 
514
523
  const myCommand = Command.make(
515
524
  'myapp',