fp-pack 0.9.2 → 0.9.3

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.
@@ -1,6 +1,6 @@
1
1
  # fp-pack Agent Role Add-on
2
2
 
3
- Document Version: 0.9.2
3
+ Document Version: 0.9.3
4
4
 
5
5
  ## What This Document Is
6
6
 
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # fp-pack AI Agent Skills
9
9
 
10
- Document Version: 0.9.2
10
+ Document Version: 0.9.3
11
11
 
12
12
  ## ⚠️ Activation Condition (Read First)
13
13
 
@@ -18,7 +18,7 @@ Before following this document:
18
18
  - Check `node_modules/fp-pack` exists
19
19
  - Check existing code imports from `fp-pack` or `fp-pack/stream`
20
20
 
21
- If `fp-pack` is **not** installed, use the projects existing conventions. Do **not** suggest adding fp-pack unless the user asks.
21
+ If `fp-pack` is **not** installed, use the project's existing conventions. Do **not** suggest adding fp-pack unless the user asks.
22
22
 
23
23
  ---
24
24
 
@@ -30,16 +30,159 @@ When writing or editing code in an fp-pack project:
30
30
  - Use `stream/*` for lazy/large/iterable processing; use array/object utils for small/eager data.
31
31
  - Use SideEffect-aware pipes only when you need **early exit**: `pipeSideEffect*` / `pipeAsyncSideEffect*`.
32
32
  - **Never call `runPipeResult` inside a pipeline**; call it at the boundary (event handler, service wrapper, etc.).
33
- - If TypeScript inference gets stuck with a data-last generic, use a small wrapper, an explicit type hint, or `pipeHint`.
33
+ - If TypeScript inference gets stuck with a data-last generic, use a small wrapper or `pipeHint`.
34
34
 
35
- ## Philosophy (What fp-pack Optimizes For)
35
+ ---
36
+
37
+ ## ⛔ CRITICAL MISTAKES TO AVOID (Read This First!)
38
+
39
+ Before you write any code, know these anti-patterns. These are the most common mistakes that break fp-pack pipelines:
40
+
41
+ ### ❌ WRONG: Using `() => value` for constants in pipelines
42
+
43
+ ```ts
44
+ // ❌ BAD - Type inference fails
45
+ const broken = pipe(
46
+ () => [1, 2, 3, 4, 5],
47
+ filter((n: number) => n % 2 === 0) // Error!
48
+ );
49
+
50
+ // ✅ CORRECT - Use from() for constant values
51
+ const works = pipe(
52
+ from([1, 2, 3, 4, 5]),
53
+ filter((n: number) => n % 2 === 0)
54
+ );
55
+ ```
56
+
57
+ ### ❌ WRONG: Using raw values with ifElse/cond
58
+
59
+ ```ts
60
+ // ❌ BAD
61
+ const broken = ifElse((score: number) => score >= 60, 'pass', 'fail');
62
+
63
+ // ✅ CORRECT - Use from() to wrap constant values
64
+ const works = ifElse((score: number) => score >= 60, from('pass'), from('fail'));
65
+ ```
66
+
67
+ ### ❌ WRONG: Calling runPipeResult inside a pipeline
68
+
69
+ ```ts
70
+ // ❌ BAD
71
+ const broken = pipe(validateUser, runPipeResult, processUser);
72
+
73
+ // ✅ CORRECT - Call runPipeResult OUTSIDE the pipeline
74
+ const pipeline = pipeSideEffect(validateUser, processUser);
75
+ const result = runPipeResult(pipeline(userData));
76
+ ```
77
+
78
+ ### ❌ WRONG: Using pipe with SideEffect-returning functions
79
+
80
+ ```ts
81
+ // ❌ BAD
82
+ const broken = pipe(
83
+ findUser, // Returns User | SideEffect
84
+ (user) => user.email // Error! SideEffect has no 'email'
85
+ );
86
+
87
+ // ✅ CORRECT - Use pipeSideEffect for SideEffect handling
88
+ const works = pipeSideEffect(
89
+ findUser,
90
+ (user) => user.email // Automatically skipped if SideEffect
91
+ );
92
+ ```
93
+
94
+ ### ❌ WRONG: Mutating arrays/objects
95
+
96
+ ```ts
97
+ // ❌ BAD
98
+ users.push(newUser);
99
+ user.name = 'Updated';
100
+
101
+ // ✅ CORRECT - Use immutable operations
102
+ const updatedUsers = append(newUser, users);
103
+ const updatedUser = assoc('name', 'Updated', user);
104
+ ```
105
+
106
+ ### ❌ WRONG: Imperative loops instead of declarative transforms
107
+
108
+ ```ts
109
+ // ❌ BAD
110
+ const results = [];
111
+ for (const user of users) {
112
+ if (user.active) results.push(user.name.toUpperCase());
113
+ }
114
+
115
+ // ✅ CORRECT
116
+ const results = pipe(
117
+ filter((user: User) => user.active),
118
+ map(user => user.name.toUpperCase())
119
+ )(users);
120
+ ```
121
+
122
+ **Remember:** If you encounter any of these patterns, STOP and use the correct fp-pack approach instead!
123
+
124
+ ---
125
+
126
+ ## 💡 Real-World Examples (Learn by Doing)
127
+
128
+ Study these common patterns first:
129
+
130
+ ### Example 1: User Data Processing Pipeline
131
+
132
+ ```ts
133
+ import { pipe, filter, map, take, sortBy } from 'fp-pack';
134
+
135
+ const getTopActiveUsers = pipe(
136
+ filter((user: User) => user.active && user.lastLogin > cutoffDate),
137
+ sortBy((user) => -user.activityScore),
138
+ map(user => ({ id: user.id, name: user.name, score: user.activityScore })),
139
+ take(10)
140
+ );
141
+
142
+ const topUsers = getTopActiveUsers(allUsers);
143
+ ```
144
+
145
+ ### Example 2: API Request with Error Handling
146
+
147
+ ```ts
148
+ import { pipeAsyncSideEffect, SideEffect, runPipeResult } from 'fp-pack';
149
+
150
+ const fetchUserData = pipeAsyncSideEffect(
151
+ async (userId: string) => {
152
+ const res = await fetch(`/api/users/${userId}`);
153
+ return res.ok ? res : SideEffect.of(() => `HTTP ${res.status}`);
154
+ },
155
+ async (res) => res.json(),
156
+ (data) => data.verified ? data : SideEffect.of(() => 'User not verified')
157
+ );
158
+
159
+ // Call at boundary
160
+ const result = runPipeResult(await fetchUserData('user-123'));
161
+ ```
162
+
163
+ ### Example 3: Data Transformation with from()
164
+
165
+ ```ts
166
+ import { pipe, from, ifElse, filter, map } from 'fp-pack';
167
+
168
+ // Constant value injection
169
+ const getStatusMessage = ifElse(
170
+ (count: number) => count > 0,
171
+ from('Items available'),
172
+ from('No items found')
173
+ );
174
+
175
+ // Data-first pattern
176
+ const processWithData = pipe(
177
+ from([1, 2, 3, 4, 5]),
178
+ filter((n: number) => n % 2 === 0),
179
+ map(n => n * 2)
180
+ );
181
+
182
+ const result = processWithData(); // [4, 8]
183
+ ```
36
184
 
