opencode-effect-enforcer 0.2.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,554 @@
1
+ ---
2
+ name: effect-optics
3
+ description: Use Effect Optic for composable, type-safe access and immutable updates to nested data structures. Covers Iso, Lens, Prism, Optional, and Traversal — when to use each, how to compose them, and practical patterns for deep updates, tagged unions, and filtered collections.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in functional optics for immutable data access and transformation.
7
+
8
+ ## Effect Source Reference
9
+
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.
12
+
13
+ Key reference files:
14
+
15
+ - `packages/effect/OPTIC.md` — full guide with examples
16
+ - `packages/effect/src/Optic.ts` — API surface and JSDoc
17
+
18
+ ## Core Import
19
+
20
+ ```ts
21
+ import { Optic } from 'effect';
22
+ ```
23
+
24
+ All optic types and constructors live under `Optic`. Supporting types come from `Result`, `Option`, and `Schema` as needed.
25
+
26
+ ## Optic Type Hierarchy
27
+
28
+ Strongest to weakest — composing two optics produces the weaker kind:
29
+
30
+ ```
31
+ Iso > Lens | Prism > Optional
32
+ ```
33
+
34
+ | Optic | get | set/replace | Use case |
35
+ | ------------- | ----------- | --------------------------- | --------------------------------------------------------- |
36
+ | **Iso** | Always | Always (no original needed) | Lossless two-way conversion (e.g. Celsius <-> Fahrenheit) |
37
+ | **Lens** | Always | Always (needs original `S`) | Always-present field in a struct |
38
+ | **Prism** | May fail | Always (no original needed) | Union variant, validated subset |
39
+ | **Optional** | May fail | May fail | General case — both reading and writing can fail |
40
+ | **Traversal** | Zero+ items | Zero+ items | Multiple elements in an array/collection |
41
+
42
+ **Traversal** is modeled as `Optional<S, ReadonlyArray<A>>` — not a separate optic kind. Use `.forEach()` and `.modifyAll()` to operate on individual elements.
43
+
44
+ ## Starting an Optic Chain
45
+
46
+ Always begin with `Optic.id<S>()` — the identity Iso on type `S`:
47
+
48
+ ```ts
49
+ import { Optic } from 'effect';
50
+
51
+ type State = { user: { name: string; age: number } };
52
+
53
+ const _age = Optic.id<State>().key('user').key('age');
54
+ ```
55
+
56
+ ## Builder Methods (Chainable)
57
+
58
+ These are called on any optic instance to drill deeper or narrow focus:
59
+
60
+ ### `.key(k)` — Lens into a struct/tuple field
61
+
62
+ Always-present field. Returns a Lens (from a Lens) or Optional (from an Optional). Does NOT work on union types.
63
+
64
+ ```ts
65
+ type S = { readonly a: { readonly b: number } };
66
+ const _b = Optic.id<S>().key('a').key('b');
67
+
68
+ _b.get({ a: { b: 42 } }); // 42
69
+ _b.replace(99, { a: { b: 42 } }); // { a: { b: 99 } }
70
+ ```
71
+
72
+ Tuples use numeric keys:
73
+
74
+ ```ts
75
+ type S = readonly [string, number];
76
+ const _0 = Optic.id<S>().key(0);
77
+ _0.get(['hello', 42]); // "hello"
78
+ ```
79
+
80
+ ### `.optionalKey(k)` — Lens that removes key on `undefined`
81
+
82
+ Like `.key()` but setting `undefined` removes the key from the struct (or splices from a tuple):
83
+
84
+ ```ts
85
+ type S = { readonly a?: number };
86
+ const _a = Optic.id<S>().optionalKey('a');
87
+
88
+ _a.replace(2, {}); // { a: 2 }
89
+ _a.replace(undefined, { a: 1 }); // {}
90
+ ```
91
+
92
+ ### `.at(k)` — Optional into a record/array index
93
+
94
+ For records or arrays where the key/index might not exist. Both get and set can fail. Always returns an Optional.
95
+
96
+ ```ts
97
+ type Env = { [key: string]: number };
98
+ const _x = Optic.id<Env>().at('x');
99
+
100
+ _x.replace(2, { x: 1 }); // { x: 2 }
101
+ // getResult fails if "x" is absent
102
+ ```
103
+
104
+ ```ts
105
+ type S = ReadonlyArray<number>;
106
+ const _0 = Optic.id<S>().at(0);
107
+
108
+ _0.replace(3, [1, 2]); // [3, 2]
109
+ ```
110
+
111
+ ### `.tag(variant)` — Prism into a tagged union variant
112
+
113
+ Narrows focus to the variant with the matching `_tag`. No-ops on non-matching variants:
114
+
115
+ ```ts
116
+ type Shape =
117
+ | { readonly _tag: 'Circle'; readonly radius: number }
118
+ | { readonly _tag: 'Rect'; readonly width: number };
119
+
120
+ const _radius = Optic.id<Shape>().tag('Circle').key('radius');
121
+
122
+ _radius.replace(10, { _tag: 'Circle', radius: 5 }); // { _tag: "Circle", radius: 10 }
123
+ _radius.replace(10, { _tag: 'Rect', width: 5 }); // { _tag: "Rect", width: 5 } (unchanged)
124
+ ```
125
+
126
+ ### `.pick(keys)` / `.omit(keys)` — Lens into a subset of struct keys
127
+
128
+ ```ts
129
+ type S = { readonly a: number; readonly b: number; readonly c: number };
130
+
131
+ const _ac = Optic.id<S>().pick(['a', 'c']);
132
+ _ac.replace({ a: 4, c: 5 }, { a: 1, b: 2, c: 3 }); // { a: 4, b: 2, c: 5 }
133
+
134
+ const _ac2 = Optic.id<S>().omit(['b']);
135
+ // same result
136
+ ```
137
+
138
+ ### `.notUndefined()` — Filter out `undefined`
139
+
140
+ ```ts
141
+ const _defined = Optic.id<number | undefined>().notUndefined();
142
+ // getResult succeeds on 42, fails on undefined
143
+ ```
144
+
145
+ Calling `notUndefined()` on an `Optional` returns another `Optional`, not a `Prism`, because writing can still fail through the existing optional focus.
146
+
147
+ ### `.check(...checks)` — Validate with Schema checks
148
+
149
+ Adds Schema validation. `getResult` fails when any check fails; `set` passes through unchanged:
150
+
151
+ ```ts
152
+ import { Optic, Schema } from 'effect';
153
+
154
+ const _pos = Optic.id<number>().check(Schema.isGreaterThan(0));
155
+ // getResult succeeds on 5, fails on -1
156
+ ```
157
+
158
+ ### `.refine(guard)` — Narrow by type guard
159
+
160
+ ```ts
161
+ type B = { readonly _tag: 'b'; readonly b: number };
162
+ type S = { readonly _tag: 'a'; readonly a: string } | B;
163
+
164
+ const _b = Optic.id<S>().refine((s: S): s is B => s._tag === 'b', {
165
+ expected: `"b" tag`
166
+ });
167
+ ```
168
+
169
+ ### `.forEach(f)` — Traverse array elements
170
+
171
+ Available when focus is `ReadonlyArray<A>`. The callback receives an `Iso<A, A>` to drill into each element:
172
+
173
+ ```ts
174
+ import { Optic, Schema } from 'effect';
175
+
176
+ type S = { readonly a: ReadonlyArray<number> };
177
+
178
+ const _positive = Optic.id<S>()
179
+ .key('a')
180
+ .forEach((item) => item.check(Schema.isGreaterThan(0)));
181
+
182
+ _positive.modifyAll((n) => n + 1)({ a: [1, -2, 3] });
183
+ // { a: [2, -2, 4] }
184
+ ```
185
+
186
+ ### `.compose(optic)` — Compose with another optic
187
+
188
+ ```ts
189
+ import { Optic, Option } from 'effect';
190
+
191
+ type State = { value: Option.Option<number> };
192
+
193
+ const _inner = Optic.id<State>().key('value').compose(Optic.some());
194
+ // Optional<State, number>
195
+ ```
196
+
197
+ ## Reading Values
198
+
199
+ | Method | Returns | When to use |
200
+ | --------------- | ------------------- | ------------------------------------ |
201
+ | `.get(s)` | `A` | Lens/Iso only — always succeeds |
202
+ | `.getResult(s)` | `Result<A, SchemaIssue.Issue>` | Any optic — explicit structured failure |
203
+
204
+ ```ts
205
+ const _a = Optic.id<{ a: number }>().key('a');
206
+ _a.get({ a: 1 }); // 1
207
+ _a.getResult({ a: 1 }); // Result.succeed(1)
208
+ ```
209
+
210
+ For traversals, use `Optic.getAll`:
211
+
212
+ ```ts
213
+ const getPositive = Optic.getAll(_positive);
214
+ getPositive({ a: [3, -1, 5] }); // [3, 5]
215
+ ```
216
+
217
+ ## Writing Values
218
+
219
+ | Method | Behavior |
220
+ | ---------------------- | ---------------------------------------------------------------------------------------- |
221
+ | `.replace(a, s)` | Returns new `S` with focused value replaced. Silently returns original on focus failure. |
222
+ | `.replaceResult(a, s)` | Returns `Result<S, SchemaIssue.Issue>` — explicit structured failure. |
223
+ | `.modify(f)` | Returns `(s: S) => S`. On focus failure, returns `s` unchanged. |
224
+ | `.modifyAll(f)` | Traversal only. Maps `f` over each focused element. |
225
+ | `.set(a)` | Prism/Iso only — builds `S` from `A` without needing original. |
226
+
227
+ ```ts
228
+ // replace
229
+ _age.replace(31, state);
230
+
231
+ // modify (returns a function)
232
+ const inc = _age.modify((n) => n + 1);
233
+ inc(state);
234
+
235
+ // modifyAll (traversal)
236
+ const doubled = _positive.modifyAll((n) => n * 2);
237
+ doubled({ items: [1, -2, 3] }); // { items: [2, -2, 6] }
238
+ ```
239
+
240
+ ## Standalone Dual Helpers
241
+
242
+ Every derived read/update operation also has a standalone dual function. Use these when composing data pipelines or when passing the operation as a value:
243
+
244
+ | Helper | Data-first | Data-last / pipeable |
245
+ | --------------------- | ----------------------------------------------- | -------------------------------------------- |
246
+ | `Optic.get` | `Optic.get(self, lens)` | `Optic.get(lens)(self)` |
247
+ | `Optic.getResult` | `Optic.getResult(self, optional)` | `Optic.getResult(optional)(self)` |
248
+ | `Optic.set` | `Optic.set(value, prism)` | `Optic.set(prism)(value)` |
249
+ | `Optic.replace` | `Optic.replace(self, optional, value)` | `Optic.replace(optional, value)(self)` |
250
+ | `Optic.replaceResult` | `Optic.replaceResult(self, optional, value)` | `Optic.replaceResult(optional, value)(self)` |
251
+ | `Optic.modify` | `Optic.modify(self, optional, f)` | `Optic.modify(optional, f)(self)` |
252
+ | `Optic.getAll` | `Optic.getAll(self, traversal)` | `Optic.getAll(traversal)(self)` |
253
+ | `Optic.modifyAll` | `Optic.modifyAll(self, traversal, f)` | `Optic.modifyAll(traversal, f)(self)` |
254
+
255
+ ```ts
256
+ import { Optic, pipe } from 'effect';
257
+
258
+ type State = { readonly user: { readonly age: number } };
259
+
260
+ const age = Optic.id<State>().key('user').key('age');
261
+ const state: State = { user: { age: 30 } };
262
+
263
+ Optic.get(state, age); // 30
264
+
265
+ const older = pipe(
266
+ state,
267
+ Optic.modify(age, (value) => value + 1),
268
+ Optic.replace(age, 40)
269
+ );
270
+ ```
271
+
272
+ The standalone helpers delegate to the corresponding instance methods. `replace` / `modify` still return the original source on focus failure, while `replaceResult` / `getResult` retain the structured `SchemaIssue.Issue`.
273
+
274
+ ## Constructors
275
+
276
+ For custom optics beyond the builder chain:
277
+
278
+ ```ts
279
+ // Iso — lossless two-way conversion
280
+ const fahrenheit = Optic.makeIso<number, number>(
281
+ (c) => (c * 9) / 5 + 32, // get: Celsius -> Fahrenheit
282
+ (f) => ((f - 32) * 5) / 9 // set: Fahrenheit -> Celsius
283
+ );
284
+
285
+ // Lens — always-present focus, needs original for replace
286
+ const _first = Optic.makeLens<readonly [string, number], string>(
287
+ (pair) => pair[0],
288
+ (s, pair) => [s, pair[1]]
289
+ );
290
+
291
+ // Prism — focus may not exist, set doesn't need original
292
+ import { Optic, Result, SchemaIssue } from 'effect';
293
+
294
+ const numeric = Optic.makePrism<string, number>((s) => {
295
+ const n = Number(s);
296
+ return Number.isNaN(n)
297
+ ? Result.fail(new SchemaIssue.InvalidValue({ message: 'not a number' }))
298
+ : Result.succeed(n);
299
+ }, String);
300
+
301
+ // Prism from Schema checks
302
+ const posInt = Optic.fromChecks<number>(
303
+ Schema.isGreaterThan(0),
304
+ Schema.isInt()
305
+ );
306
+
307
+ // Optional — both reading and writing can fail
308
+ const atKey = (key: string) =>
309
+ Optic.makeOptional<Record<string, number>, number>(
310
+ (s) =>
311
+ Object.hasOwn(s, key)
312
+ ? Result.succeed(s[key])
313
+ : Result.fail(
314
+ new SchemaIssue.Pointer(
315
+ [key],
316
+ new SchemaIssue.MissingKey(undefined)
317
+ )
318
+ ),
319
+ (a, s) =>
320
+ Object.hasOwn(s, key)
321
+ ? Result.succeed({ ...s, [key]: a })
322
+ : Result.fail(
323
+ new SchemaIssue.Pointer(
324
+ [key],
325
+ new SchemaIssue.MissingKey(undefined)
326
+ )
327
+ )
328
+ );
329
+ ```
330
+
331
+ Since beta.105, all fallible optic operations use `SchemaIssue.Issue`, not `string`. Custom `makePrism` and `makeOptional` implementations must return structured issues. Issues do not format themselves through `toString`; use `SchemaIssue.makeFormatterDefault()` when a human-readable message is needed:
332
+
333
+ ```ts
334
+ const formatIssue = SchemaIssue.makeFormatterDefault();
335
+ const message = Result.match(_a.getResult({}), {
336
+ onSuccess: (value) => `value: ${value}`,
337
+ onFailure: formatIssue
338
+ });
339
+ ```
340
+
341
+ ## Built-in Prisms
342
+
343
+ ```ts
344
+ // Option
345
+ Optic.some<A>(); // Prism<Option<A>, A> — focus on Some
346
+ Optic.none<A>(); // Prism<Option<A>, undefined> — focus on None
347
+
348
+ // Result
349
+ Optic.success<A, E>(); // Prism<Result<A, E>, A>
350
+ Optic.failure<A, E>(); // Prism<Result<A, E>, E>
351
+
352
+ // Record <-> entries
353
+ Optic.entries<A>(); // Iso<Record<string, A>, ReadonlyArray<readonly [string, A]>>
354
+ ```
355
+
356
+ ## Schema Integration
357
+
358
+ Generate optics from Schema definitions with `Schema.toIso`:
359
+
360
+ ```ts
361
+ import { Schema } from 'effect';
362
+
363
+ const schema = Schema.Struct({
364
+ a: Schema.String,
365
+ b: Schema.Number
366
+ });
367
+
368
+ const _b = Schema.toIso(schema).key('b');
369
+ _b.replace(2, { a: 'a', b: 1 }); // { a: "a", b: 2 }
370
+ ```
371
+
372
+ Works with class-based schemas too:
373
+
374
+ ```ts
375
+ class Person extends Schema.Class<Person>('Person')({
376
+ name: Schema.String,
377
+ age: Schema.Number
378
+ }) {}
379
+
380
+ const _name = Schema.toIso(Person).key('name');
381
+ _name.replace('Bob', new Person({ name: 'Alice', age: 30 }));
382
+ // Person { name: "Bob", age: 30 }
383
+ ```
384
+
385
+ ## Practical Patterns
386
+
387
+ ### Deep nested update (define once, reuse everywhere)
388
+
389
+ ```ts
390
+ import { Optic, String } from 'effect';
391
+
392
+ interface Street {
393
+ readonly num: number;
394
+ readonly name: string;
395
+ }
396
+ interface Address {
397
+ readonly city: string;
398
+ readonly street: Street;
399
+ }
400
+ interface Company {
401
+ readonly name: string;
402
+ readonly address: Address;
403
+ }
404
+ interface Employee {
405
+ readonly name: string;
406
+ readonly company: Company;
407
+ }
408
+
409
+ const _streetName = Optic.id<Employee>()
410
+ .key('company')
411
+ .key('address')
412
+ .key('street')
413
+ .key('name');
414
+
415
+ // Reuse with different transforms
416
+ const capitalize = _streetName.modify(String.capitalize);
417
+ const upper = _streetName.modify((s) => s.toUpperCase());
418
+ ```
419
+
420
+ ### Tagged union — safe variant access
421
+
422
+ ```ts
423
+ type Shape =
424
+ | { readonly _tag: 'Circle'; readonly radius: number }
425
+ | {
426
+ readonly _tag: 'Rect';
427
+ readonly width: number;
428
+ readonly height: number;
429
+ };
430
+
431
+ const _circleRadius = Optic.id<Shape>().tag('Circle').key('radius');
432
+ const _rectArea = Optic.id<Shape>().tag('Rect').pick(['width', 'height']);
433
+
434
+ // replace is a no-op on non-matching variants
435
+ _circleRadius.replace(10, { _tag: 'Rect', width: 5, height: 3 });
436
+ // { _tag: "Rect", width: 5, height: 3 } — unchanged
437
+ ```
438
+
439
+ ### Traversal with filtering
440
+
441
+ ```ts
442
+ import { Optic, Schema } from 'effect';
443
+
444
+ type S = {
445
+ readonly todos?: ReadonlyArray<{
446
+ readonly title?: string;
447
+ readonly description: string;
448
+ }>;
449
+ };
450
+
451
+ const _titles = Optic.id<S>()
452
+ .key('todos')
453
+ .notUndefined()
454
+ .forEach((item) => item.key('title').notUndefined());
455
+
456
+ const shout = _titles.modifyAll((t) => t.toUpperCase());
457
+
458
+ shout({
459
+ todos: [
460
+ { title: 'milk', description: 'buy milk' },
461
+ { description: 'buy bread' }
462
+ ]
463
+ });
464
+ // { todos: [{ title: "MILK", description: "buy milk" }, { description: "buy bread" }] }
465
+ ```
466
+
467
+ ### Record traversal via entries
468
+
469
+ ```ts
470
+ import { Optic, Schema } from 'effect';
471
+
472
+ const _positiveValues = Optic.entries<number>().forEach((entry) =>
473
+ entry.key(1).check(Schema.isGreaterThan(0))
474
+ );
475
+
476
+ const inc = _positiveValues.modifyAll((n) => n + 1);
477
+ inc({ a: 0, b: 3, c: -1 }); // { a: 0, b: 4, c: -1 }
478
+ ```
479
+
480
+ ### Debugging focus failures
481
+
482
+ Use `getResult` to see explicit success/failure:
483
+
484
+ ```ts
485
+ import { Optic, Result } from 'effect';
486
+
487
+ type S = { readonly a?: number };
488
+ const _a = Optic.id<S>().at('a');
489
+
490
+ const result = _a.getResult({});
491
+ Result.match(result, {
492
+ onSuccess: (value) => `value: ${value}`,
493
+ onFailure: () => 'no focus'
494
+ });
495
+ // "no focus"
496
+ ```
497
+
498
+ ## Quick Reference Table
499
+
500
+ | Data shape | Builder method |
501
+ | -------------------------------------- | --------------------------------------------------- |
502
+ | Always-present field | `.key("field")` |
503
+ | Optional field (keep `undefined`) | `.key("field")` |
504
+ | Optional field (remove on `undefined`) | `.optionalKey("field")` |
505
+ | Union case by `_tag` | `.tag("Variant")` |
506
+ | Record/array index (may be absent) | `.at(key)` |
507
+ | Filter + update collection items | `.forEach(el => el.check(...))` / `.notUndefined()` |
508
+ | Subset of struct keys | `.pick([...])` / `.omit([...])` |
509
+ | Narrow by type guard | `.refine(guard)` |
510
+ | Option.Some | `.compose(Optic.some())` |
511
+ | Result.Success | `.compose(Optic.success())` |
512
+
513
+ ## Known Limitations
514
+
515
+ - Only works with **plain JavaScript objects** and collections (structs, records, tuples, arrays). Class instances cause runtime errors on `replace`/`modify` (unless generated via `Schema.toIso` on a class schema).
516
+ - `.key()`, `.optionalKey()`, `.at()`, `.pick()`, `.omit()` do NOT work on union types (compile error). Use `.tag()` or `.refine()` first to narrow.
517
+ - No-op updates may still allocate a new root — do not rely on reference identity to detect no-ops.
518
+ - `replace` silently returns the original `S` when the optic cannot focus. Use `replaceResult` for explicit failure detection.
519
+
520
+ ## Anti-Patterns
521
+
522
+ ### WRONG: Repeating paths instead of defining an optic once
523
+
524
+ ```ts
525
+ // Bad — duplicated navigation
526
+ const upper = {
527
+ ...state,
528
+ user: {
529
+ ...state.user,
530
+ profile: {
531
+ ...state.user.profile,
532
+ name: state.user.profile.name.toUpperCase()
533
+ }
534
+ }
535
+ };
536
+ const lower = {
537
+ ...state,
538
+ user: {
539
+ ...state.user,
540
+ profile: {
541
+ ...state.user.profile,
542
+ name: state.user.profile.name.toLowerCase()
543
+ }
544
+ }
545
+ };
546
+ ```
547
+
548
+ ### RIGHT: Define the optic once, reuse for different transforms
549
+
550
+ ```ts
551
+ const _name = Optic.id<S>().key('user').key('profile').key('name');
552
+ const upper = _name.modify((n) => n.toUpperCase())(state);
553
+ const lower = _name.modify((n) => n.toLowerCase())(state);
554
+ ```