37
- fp-pack is a TypeScript FP utility library focused on:
38
- 1) Function composition via `pipe` / `pipeAsync`
39
- 2) Declarative, immutable transforms (avoid loops and mutations)
40
- 3) Practical TypeScript inference (data-last + currying for most helpers)
41
- 4) Optional early-exit handling via `SideEffect` (only when needed)
42
- 5) Lazy iterable processing via `fp-pack/stream` for performance-sensitive flows
185
+ **These patterns cover 90% of real-world use cases!**
43
186
 
44
187
  ---
45
188
 
@@ -47,8 +190,6 @@ fp-pack is a TypeScript FP utility library focused on:
47
190
 
48
191
  ### `pipe` (sync)
49
192
 
50
- Use `pipe` to build a unary function:
51
-
52
193
  ```ts
53
194
  import { pipe, filter, map, take } from 'fp-pack';
54
195
 
@@ -61,8 +202,6 @@ const processUsers = pipe(
61
202
 
62
203
  ### `pipeAsync` (async)
63
204
 
64
- Use `pipeAsync` when any step is async:
65
-
66
205
  ```ts
67
206
  import { pipeAsync } from 'fp-pack';
68
207
 
@@ -75,120 +214,82 @@ const fetchUser = pipeAsync(
75
214
 
76
215
  ---
77
216
 
78
- ## Currying & Data-Last Conventions
79
-
80
- Most multi-arg helpers are **data-last** and **curried**, so they work naturally in `pipe`:
217
+ ## Currying & Data-Last
81
218
 
219
+ Most multi-arg helpers are **data-last** and **curried**:
82
220
  - Good: `map(fn)`, `filter(pred)`, `replace(from, to)`, `assoc('k', v)`, `path(['a','b'])`
83
- - Avoid in pipelines: helpers that require you to pass the data first, or that return a function whose type depends on the final data arg (see next section)
84
-
85
- Single-arg helpers are already unary; “currying them” adds no value—just use them directly.
221
+ - Single-arg helpers are already unary—just use them directly
86
222
 
87
223
  ---
88
224
 
89
- ## TypeScript: Data-last Generic Inference Caveats (Important)
225
+ ## TypeScript: Data-last Generic Inference
90
226
 
91
- Some data-last helpers return a **generic function** whose type is only determined by the final data argument.
92
- Inside `pipe`/`pipeAsync`, TypeScript sometimes can’t infer that type, so you may need a tiny hint.
227
+ Some data-last helpers return a **generic function** whose type is only determined by the final data argument. Inside `pipe`, TypeScript sometimes can't infer that type.
93
228
 
94
- ### Recommended fixes (choose 1)
229
+ ### Quick fixes (choose 1)
95
230
 
96
231
  ```ts
97
232
  import { pipe, pipeHint, zip, some } from 'fp-pack';
98
233
 
99
- // 1) data-first wrapper (most explicit)
234
+ // 1) data-first wrapper
100
235
  const withWrapper = pipe(
101
236
  (values: number[]) => zip([1, 2, 3], values),
102
237
  some(([a, b]) => a > b)
103
238
  );
104
239
 
105
- // 2) explicit function type hint (short, but uses an assertion)
106
- const withHint = pipe(
107
- zip([1, 2, 3]) as (values: number[]) => Array<[number, number]>,
108
- some(([a, b]) => a > b)
109
- );
110
-
111
- // 3) pipeHint helper (keeps pipeline style)
240
+ // 2) pipeHint helper
112
241
  const withPipeHint = pipe(
113
242
  pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
114
243
  some(([a, b]) => a > b)
115
244
  );
116
245
  ```
117
246
 
118
- ### Utilities that may need a hint in data-last pipelines
119
-
120
- - Array: `chunk`, `drop`, `take`, `zip`
121
- - Object: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
122
- - Async: `timeout`
123
- - Stream: `chunk`, `drop`, `take`, `zip`
124
-
125
- When in doubt: check the `.d.ts` signature (see “Quick Signature Lookup”).
247
+ **Utilities that may need a hint:** `chunk`, `drop`, `take`, `zip`, `assoc`, `path`, `prop`, `timeout`
126
248
 
127
249
  ---
128
250
 
129
251
  ## SideEffect Pattern (Use Only When Needed)
130
252
 
131
- Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you truly need **early termination**:
253
+ Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you need **early termination**:
132
254
  - validation pipelines that should stop early
133
255
  - recoverable errors you want to model as data
134
256
  - branching flows where you want to short-circuit
135
257
 
136
258
  ### SideEffect-aware pipes
137
259
  - `pipeSideEffect` / `pipeAsyncSideEffect`: convenient, but may widen effects to `any`
138
- - `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended when you care about types)
139
-
140
- ### Rules
141
- - Do **not** call `runPipeResult` or `matchSideEffect` **inside** the pipeline.
142
- - Prefer `isSideEffect` for precise runtime narrowing in both branches.
143
- - Use `runPipeResult` only at the boundary, and provide generics if inference is widened.
260
+ - `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended)
144
261
 
145
262
  ### Key functions
146
263
  - `SideEffect.of(effectFn, label?)`
147
264
  - `isSideEffect(value)` (type guard)
148
265
  - `runPipeResult(result)` (execute effect or return value; **outside** pipelines)
149
- - `matchSideEffect(result, { value, effect })`
150
-
151
- ### `runPipeResult` type behavior (important)
152
266
 
153
- - If the input is **narrowed** to `SideEffect<R>` (e.g. inside `if (isSideEffect(x))`), `runPipeResult(x)` returns `R`.
154
- - If the input is **widened** to `SideEffect<any>` or `any` (common with non-strict pipelines), `runPipeResult(x)` becomes `any` unless you provide generics.
267
+ ### Example
155
268
 
156
269
  ```ts
157
- import {
158
- pipeSideEffect,
159
- pipeSideEffectStrict,
160
- SideEffect,
161
- isSideEffect,
162
- runPipeResult,
163
- } from 'fp-pack';
164
-
165
- const nonStrict = pipeSideEffect(
166
- (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
167
- );
168
- const widened: number | SideEffect<any> = nonStrict(-1);
169
- const unsafe = runPipeResult(widened); // any
270
+ import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
170
271
 
171
- const strict = pipeSideEffectStrict(
172
- (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
173
- );
174
- const precise = strict(-1); // number | SideEffect<'NEG'>
272
+ const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
273
+ const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
274
+
275
+ const result = pipeline(-1); // number | SideEffect<'NEG'>
175
276
 
176
- if (isSideEffect(precise)) {
177
- const err = runPipeResult(precise); // 'NEG'
277
+ if (isSideEffect(result)) {
278
+ const err = runPipeResult(result); // 'NEG'
279
+ } else {
280
+ // result is number
178
281
  }
179
282
  ```
180
283
 
181
284
  ---
182
285
 
183
- ## Stream Functions (`fp-pack/stream`) — Lazy Iterables
286
+ ## Stream Functions (`fp-pack/stream`)
184
287
 
185
288
  Use stream utilities when:
186
- - data is large or unbounded (`range`, streams, generators)
187
- - you want lazy evaluation (avoid allocating intermediate arrays)
289
+ - data is large or unbounded
290
+ - you want lazy evaluation
188
291
  - you want to support `Iterable` and `AsyncIterable`
189
292
 
190
- Stream utilities are in `fp-pack/stream` and are designed to be pipe-friendly.
191
-
192
293
  ```ts
193
294
  import { pipe } from 'fp-pack';
194
295
  import { range, filter, map, take, toArray } from 'fp-pack/stream';
@@ -205,115 +306,78 @@ const result = first100SquaresOfEvens(range(Infinity));
205
306
 
206
307
  ---
207
308
 
208
- ## Available Functions (Practical Index)
209
-
210
- This is not a complete API reference; it’s the “what to reach for” index when composing pipelines.
309
+ ## Available Functions (Quick Index)
211
310
 
212
311
  ### Composition
213
312
  - `pipe`, `pipeAsync`
214
313
  - SideEffect-aware: `pipeSideEffect`, `pipeSideEffectStrict`, `pipeAsyncSideEffect`, `pipeAsyncSideEffectStrict`
215
- - Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `flip`, `partial`, `curry`, `compose`, `complement`
314
+ - Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `curry`, `compose`
216
315
  - SideEffect helpers: `SideEffect`, `isSideEffect`, `matchSideEffect`, `runPipeResult`
217
316
 
218
317
  ### Array
219
- - Core transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
318
+ - Transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
220
319
  - Queries: `find`, `some`, `every`
221
- - Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
222
- - Ordering/grouping: `sort`, `sortBy`, `groupBy`, `uniqBy`
223
- - Pairing: `zip`, `zipWith`
224
- - Combining: `concat`, `append`, `prepend`, `flatten`
320
+ - Slicing: `take`, `drop`, `chunk`
321
+ - Ordering: `sort`, `sortBy`, `groupBy`, `uniqBy`
322
+ - Combining: `zip`, `concat`, `append`, `flatten`
225
323
 
226
324
  ### Object
227
- - Access: `prop`, `propOr`, `propStrict`, `path`, `pathOr`
325
+ - Access: `prop`, `path`, `propOr`, `pathOr`
228
326
  - Pick/drop: `pick`, `omit`
229
327
  - Updates: `assoc`, `assocPath`, `dissocPath`
230
328
  - Merge: `merge`, `mergeDeep`
231
329
  - Transforms: `mapValues`, `evolve`
232
- - Predicates: `has`
233
330
 
234
331
  ### Control Flow
235
332
  - `ifElse`, `when`, `unless`, `cond`, `guard`, `tryCatch`
236
333
 
237
334
  ### Async
238
335
  - `retry`, `timeout`, `delay`
239
- - Function-returning helpers: `debounce*`, `throttle`
336
+ - `debounce*`, `throttle`
240
337
 
241
338
  ### Stream (Lazy Iterables)
242
339
  - Building: `range`
243
340
  - Transforms: `map`, `filter`, `flatMap`, `flatten`
244
- - Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
245
- - Queries/reductions: `find`, `some`, `every`, `reduce`, `scan`
246
- - Pairing/combining: `zip`, `zipWith`, `concat`, `append`, `prepend`
341
+ - Slicing: `take`, `drop`, `chunk`
342
+ - Queries: `find`, `some`, `every`, `reduce`
343
+ - Combining: `zip`, `concat`
247
344
  - Utilities: `toArray`, `toAsync`
248
345
 
249
- ### Math / String / Equality / Debug
250
- - Math: `add`, `sub`, `mul`, `div`, `randomInt`, `clamp`
251
- - String: `split`, `join`, `replace`, `match`, `trim`
346
+ ### Others
347
+ - Math: `add`, `sub`, `mul`, `div`, `clamp`
348
+ - String: `split`, `join`, `replace`, `trim`
252
349
  - Equality: `equals`, `isNil`
253
350
  - Debug: `assert`, `invariant`
254
351
 
255
352
  ---
256
353
 
257
- ## Micro-Patterns (Keep It Short)
258
-
259
- ### Boundary handling (UI/event/service)
354
+ ## Micro-Patterns
260
355
 
261
- Do the work in a pipeline, unwrap at the boundary:
356
+ ### Boundary handling
262
357
 
263
358
  ```ts
264
- import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
359
+ const pipeline = pipeSideEffectStrict(validate, process);
265
360
 
266
- const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
267
- const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
268
-
269
- export const handler = (n: number) => {
270
- const result = pipeline(n);
271
- if (isSideEffect(result)) return runPipeResult(result); // 'NEG'
272
- return result; // number
361
+ export const handler = (data) => {
362
+ const result = pipeline(data);
363
+ if (isSideEffect(result)) return runPipeResult(result);
364
+ return result;
273
365
  };
274
366
  ```
275
367
 
276
- ### Data-first style with `from()`
277
-
278
- Some agents/users prefer “data-first” pipelines. You can keep `pipe` data-last and still write data-first flows by injecting the initial value with `from()`:
368
+ ### Data-first with from()
279
369
 
280
370
  ```ts
281
- import { pipe, from, map, filter, take } from 'fp-pack';
282
-
283
- // data-first feeling: start with data, then compose transforms
284
371
  const result = pipe(
285
372
  from([1, 2, 3, 4, 5]),
286
- filter((n: number) => n % 2 === 0),
287
- map((n) => n * 10),
288
- take(2)
289
- )(); // [20, 40]
290
-
291
- // same idea when you already have an input (normal usage)
292
- const process = pipe(
293
373
  filter((n: number) => n % 2 === 0),
294
374
  map((n) => n * 10)
295
- );
296
- const result2 = process([1, 2, 3, 4, 5]); // [20, 40]
297
- ```
298
-
299
- ### `ifElse` uses functions (not values)
300
-
301
- ```ts
302
- import { ifElse, from } from 'fp-pack';
303
-
304
- const label = ifElse(
305
- (n: number) => n >= 60,
306
- from('pass'),
307
- from('fail')
308
- );
375
+ )(); // [20, 40]
309
376
  ```
310
377
 
311
- ### Stream pipeline to array
378
+ ### Stream to array
312
379
 
313
380
  ```ts
314
- import { pipe } from 'fp-pack';
315
- import { map, filter, toArray } from 'fp-pack/stream';
316
-
317
381
  const toIds = pipe(
318
382
  filter((u: User) => u.active),
319
383
  map((u) => u.id),
@@ -321,46 +385,9 @@ const toIds = pipe(
321
385
  );
322
386
  ```
323
387
 
324
- ### Data-last inference workaround (when needed)
325
-
326
- Use a wrapper or `pipeHint` when a data-last generic won’t infer inside `pipe`:
327
-
328
- ```ts
329
- import { pipe, pipeHint, zip } from 'fp-pack';
330
-
331
- // wrapper
332
- const zipUsers = pipe((xs: User[]) => zip(['a', 'b', 'c'], xs));
333
-
334
- // pipeHint
335
- const zipUsers2 = pipe(
336
- pipeHint<User[], Array<[string, User]>>(zip(['a', 'b', 'c']))
337
- );
338
- ```
339
-
340
- ### Async pipelines (retry/timeout)
341
-
342
- Keep async steps inside `pipeAsync` and push configuration (like `timeout`) via currying:
388
+ ### Object updates
343
389
 
344
390
  ```ts
345
- import { pipeAsync, retry, timeout } from 'fp-pack';
346
-
347
- const fetchJson = pipeAsync(
348
- (url: string) => fetch(url),
349
- (res) => res.json()
350
- );
351
-
352
- const fetchJsonWithGuards = pipeAsync(
353
- fetchJson,
354
- timeout(5_000),
355
- retry(3)
356
- );
357
- ```
358
-
359
- ### Object updates (avoid mutation)
360
-
361
- ```ts
362
- import { pipe, assocPath, merge } from 'fp-pack';
363
-
364
391
  const updateAccount = pipe(
365
392
  assocPath(['profile', 'role'], 'member'),
366
393
  merge({ updatedAt: Date.now() })
@@ -369,71 +396,40 @@ const updateAccount = pipe(
369
396
 
370
397
  ---
371
398
 
372
- ## Decision Guide (What Should the Agent Pick?)
399
+ ## Decision Guide
373
400
 
374
401
  - Is everything sync and pure? → `pipe`
375
402
  - Any step async? → `pipeAsync`
376
403
  - Need early-exit + typed effect unions? → `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
377
- - Need early-exit but type precision doesnt matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
378
- - Handling result at boundary?
379
- - Need exact branch types? → `isSideEffect` + separate branches
380
- - Just want to “unwrap” and don’t care about precision? → `runPipeResult` (provide generics if widened)
404
+ - Need early-exit but type precision doesn't matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
405
+ - Handling result at boundary? → `isSideEffect` for branching, `runPipeResult` to unwrap
381
406
  - Large/unbounded/iterable data? → `fp-pack/stream`
382
407
 
383
408
  ---
384
409
 
385
- ## Quick Reference
386
-
387
- ### Import Paths
410
+ ## Import Paths
388
411
 
389
412
  - Main: `import { pipe, map, filter } from 'fp-pack'`
390
- - SideEffect pipes: `import { pipeSideEffect, pipeSideEffectStrict, SideEffect } from 'fp-pack'`
413
+ - SideEffect: `import { pipeSideEffect, SideEffect } from 'fp-pack'`
391
414
  - Async: `import { pipeAsync, retry, timeout } from 'fp-pack'`
392
415
  - Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
393
416
 
394
- ### When to Use What
395
-
396
- - Pure sync transforms: `pipe`
397
- - Pure async transforms: `pipeAsync`
398
- - Early-exit pipelines: `pipeSideEffect*` / `pipeAsyncSideEffect*`
399
- - Strict effect unions: prefer `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
400
- - Runtime branching: prefer `isSideEffect`
401
- - Boundary unwrapping: `runPipeResult` (provide generics if inference is widened)
402
- - Large/unbounded data: `fp-pack/stream`
403
-
404
- ---
405
-
406
- ## Anti-Patterns (Avoid)
407
-
408
- - Imperative loops when a pipeline is clearer
409
- - Mutating inputs (prefer immutable transforms)
410
- - Chaining `Array.prototype.*` in complex transforms (prefer `pipe`)
411
- - Calling `runPipeResult` inside a SideEffect-aware pipeline
412
- - Adding “classic monads” to emulate Option/Either instead of using `SideEffect` pipes when necessary
413
-
414
417
  ---
415
418
 
416
419
  ## Quick Signature Lookup (When Unsure)
417
420
 
418
- Prefer local types first:
421
+ If TypeScript inference is stuck or you need to verify a function signature:
422
+
423
+ **In fp-pack project:**
419
424
  - Main types: `dist/index.d.ts`
420
425
  - Stream types: `dist/stream/index.d.ts`
421
426
 
422
- If you’re in a consumer project:
423
- - `node_modules/fp-pack/dist/index.d.ts`
424
- - `node_modules/fp-pack/dist/stream/index.d.ts`
425
-
426
- ---
427
-
428
- ## Writing New Helpers (If Needed)
429
-
430
- If you add your own utilities that should compose well:
431
- - Keep them **unary** for pipelines (or return a unary function via currying).
432
- - Prefer **data-last** argument order.
433
- - For generic/overloaded helpers, consider providing an explicit type alias to preserve inference in TypeScript.
427
+ **In consumer project:**
428
+ - Main types: `node_modules/fp-pack/dist/index.d.ts`
429
+ - Stream types: `node_modules/fp-pack/dist/stream/index.d.ts`
434
430
 
435
431
  ---
436
432
 
437
433
  ## Summary
438
434
 
439
- Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary (with generics if inference widens to `any`).
435
+ Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary.
@@ -1,6 +1,6 @@
1
1
  # fp-pack AI Agent Skills
2
2
 
3
- Document Version: 0.9.2
3
+ Document Version: 0.9.3
4
4
 
5
5
  ## ⚠️ Activation Condition (Read First)
6
6
 
@@ -11,7 +11,7 @@ Before following this document:
11
11
  - Check `node_modules/fp-pack` exists
12
12
  - Check existing code imports from `fp-pack` or `fp-pack/stream`
13
13
 
14
- If `fp-pack` is **not** installed, use the projects existing conventions. Do **not** suggest adding fp-pack unless the user asks.
14
+ If `fp-pack` is **not** installed, use the project's existing conventions. Do **not** suggest adding fp-pack unless the user asks.
15
15
 
16
16
  ---
17
17
 
@@ -23,16 +23,159 @@ When writing or editing code in an fp-pack project:
23
23
  - Use `stream/*` for lazy/large/iterable processing; use array/object utils for small/eager data.
24
24
  - Use SideEffect-aware pipes only when you need **early exit**: `pipeSideEffect*` / `pipeAsyncSideEffect*`.
25
25
  - **Never call `runPipeResult` inside a pipeline**; call it at the boundary (event handler, service wrapper, etc.).
26
- - If TypeScript inference gets stuck with a data-last generic, use a small wrapper, an explicit type hint, or `pipeHint`.
26
+ - If TypeScript inference gets stuck with a data-last generic, use a small wrapper or `pipeHint`.
27
27
 
28
- ## Philosophy (What fp-pack Optimizes For)
28
+ ---
29
+
30
+ ## ⛔ CRITICAL MISTAKES TO AVOID (Read This First!)
31
+
32
+ Before you write any code, know these anti-patterns. These are the most common mistakes that break fp-pack pipelines:
33
+
34
+ ### ❌ WRONG: Using `() => value` for constants in pipelines
35
+
36
+ ```ts
37
+ // ❌ BAD - Type inference fails
38
+ const broken = pipe(
39
+ () => [1, 2, 3, 4, 5],
40
+ filter((n: number) => n % 2 === 0) // Error!
41
+ );
42
+
43
+ // ✅ CORRECT - Use from() for constant values
44
+ const works = pipe(
45
+ from([1, 2, 3, 4, 5]),
46
+ filter((n: number) => n % 2 === 0)
47
+ );
48
+ ```
49
+
50
+ ### ❌ WRONG: Using raw values with ifElse/cond
51
+
52
+ ```ts
53
+ // ❌ BAD
54
+ const broken = ifElse((score: number) => score >= 60, 'pass', 'fail');
55
+
56
+ // ✅ CORRECT - Use from() to wrap constant values
57
+ const works = ifElse((score: number) => score >= 60, from('pass'), from('fail'));
58
+ ```
59
+
60
+ ### ❌ WRONG: Calling runPipeResult inside a pipeline
61
+
62
+ ```ts
63
+ // ❌ BAD
64
+ const broken = pipe(validateUser, runPipeResult, processUser);
65
+
66
+ // ✅ CORRECT - Call runPipeResult OUTSIDE the pipeline
67
+ const pipeline = pipeSideEffect(validateUser, processUser);
68
+ const result = runPipeResult(pipeline(userData));
69
+ ```
70
+
71
+ ### ❌ WRONG: Using pipe with SideEffect-returning functions
72
+
73
+ ```ts
74
+ // ❌ BAD
75
+ const broken = pipe(
76
+ findUser, // Returns User | SideEffect
77
+ (user) => user.email // Error! SideEffect has no 'email'
78
+ );
79
+
80
+ // ✅ CORRECT - Use pipeSideEffect for SideEffect handling
81
+ const works = pipeSideEffect(
82
+ findUser,
83
+ (user) => user.email // Automatically skipped if SideEffect
84
+ );
85
+ ```
86
+
87
+ ### ❌ WRONG: Mutating arrays/objects
88
+
89
+ ```ts
90
+ // ❌ BAD
91
+ users.push(newUser);
92
+ user.name = 'Updated';
93
+
94
+ // ✅ CORRECT - Use immutable operations
95
+ const updatedUsers = append(newUser, users);
96
+ const updatedUser = assoc('name', 'Updated', user);
97
+ ```
98
+
99
+ ### ❌ WRONG: Imperative loops instead of declarative transforms
100
+
101
+ ```ts
102
+ // ❌ BAD
103
+ const results = [];
104
+ for (const user of users) {
105
+ if (user.active) results.push(user.name.toUpperCase());
106
+ }
107
+
108
+ // ✅ CORRECT
109
+ const results = pipe(
110
+ filter((user: User) => user.active),
111
+ map(user => user.name.toUpperCase())
112
+ )(users);
113
+ ```
114
+
115
+ **Remember:** If you encounter any of these patterns, STOP and use the correct fp-pack approach instead!
116
+
117
+ ---
118
+
119
+ ## 💡 Real-World Examples (Learn by Doing)
120
+
121
+ Study these common patterns first:
122
+
123
+ ### Example 1: User Data Processing Pipeline
124
+
125
+ ```ts
126
+ import { pipe, filter, map, take, sortBy } from 'fp-pack';
127
+
128
+ const getTopActiveUsers = pipe(
129
+ filter((user: User) => user.active && user.lastLogin > cutoffDate),
130
+ sortBy((user) => -user.activityScore),
131
+ map(user => ({ id: user.id, name: user.name, score: user.activityScore })),
132
+ take(10)
133
+ );
134
+
135
+ const topUsers = getTopActiveUsers(allUsers);
136
+ ```
137
+
138
+ ### Example 2: API Request with Error Handling
139
+
140
+ ```ts
141
+ import { pipeAsyncSideEffect, SideEffect, runPipeResult } from 'fp-pack';
142
+
143
+ const fetchUserData = pipeAsyncSideEffect(
144
+ async (userId: string) => {
145
+ const res = await fetch(`/api/users/${userId}`);
146
+ return res.ok ? res : SideEffect.of(() => `HTTP ${res.status}`);
147
+ },
148
+ async (res) => res.json(),
149
+ (data) => data.verified ? data : SideEffect.of(() => 'User not verified')
150
+ );
151
+
152
+ // Call at boundary
153
+ const result = runPipeResult(await fetchUserData('user-123'));
154
+ ```
155
+
156
+ ### Example 3: Data Transformation with from()
157
+
158
+ ```ts
159
+ import { pipe, from, ifElse, filter, map } from 'fp-pack';
160
+
161
+ // Constant value injection
162
+ const getStatusMessage = ifElse(
163
+ (count: number) => count > 0,
164
+ from('Items available'),
165
+ from('No items found')
166
+ );
167
+
168
+ // Data-first pattern
169
+ const processWithData = pipe(
170
+ from([1, 2, 3, 4, 5]),
171
+ filter((n: number) => n % 2 === 0),
172
+ map(n => n * 2)
173
+ );
174
+
175
+ const result = processWithData(); // [4, 8]
176
+ ```
29
177
 
30
- fp-pack is a TypeScript FP utility library focused on:
31
- 1) Function composition via `pipe` / `pipeAsync`
32
- 2) Declarative, immutable transforms (avoid loops and mutations)
33
- 3) Practical TypeScript inference (data-last + currying for most helpers)
34
- 4) Optional early-exit handling via `SideEffect` (only when needed)
35
- 5) Lazy iterable processing via `fp-pack/stream` for performance-sensitive flows
178
+ **These patterns cover 90% of real-world use cases!**
36
179
 
37
180
  ---
38
181
 
@@ -40,8 +183,6 @@ fp-pack is a TypeScript FP utility library focused on:
40
183
 
41
184
  ### `pipe` (sync)
42
185
 
43
- Use `pipe` to build a unary function:
44
-
45
186
  ```ts
46
187
  import { pipe, filter, map, take } from 'fp-pack';
47
188
 
@@ -54,8 +195,6 @@ const processUsers = pipe(
54
195
 
55
196
  ### `pipeAsync` (async)
56
197
 
57
- Use `pipeAsync` when any step is async:
58
-
59
198
  ```ts
60
199
  import { pipeAsync } from 'fp-pack';
61
200
 
@@ -68,120 +207,82 @@ const fetchUser = pipeAsync(
68
207
 
69
208
  ---
70
209
 
71
- ## Currying & Data-Last Conventions
72
-
73
- Most multi-arg helpers are **data-last** and **curried**, so they work naturally in `pipe`:
210
+ ## Currying & Data-Last
74
211
 
212
+ Most multi-arg helpers are **data-last** and **curried**:
75
213
  - Good: `map(fn)`, `filter(pred)`, `replace(from, to)`, `assoc('k', v)`, `path(['a','b'])`
76
- - Avoid in pipelines: helpers that require you to pass the data first, or that return a function whose type depends on the final data arg (see next section)
77
-
78
- Single-arg helpers are already unary; “currying them” adds no value—just use them directly.
214
+ - Single-arg helpers are already unary—just use them directly
79
215
 
80
216
  ---
81
217
 
82
- ## TypeScript: Data-last Generic Inference Caveats (Important)
218
+ ## TypeScript: Data-last Generic Inference
83
219
 
84
- Some data-last helpers return a **generic function** whose type is only determined by the final data argument.
85
- Inside `pipe`/`pipeAsync`, TypeScript sometimes can’t infer that type, so you may need a tiny hint.
220
+ Some data-last helpers return a **generic function** whose type is only determined by the final data argument. Inside `pipe`, TypeScript sometimes can't infer that type.
86
221
 
87
- ### Recommended fixes (choose 1)
222
+ ### Quick fixes (choose 1)
88
223
 
89
224
  ```ts
90
225
  import { pipe, pipeHint, zip, some } from 'fp-pack';
91
226
 
92
- // 1) data-first wrapper (most explicit)
227
+ // 1) data-first wrapper
93
228
  const withWrapper = pipe(
94
229
  (values: number[]) => zip([1, 2, 3], values),
95
230
  some(([a, b]) => a > b)
96
231
  );
97
232
 
98
- // 2) explicit function type hint (short, but uses an assertion)
99
- const withHint = pipe(
100
- zip([1, 2, 3]) as (values: number[]) => Array<[number, number]>,
101
- some(([a, b]) => a > b)
102
- );
103
-
104
- // 3) pipeHint helper (keeps pipeline style)
233
+ // 2) pipeHint helper
105
234
  const withPipeHint = pipe(
106
235
  pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
107
236
  some(([a, b]) => a > b)
108
237
  );
109
238
  ```
110
239
 
111
- ### Utilities that may need a hint in data-last pipelines
112
-
113
- - Array: `chunk`, `drop`, `take`, `zip`
114
- - Object: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
115
- - Async: `timeout`
116
- - Stream: `chunk`, `drop`, `take`, `zip`
117
-
118
- When in doubt: check the `.d.ts` signature (see “Quick Signature Lookup”).
240
+ **Utilities that may need a hint:** `chunk`, `drop`, `take`, `zip`, `assoc`, `path`, `prop`, `timeout`
119
241
 
120
242
  ---
121
243
 
122
244
  ## SideEffect Pattern (Use Only When Needed)
123
245
 
124
- Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you truly need **early termination**:
246
+ Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you need **early termination**:
125
247
  - validation pipelines that should stop early
126
248
  - recoverable errors you want to model as data
127
249
  - branching flows where you want to short-circuit
128
250
 
129
251
  ### SideEffect-aware pipes
130
252
  - `pipeSideEffect` / `pipeAsyncSideEffect`: convenient, but may widen effects to `any`
131
- - `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended when you care about types)
132
-
133
- ### Rules
134
- - Do **not** call `runPipeResult` or `matchSideEffect` **inside** the pipeline.
135
- - Prefer `isSideEffect` for precise runtime narrowing in both branches.
136
- - Use `runPipeResult` only at the boundary, and provide generics if inference is widened.
253
+ - `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended)
137
254
 
138
255
  ### Key functions
139
256
  - `SideEffect.of(effectFn, label?)`
140
257
  - `isSideEffect(value)` (type guard)
141
258
  - `runPipeResult(result)` (execute effect or return value; **outside** pipelines)
142
- - `matchSideEffect(result, { value, effect })`
143
-
144
- ### `runPipeResult` type behavior (important)
145
259
 
146
- - If the input is **narrowed** to `SideEffect<R>` (e.g. inside `if (isSideEffect(x))`), `runPipeResult(x)` returns `R`.
147
- - If the input is **widened** to `SideEffect<any>` or `any` (common with non-strict pipelines), `runPipeResult(x)` becomes `any` unless you provide generics.
260
+ ### Example
148
261
 
149
262
  ```ts
150
- import {
151
- pipeSideEffect,
152
- pipeSideEffectStrict,
153
- SideEffect,
154
- isSideEffect,
155
- runPipeResult,
156
- } from 'fp-pack';
157
-
158
- const nonStrict = pipeSideEffect(
159
- (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
160
- );
161
- const widened: number | SideEffect<any> = nonStrict(-1);
162
- const unsafe = runPipeResult(widened); // any
263
+ import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
163
264
 
164
- const strict = pipeSideEffectStrict(
165
- (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
166
- );
167
- const precise = strict(-1); // number | SideEffect<'NEG'>
265
+ const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
266
+ const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
267
+
268
+ const result = pipeline(-1); // number | SideEffect<'NEG'>
168
269
 
169
- if (isSideEffect(precise)) {
170
- const err = runPipeResult(precise); // 'NEG'
270
+ if (isSideEffect(result)) {
271
+ const err = runPipeResult(result); // 'NEG'
272
+ } else {
273
+ // result is number
171
274
  }
172
275
  ```
173
276
 
174
277
  ---
175
278
 
176
- ## Stream Functions (`fp-pack/stream`) — Lazy Iterables
279
+ ## Stream Functions (`fp-pack/stream`)
177
280
 
178
281
  Use stream utilities when:
179
- - data is large or unbounded (`range`, streams, generators)
180
- - you want lazy evaluation (avoid allocating intermediate arrays)
282
+ - data is large or unbounded
283
+ - you want lazy evaluation
181
284
  - you want to support `Iterable` and `AsyncIterable`
182
285
 
183
- Stream utilities are in `fp-pack/stream` and are designed to be pipe-friendly.
184
-
185
286
  ```ts
186
287
  import { pipe } from 'fp-pack';
187
288
  import { range, filter, map, take, toArray } from 'fp-pack/stream';
@@ -198,115 +299,78 @@ const result = first100SquaresOfEvens(range(Infinity));
198
299
 
199
300
  ---
200
301
 
201
- ## Available Functions (Practical Index)
202
-
203
- This is not a complete API reference; it’s the “what to reach for” index when composing pipelines.
302
+ ## Available Functions (Quick Index)
204
303
 
205
304
  ### Composition
206
305
  - `pipe`, `pipeAsync`
207
306
  - SideEffect-aware: `pipeSideEffect`, `pipeSideEffectStrict`, `pipeAsyncSideEffect`, `pipeAsyncSideEffectStrict`
208
- - Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `flip`, `partial`, `curry`, `compose`, `complement`
307
+ - Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `curry`, `compose`
209
308
  - SideEffect helpers: `SideEffect`, `isSideEffect`, `matchSideEffect`, `runPipeResult`
210
309
 
211
310
  ### Array
212
- - Core transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
311
+ - Transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
213
312
  - Queries: `find`, `some`, `every`
214
- - Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
215
- - Ordering/grouping: `sort`, `sortBy`, `groupBy`, `uniqBy`
216
- - Pairing: `zip`, `zipWith`
217
- - Combining: `concat`, `append`, `prepend`, `flatten`
313
+ - Slicing: `take`, `drop`, `chunk`
314
+ - Ordering: `sort`, `sortBy`, `groupBy`, `uniqBy`
315
+ - Combining: `zip`, `concat`, `append`, `flatten`
218
316
 
219
317
  ### Object
220
- - Access: `prop`, `propOr`, `propStrict`, `path`, `pathOr`
318
+ - Access: `prop`, `path`, `propOr`, `pathOr`
221
319
  - Pick/drop: `pick`, `omit`
222
320
  - Updates: `assoc`, `assocPath`, `dissocPath`
223
321
  - Merge: `merge`, `mergeDeep`
224
322
  - Transforms: `mapValues`, `evolve`
225
- - Predicates: `has`
226
323
 
227
324
  ### Control Flow
228
325
  - `ifElse`, `when`, `unless`, `cond`, `guard`, `tryCatch`
229
326
 
230
327
  ### Async
231
328
  - `retry`, `timeout`, `delay`
232
- - Function-returning helpers: `debounce*`, `throttle`
329
+ - `debounce*`, `throttle`
233
330
 
234
331
  ### Stream (Lazy Iterables)
235
332
  - Building: `range`
236
333
  - Transforms: `map`, `filter`, `flatMap`, `flatten`
237
- - Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
238
- - Queries/reductions: `find`, `some`, `every`, `reduce`, `scan`
239
- - Pairing/combining: `zip`, `zipWith`, `concat`, `append`, `prepend`
334
+ - Slicing: `take`, `drop`, `chunk`
335
+ - Queries: `find`, `some`, `every`, `reduce`
336
+ - Combining: `zip`, `concat`
240
337
  - Utilities: `toArray`, `toAsync`
241
338
 
242
- ### Math / String / Equality / Debug
243
- - Math: `add`, `sub`, `mul`, `div`, `randomInt`, `clamp`
244
- - String: `split`, `join`, `replace`, `match`, `trim`
339
+ ### Others
340
+ - Math: `add`, `sub`, `mul`, `div`, `clamp`
341
+ - String: `split`, `join`, `replace`, `trim`
245
342
  - Equality: `equals`, `isNil`
246
343
  - Debug: `assert`, `invariant`
247
344
 
248
345
  ---
249
346
 
250
- ## Micro-Patterns (Keep It Short)
251
-
252
- ### Boundary handling (UI/event/service)
347
+ ## Micro-Patterns
253
348
 
254
- Do the work in a pipeline, unwrap at the boundary:
349
+ ### Boundary handling
255
350
 
256
351
  ```ts
257
- import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
352
+ const pipeline = pipeSideEffectStrict(validate, process);
258
353
 
259
- const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
260
- const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
261
-
262
- export const handler = (n: number) => {
263
- const result = pipeline(n);
264
- if (isSideEffect(result)) return runPipeResult(result); // 'NEG'
265
- return result; // number
354
+ export const handler = (data) => {
355
+ const result = pipeline(data);
356
+ if (isSideEffect(result)) return runPipeResult(result);
357
+ return result;
266
358
  };
267
359
  ```
268
360
 
269
- ### Data-first style with `from()`
270
-
271
- Some agents/users prefer “data-first” pipelines. You can keep `pipe` data-last and still write data-first flows by injecting the initial value with `from()`:
361
+ ### Data-first with from()
272
362
 
273
363
  ```ts
274
- import { pipe, from, map, filter, take } from 'fp-pack';
275
-
276
- // data-first feeling: start with data, then compose transforms
277
364
  const result = pipe(
278
365
  from([1, 2, 3, 4, 5]),
279
- filter((n: number) => n % 2 === 0),
280
- map((n) => n * 10),
281
- take(2)
282
- )(); // [20, 40]
283
-
284
- // same idea when you already have an input (normal usage)
285
- const process = pipe(
286
366
  filter((n: number) => n % 2 === 0),
287
367
  map((n) => n * 10)
288
- );
289
- const result2 = process([1, 2, 3, 4, 5]); // [20, 40]
290
- ```
291
-
292
- ### `ifElse` uses functions (not values)
293
-
294
- ```ts
295
- import { ifElse, from } from 'fp-pack';
296
-
297
- const label = ifElse(
298
- (n: number) => n >= 60,
299
- from('pass'),
300
- from('fail')
301
- );
368
+ )(); // [20, 40]
302
369
  ```
303
370
 
304
- ### Stream pipeline to array
371
+ ### Stream to array
305
372
 
306
373
  ```ts
307
- import { pipe } from 'fp-pack';
308
- import { map, filter, toArray } from 'fp-pack/stream';
309
-
310
374
  const toIds = pipe(
311
375
  filter((u: User) => u.active),
312
376
  map((u) => u.id),
@@ -314,46 +378,9 @@ const toIds = pipe(
314
378
  );
315
379
  ```
316
380
 
317
- ### Data-last inference workaround (when needed)
318
-
319
- Use a wrapper or `pipeHint` when a data-last generic won’t infer inside `pipe`:
320
-
321
- ```ts
322
- import { pipe, pipeHint, zip } from 'fp-pack';
323
-
324
- // wrapper
325
- const zipUsers = pipe((xs: User[]) => zip(['a', 'b', 'c'], xs));
326
-
327
- // pipeHint
328
- const zipUsers2 = pipe(
329
- pipeHint<User[], Array<[string, User]>>(zip(['a', 'b', 'c']))
330
- );
331
- ```
332
-
333
- ### Async pipelines (retry/timeout)
334
-
335
- Keep async steps inside `pipeAsync` and push configuration (like `timeout`) via currying:
381
+ ### Object updates
336
382
 
337
383
  ```ts
338
- import { pipeAsync, retry, timeout } from 'fp-pack';
339
-
340
- const fetchJson = pipeAsync(
341
- (url: string) => fetch(url),
342
- (res) => res.json()
343
- );
344
-
345
- const fetchJsonWithGuards = pipeAsync(
346
- fetchJson,
347
- timeout(5_000),
348
- retry(3)
349
- );
350
- ```
351
-
352
- ### Object updates (avoid mutation)
353
-
354
- ```ts
355
- import { pipe, assocPath, merge } from 'fp-pack';
356
-
357
384
  const updateAccount = pipe(
358
385
  assocPath(['profile', 'role'], 'member'),
359
386
  merge({ updatedAt: Date.now() })
@@ -362,71 +389,40 @@ const updateAccount = pipe(
362
389
 
363
390
  ---
364
391
 
365
- ## Decision Guide (What Should the Agent Pick?)
392
+ ## Decision Guide
366
393
 
367
394
  - Is everything sync and pure? → `pipe`
368
395
  - Any step async? → `pipeAsync`
369
396
  - Need early-exit + typed effect unions? → `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
370
- - Need early-exit but type precision doesnt matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
371
- - Handling result at boundary?
372
- - Need exact branch types? → `isSideEffect` + separate branches
373
- - Just want to “unwrap” and don’t care about precision? → `runPipeResult` (provide generics if widened)
397
+ - Need early-exit but type precision doesn't matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
398
+ - Handling result at boundary? → `isSideEffect` for branching, `runPipeResult` to unwrap
374
399
  - Large/unbounded/iterable data? → `fp-pack/stream`
375
400
 
376
401
  ---
377
402
 
378
- ## Quick Reference
379
-
380
- ### Import Paths
403
+ ## Import Paths
381
404
 
382
405
  - Main: `import { pipe, map, filter } from 'fp-pack'`
383
- - SideEffect pipes: `import { pipeSideEffect, pipeSideEffectStrict, SideEffect } from 'fp-pack'`
406
+ - SideEffect: `import { pipeSideEffect, SideEffect } from 'fp-pack'`
384
407
  - Async: `import { pipeAsync, retry, timeout } from 'fp-pack'`
385
408
  - Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
386
409
 
387
- ### When to Use What
388
-
389
- - Pure sync transforms: `pipe`
390
- - Pure async transforms: `pipeAsync`
391
- - Early-exit pipelines: `pipeSideEffect*` / `pipeAsyncSideEffect*`
392
- - Strict effect unions: prefer `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
393
- - Runtime branching: prefer `isSideEffect`
394
- - Boundary unwrapping: `runPipeResult` (provide generics if inference is widened)
395
- - Large/unbounded data: `fp-pack/stream`
396
-
397
- ---
398
-
399
- ## Anti-Patterns (Avoid)
400
-
401
- - Imperative loops when a pipeline is clearer
402
- - Mutating inputs (prefer immutable transforms)
403
- - Chaining `Array.prototype.*` in complex transforms (prefer `pipe`)
404
- - Calling `runPipeResult` inside a SideEffect-aware pipeline
405
- - Adding “classic monads” to emulate Option/Either instead of using `SideEffect` pipes when necessary
406
-
407
410
  ---
408
411
 
409
412
  ## Quick Signature Lookup (When Unsure)
410
413
 
411
- Prefer local types first:
414
+ If TypeScript inference is stuck or you need to verify a function signature:
415
+
416
+ **In fp-pack project:**
412
417
  - Main types: `dist/index.d.ts`
413
418
  - Stream types: `dist/stream/index.d.ts`
414
419
 
415
- If you’re in a consumer project:
416
- - `node_modules/fp-pack/dist/index.d.ts`
417
- - `node_modules/fp-pack/dist/stream/index.d.ts`
418
-
419
- ---
420
-
421
- ## Writing New Helpers (If Needed)
422
-
423
- If you add your own utilities that should compose well:
424
- - Keep them **unary** for pipelines (or return a unary function via currying).
425
- - Prefer **data-last** argument order.
426
- - For generic/overloaded helpers, consider providing an explicit type alias to preserve inference in TypeScript.
420
+ **In consumer project:**
421
+ - Main types: `node_modules/fp-pack/dist/index.d.ts`
422
+ - Stream types: `node_modules/fp-pack/dist/stream/index.d.ts`
427
423
 
428
424
  ---
429
425
 
430
426
  ## Summary
431
427
 
432
- Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary (with generics if inference widens to `any`).
428
+ Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fp-pack",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
5
  "description": "Functional programming utilities library for TypeScript",
6
6
  "keywords": [