pi-rnd 0.2.1

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/agents/rnd-builder.md +98 -0
  4. package/agents/rnd-integrator.md +104 -0
  5. package/agents/rnd-planner.md +208 -0
  6. package/agents/rnd-verifier.md +164 -0
  7. package/dist/doctor.js +166 -0
  8. package/dist/doctor.js.map +1 -0
  9. package/dist/gates/bash-discipline.js +27 -0
  10. package/dist/gates/bash-discipline.js.map +1 -0
  11. package/dist/gates/read-evidence-pack.js +23 -0
  12. package/dist/gates/read-evidence-pack.js.map +1 -0
  13. package/dist/gates/registry.js +24 -0
  14. package/dist/gates/registry.js.map +1 -0
  15. package/dist/gates/rnd-dir-required.js +31 -0
  16. package/dist/gates/rnd-dir-required.js.map +1 -0
  17. package/dist/index.js +20 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/orchestrator/prompts.js +58 -0
  20. package/dist/orchestrator/prompts.js.map +1 -0
  21. package/dist/orchestrator/rnd-dir.js +20 -0
  22. package/dist/orchestrator/rnd-dir.js.map +1 -0
  23. package/dist/orchestrator/spawn.js +67 -0
  24. package/dist/orchestrator/spawn.js.map +1 -0
  25. package/dist/orchestrator/start.js +195 -0
  26. package/dist/orchestrator/start.js.map +1 -0
  27. package/dist/orchestrator/state.js +15 -0
  28. package/dist/orchestrator/state.js.map +1 -0
  29. package/dist/orchestrator/types.js +2 -0
  30. package/dist/orchestrator/types.js.map +1 -0
  31. package/docs/PI-API.md +574 -0
  32. package/docs/PORTING.md +105 -0
  33. package/package.json +57 -0
  34. package/skills/fp-practices/SKILL.md +128 -0
  35. package/skills/fp-practices/bash.md +114 -0
  36. package/skills/fp-practices/duckdb.md +116 -0
  37. package/skills/fp-practices/elixir.md +115 -0
  38. package/skills/fp-practices/javascript.md +119 -0
  39. package/skills/fp-practices/koka.md +120 -0
  40. package/skills/fp-practices/lean.md +120 -0
  41. package/skills/fp-practices/postgresql.md +120 -0
  42. package/skills/fp-practices/python.md +120 -0
  43. package/skills/fp-practices/svelte.md +114 -0
  44. package/skills/kiss-practices/SKILL.md +41 -0
  45. package/skills/kiss-practices/bash.md +70 -0
  46. package/skills/kiss-practices/duckdb.md +30 -0
  47. package/skills/kiss-practices/elixir.md +38 -0
  48. package/skills/kiss-practices/javascript.md +43 -0
  49. package/skills/kiss-practices/koka.md +34 -0
  50. package/skills/kiss-practices/lean.md +45 -0
  51. package/skills/kiss-practices/markdown.md +20 -0
  52. package/skills/kiss-practices/postgresql.md +31 -0
  53. package/skills/kiss-practices/python.md +64 -0
  54. package/skills/kiss-practices/svelte.md +59 -0
  55. package/skills/rnd-building/SKILL.md +256 -0
  56. package/skills/rnd-decomposition/SKILL.md +188 -0
  57. package/skills/rnd-experiments/SKILL.md +197 -0
  58. package/skills/rnd-failure-modes/SKILL.md +222 -0
  59. package/skills/rnd-iteration/SKILL.md +170 -0
  60. package/skills/rnd-orchestration/SKILL.md +314 -0
  61. package/skills/rnd-scaling/SKILL.md +188 -0
  62. package/skills/rnd-verification/SKILL.md +248 -0
  63. package/skills/using-rnd-framework/SKILL.md +65 -0
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "pi-rnd",
3
+ "version": "0.2.1",
4
+ "description": "Scientific-method R&D orchestration for PI Coding Agent — methodology skills, subagent definitions, /rnd-start pipeline, /rnd-doctor, and a composable tool_call gate registry",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "watch": "tsc --watch",
10
+ "prepack": "tsc"
11
+ },
12
+ "pi": {
13
+ "extensions": [
14
+ "./dist/index.js"
15
+ ],
16
+ "skills": [
17
+ "./skills"
18
+ ]
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://tangled.org/oleksify.me/pi-rnd.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://tangled.org/oleksify.me/pi-rnd/issues"
26
+ },
27
+ "homepage": "https://tangled.org/oleksify.me/pi-rnd",
28
+ "keywords": [
29
+ "pi",
30
+ "pi-extension",
31
+ "pi-package",
32
+ "rnd",
33
+ "orchestration",
34
+ "scientific-method",
35
+ "subagents"
36
+ ],
37
+ "peerDependencies": {
38
+ "@earendil-works/pi-coding-agent": "^0.74.0",
39
+ "@tintinweb/pi-subagents": "^0.7.3"
40
+ },
41
+ "devDependencies": {
42
+ "@earendil-works/pi-coding-agent": "^0.74.0",
43
+ "@types/node": "^22.0.0",
44
+ "typescript": "^5.6.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "files": [
50
+ "dist/",
51
+ "agents/",
52
+ "skills/",
53
+ "docs/",
54
+ "README.md",
55
+ "LICENSE"
56
+ ]
57
+ }
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: fp-practices
3
+ description: "Use alongside KISS practices to guide agents toward functional programming patterns — pure functions, data transformations, composition, command-query separation, and immutability"
4
+ effort: low
5
+ ---
6
+
7
+ # Functional Programming Practices
8
+
9
+ ## Overview
10
+
11
+ Concrete rules for writing code in a functional style. These complement KISS practices — KISS prevents over-engineering, FP practices guide _how_ to structure the code that remains.
12
+
13
+ **Core principle:** Separate what you compute from what you do. Pure logic in, effects out.
14
+
15
+ ## How to Use
16
+
17
+ **During Phase 0 (Discovery):**
18
+ 1. Detect which languages/frameworks are present in the project (by file extensions, config files, or dependencies)
19
+ 2. Read only the relevant language files from the `fp-practices` skill directory (skill is auto-injected via PI's skill discovery — sibling `.md` files are in the same directory as this file)
20
+ 3. Include the language-specific FP rules in the discovery context passed to the Planner
21
+
22
+ **Language detection heuristics:**
23
+
24
+ | Files present | Load |
25
+ |---|---|
26
+ | `*.sh`, `*.bash`, `Makefile` | `bash.md` |
27
+ | `*.ex`, `*.exs`, `mix.exs` | `elixir.md` |
28
+ | `*.js`, `*.ts`, `*.jsx`, `*.tsx`, `*.css`, `*.html` | `javascript.md` |
29
+ | `*.py`, `pyproject.toml`, `requirements.txt` | `python.md` |
30
+ | `*.lean`, `lakefile.lean` | `lean.md` |
31
+ | `*.svelte`, `svelte.config.*` | `svelte.md` |
32
+ | `*.kk`, `koka.json` | `koka.md` |
33
+ | `mix.exs` with `:postgrex` or `:ecto`, or `*.sql` files | `postgresql.md` |
34
+ | DuckDB usage, `*.duckdb` files, or analytical/data tasks | `duckdb.md` |
35
+
36
+ **Overriding:** Projects can ship their own `fp-practices` skill in `.pi/skills/fp-practices/SKILL.md` to override these defaults with project-specific rules.
37
+
38
+ ## The Rules
39
+
40
+ ### 1. Pure Functions First
41
+
42
+ A pure function takes inputs and returns outputs. It does not read globals, write files, call APIs, or mutate arguments.
43
+
44
+ **Do:**
45
+ - Write the computation as a pure function that takes data and returns data
46
+ - Push side effects (I/O, database, network) to the caller or the edges of the system
47
+ - Pass dependencies as arguments, not through closures over mutable state
48
+
49
+ **Don't:**
50
+ - Mix computation and I/O in the same function — a function that calculates a price AND saves it to the database does two things
51
+ - Read environment variables or config inside business logic — pass the values in
52
+ - Use `Date.now()`, `Math.random()`, or other non-deterministic calls inside pure logic — inject them as arguments
53
+
54
+ ### 2. Data Transformations Over Mutation
55
+
56
+ Express logic as a pipeline of transformations on data, not a sequence of mutations on state.
57
+
58
+ **Do:**
59
+ - Use map, filter, reduce (or language equivalents) to transform collections
60
+ - Return new data structures instead of modifying existing ones
61
+ - Chain transformations: `input |> validate |> transform |> format`
62
+
63
+ **Don't:**
64
+ - Build up results with `let result = []; for (...) { result.push(...) }` when `items.map(...)` works
65
+ - Mutate function arguments — if you need to change shape, return a new object
66
+ - Use index-based loops when the intent is "transform each item" — map expresses intent better
67
+
68
+ ### 3. Composition Over Inheritance
69
+
70
+ Build behavior by combining small, focused functions — not by extending class hierarchies.
71
+
72
+ **Do:**
73
+ - Write small functions that do one thing and compose them: `const process = compose(validate, transform, save)`
74
+ - Use higher-order functions (functions that take or return functions) for shared behavior
75
+ - Prefer data + functions over objects + methods — a plain object with helper functions is often simpler than a class
76
+
77
+ **Don't:**
78
+ - Create class hierarchies for code reuse — use composition instead
79
+ - Add `extends` or `super` when a function parameter achieves the same thing
80
+ - Build "base classes" that child classes override — pass behavior as functions
81
+
82
+ ### 4. Command-Query Separation
83
+
84
+ A function either returns data (query) or causes an effect (command). Never both.
85
+
86
+ **Do:**
87
+ - Functions that compute or look up data should return the result and have no side effects
88
+ - Functions that perform effects (save, send, delete) should not return computed data — return only success/failure status if needed
89
+ - Separate "decide what to do" (pure) from "do it" (effectful)
90
+
91
+ **Don't:**
92
+ - Write `getOrCreate` functions that both query and mutate — split into `find` + `create`
93
+ - Return a value AND log/save/send as a side effect in the same function
94
+ - Mix validation (pure) with rejection actions (effectful) — validate first, act on the result
95
+
96
+ ### 5. Immutability by Default
97
+
98
+ Declare bindings as immutable unless mutation is specifically needed.
99
+
100
+ **Do:**
101
+ - Use `const` (JS/TS), `val` (Kotlin), `let` (Swift/Rust), or the immutable equivalent in your language
102
+ - Use spread/destructuring to create modified copies: `{...user, name: newName}`
103
+ - Prefer `readonly` types in TypeScript for function parameters
104
+
105
+ **Don't:**
106
+ - Use `let`/`var` when the value is assigned once and never changed
107
+ - Mutate arrays in place (`push`, `splice`) when `map`/`filter`/`concat` works
108
+ - Reassign variables to track state through a function — restructure as a pipeline instead
109
+
110
+ ### 6. Polish: Consistency and Organization
111
+
112
+ **Do:**
113
+ - Use one naming convention for pure functions across modules: either `verb_noun` or `noun_verb` as the language dictates — don't mix `computeTotal`/`total_compute` styles within the same codebase
114
+ - Extract helpers shared by two or more call sites to a single canonical location — don't let the same pure transformation live in multiple modules with slight variations
115
+ - Group pure functions by the domain they operate on, not alphabetically — `user_*` functions together, `order_*` together; alphabetical grouping hides cohesion
116
+
117
+ **Don't:**
118
+ - Duplicate a pure transformation across modules to avoid adding a shared helper file — copy-pasted logic drifts and causes silent inconsistencies at the wave level
119
+ - Mix domain-level functions and low-level utility functions in the same module without clear separation — readers should be able to find all business logic in one place and all plumbing in another
120
+
121
+ ## When to Break These Rules
122
+
123
+ These rules have legitimate exceptions:
124
+
125
+ - **Performance-critical loops** — mutation in a tight loop is acceptable when measured profiling shows the functional version is too slow
126
+ - **Language idioms** — if the language strongly favors imperative style (Go, C), adapt the principles to the idiom rather than fighting it
127
+ - **Framework constraints** — React hooks, ORM callbacks, and similar framework patterns may require mutation or mixed command-query; follow the framework's conventions
128
+ - **Startup/initialization code** — building up configuration objects during app boot is naturally imperative; don't force it into a pipeline
@@ -0,0 +1,114 @@
1
+ # Bash — FP Patterns
2
+
3
+ Bash-specific patterns for the five FP rules in SKILL.md. Assumes `set -euo pipefail`.
4
+
5
+ ## 1. Pipe Composition
6
+
7
+ Express multi-step transformations as pipelines of named functions.
8
+
9
+ **Do:**
10
+ ```bash
11
+ parse_csv() { cut -d',' -f1; }
12
+ strip_blanks() { grep -v '^[[:space:]]*$'; }
13
+ uppercase() { tr '[:lower:]' '[:upper:]'; }
14
+
15
+ process_names() { parse_csv | strip_blanks | uppercase; }
16
+ ```
17
+
18
+ **Don't:** accumulate into a `result` variable with repeated `result=$(echo "$result" | ...)` — that hides the transformation and is harder to test each stage.
19
+
20
+ ## 2. Immutability — `local -r`
21
+
22
+ Use `local -r` for any binding assigned once. Documents intent; prevents accidental reassignment.
23
+
24
+ **Do:**
25
+ ```bash
26
+ normalize_path() {
27
+ local -r raw="$1"
28
+ local -r trimmed="${raw%/}"
29
+ printf '%s\n' "${trimmed:-/}"
30
+ }
31
+ ```
32
+
33
+ **Don't:** use bare `local path="$1"` and then mutate `path` in the same function — each step should produce a new named binding.
34
+
35
+ ## 3. Higher-Order Functions — map / filter / reduce
36
+
37
+ Implement the pattern as functions that accept a function name and read stdin line-by-line.
38
+
39
+ ```bash
40
+ set -euo pipefail
41
+
42
+ map_lines() { # map_lines fn < input
43
+ local -r fn="$1"
44
+ while IFS= read -r line; do "$fn" "$line"; done
45
+ }
46
+
47
+ filter_lines() { # filter_lines fn < input
48
+ local -r fn="$1"
49
+ while IFS= read -r line; do
50
+ "$fn" "$line" && printf '%s\n' "$line" || true
51
+ done
52
+ }
53
+
54
+ reduce_lines() { # reduce_lines fn init < input
55
+ local -r fn="$1"; local acc="${2:-}"
56
+ while IFS= read -r line; do acc="$("$fn" "$acc" "$line")"; done
57
+ printf '%s\n' "$acc"
58
+ }
59
+ ```
60
+
61
+ **Usage:**
62
+ ```bash
63
+ double() { printf '%d\n' "$(( $1 * 2 ))"; }
64
+ positive() { (( $1 > 0 )); }
65
+ sum() { printf '%d\n' "$(( $1 + $2 ))"; }
66
+
67
+ printf '1\n2\n3\n' | map_lines double # 2 4 6
68
+ printf '1\n-2\n3\n' | filter_lines positive # 1 3
69
+ printf '1\n2\n3\n' | reduce_lines sum 0 # 6
70
+ ```
71
+
72
+ ## 4. Side-Effect Isolation
73
+
74
+ Keep computation pure; push all I/O to the outermost caller.
75
+
76
+ **Do:**
77
+ ```bash
78
+ build_report() { local -r name="$1" count="$2"; printf '%s: %d\n' "$name" "$count"; }
79
+ write_report() { build_report "$(hostname)" 42 > "$1"; }
80
+ ```
81
+
82
+ **Don't:** mix `printf` output with `> file` redirections inside the same function — a function that both computes and writes cannot be tested without the filesystem.
83
+
84
+ ## 5. Pure Function Conventions
85
+
86
+ A pure bash function takes positional arguments, prints its result to stdout, and reads no globals.
87
+
88
+ **Do:**
89
+ ```bash
90
+ slugify() {
91
+ local -r input="$1"
92
+ printf '%s\n' "${input// /-}" | tr '[:upper:]' '[:lower:]'
93
+ }
94
+ ```
95
+
96
+ **Don't:** write results into global variables (`SLUG=...`) and expect callers to read them — use stdout so callers capture with `$()`.
97
+
98
+ ## 6. Command-Query Separation
99
+
100
+ A function either prints data (query) or causes an effect (command) — not both.
101
+
102
+ **Do:**
103
+ ```bash
104
+ user_exists() { local -r n="$1"; getent passwd "$n" > /dev/null; } # query
105
+ create_user() { local -r n="$1"; useradd "$n"; } # command
106
+ ```
107
+
108
+ **Don't:**
109
+ ```bash
110
+ ensure_user() {
111
+ useradd "$1" 2>/dev/null || true # command
112
+ id -u "$1" # query — mixed
113
+ }
114
+ ```
@@ -0,0 +1,116 @@
1
+ # DuckDB — FP Patterns
2
+
3
+ DuckDB-specific patterns for the five FP rules in SKILL.md.
4
+
5
+ ## 1. CTEs as Analytical Pipeline Stages
6
+
7
+ Decompose multi-step analysis into named CTEs rather than nested subqueries.
8
+
9
+ **Do:**
10
+ ```sql
11
+ WITH
12
+ raw AS (SELECT * FROM read_parquet('events/*.parquet')),
13
+ filtered AS (
14
+ SELECT user_id, event_type, ts
15
+ FROM raw WHERE event_type IN ('purchase', 'refund')
16
+ ),
17
+ enriched AS (
18
+ SELECT f.*, u.country FROM filtered f JOIN users u USING (user_id)
19
+ )
20
+ SELECT country, count(*) AS total FROM enriched
21
+ GROUP BY country ORDER BY total DESC;
22
+ ```
23
+
24
+ **Don't:** write a single deeply-nested `SELECT` — each logical step should be a named CTE that can be read and replaced independently.
25
+
26
+ ## 2. Window Functions as Pure Transformations
27
+
28
+ Window functions add computed columns without mutating rows — treat them as row-level pure functions.
29
+
30
+ **Do:**
31
+ ```sql
32
+ WITH sales AS (SELECT * FROM read_csv('sales.csv')),
33
+ ranked AS (
34
+ SELECT *,
35
+ row_number() OVER (PARTITION BY region ORDER BY revenue DESC) AS rank,
36
+ sum(revenue) OVER (PARTITION BY region) AS region_total
37
+ FROM sales
38
+ )
39
+ SELECT * FROM ranked WHERE rank <= 10;
40
+ ```
41
+
42
+ **Don't:** use correlated subqueries to compute running totals or ranks — window functions express the same intent with no side effects and are vectorized by DuckDB.
43
+
44
+ ## 3. QUALIFY for Declarative Row Filtering
45
+
46
+ Use `QUALIFY` to filter on window results in a single pass — no wrapping subquery needed.
47
+
48
+ **Do:**
49
+ ```sql
50
+ SELECT user_id, session_id, started_at,
51
+ row_number() OVER (PARTITION BY user_id ORDER BY started_at DESC) AS rn
52
+ FROM sessions
53
+ QUALIFY rn = 1;
54
+ ```
55
+
56
+ **Don't:** wrap in a subquery just to reference the window alias: `SELECT * FROM (...) t WHERE t.rn = 1`.
57
+
58
+ ## 4. list_transform / list_filter / list_reduce
59
+
60
+ DuckDB list functions are map/filter/reduce for nested arrays — use them instead of unnest-aggregate roundtrips.
61
+
62
+ **Do:**
63
+ ```sql
64
+ SELECT order_id,
65
+ list_transform(items, x -> x.price * x.qty) AS line_totals,
66
+ list_reduce(list_transform(items, x -> x.price * x.qty), (a, b) -> a + b) AS total,
67
+ list_filter(items, x -> x.category = 'electronics') AS electronics
68
+ FROM orders;
69
+ ```
70
+
71
+ **Don't:** unnest, aggregate with GROUP BY, then rejoin — list functions handle this in a single pass.
72
+
73
+ ## 5. read_parquet / read_csv as Pure Sources
74
+
75
+ Treat file-reading functions as pure sources. Query them directly; never stage into temp tables.
76
+
77
+ **Do:**
78
+ ```sql
79
+ WITH src AS (
80
+ SELECT * FROM read_parquet('s3://bucket/data/**/*.parquet', hive_partitioning = true)
81
+ )
82
+ SELECT year, month, sum(amount) FROM src GROUP BY year, month;
83
+ ```
84
+
85
+ **Don't:** `CREATE TABLE tmp AS SELECT ...` then query `tmp` — temp tables add mutable state and require cleanup.
86
+
87
+ ## 6. Struct Operations as Record Transformations
88
+
89
+ Bundle related columns into typed structs and transform them as values.
90
+
91
+ **Do:**
92
+ ```sql
93
+ SELECT id,
94
+ struct_pack(lat := lat, lon := lon) AS location,
95
+ struct_pack(open := open_price, close := close_price,
96
+ delta := close_price - open_price) AS ohlc
97
+ FROM market_data;
98
+ ```
99
+
100
+ **Don't:** carry many loose columns through every CTE stage — structs make the pipeline's shape explicit and prevent column-name collisions.
101
+
102
+ ## 7. Command-Query Separation
103
+
104
+ Queries return data; writes mutate state. Keep them separate.
105
+
106
+ **Do:**
107
+ ```sql
108
+ -- query: returns relation, no side effects
109
+ SELECT product_id, sum(quantity) AS sold FROM read_parquet('orders.parquet') GROUP BY 1;
110
+
111
+ -- command: isolated write
112
+ COPY (SELECT product_id, sum(quantity) AS sold FROM read_parquet('orders.parquet') GROUP BY 1)
113
+ TO 'summary.parquet' (FORMAT PARQUET);
114
+ ```
115
+
116
+ **Don't:** mix `INSERT INTO` inside a CTE that also returns rows — a CTE that both mutates and queries violates CQS and makes the pipeline non-reproducible.
@@ -0,0 +1,115 @@
1
+ # Elixir — FP Patterns
2
+
3
+ Idiomatic Elixir: data flowing through transformations, pattern matching over branching, explicit effect boundaries.
4
+
5
+ ## 1. Pipe Operator Composition
6
+
7
+ Use `|>` to express a data transformation pipeline. Each step is a named function — readable, testable, reorderable.
8
+
9
+ **Do:**
10
+ ```elixir
11
+ def process_order(params) do
12
+ params
13
+ |> validate_fields()
14
+ |> normalize_currency()
15
+ |> apply_discounts()
16
+ |> build_order_struct()
17
+ end
18
+ ```
19
+
20
+ **Don't:** nest as `build_order_struct(apply_discounts(normalize_currency(validate_fields(params))))` — unreadable and hard to insert a step.
21
+
22
+ ## 2. Pattern Matching Over Conditionals
23
+
24
+ Match on the shape and values of data directly in function heads and `case` expressions instead of if/else chains.
25
+
26
+ **Do:**
27
+ ```elixir
28
+ def handle_result({:ok, user}), do: render_user(user)
29
+ def handle_result({:error, :not_found}), do: {:error, "User not found"}
30
+ def handle_result({:error, reason}), do: {:error, "Unexpected: #{reason}"}
31
+ ```
32
+
33
+ **Don't:**
34
+ ```elixir
35
+ def handle_result(result) do
36
+ if elem(result, 0) == :ok do
37
+ render_user(elem(result, 1))
38
+ else
39
+ {:error, "failed"}
40
+ end
41
+ end
42
+ ```
43
+
44
+ ## 3. `with` Blocks for Happy-Path Chaining
45
+
46
+ Use `with` to sequence operations that each return `{:ok, value}` or `{:error, reason}`, short-circuiting on the first failure.
47
+
48
+ **Do:**
49
+ ```elixir
50
+ def create_account(params) do
51
+ with {:ok, email} <- validate_email(params.email),
52
+ {:ok, user} <- Repo.insert(%User{email: email}),
53
+ {:ok, _token} <- Mailer.send_welcome(user) do
54
+ {:ok, user}
55
+ end
56
+ end
57
+ ```
58
+
59
+ **Don't:** nest `case` expressions or use `try/rescue` for expected failures — `with` keeps the happy path linear and the error handling in one place.
60
+
61
+ ## 4. GenServer as Effect Boundary
62
+
63
+ Keep business logic in pure functions; use GenServer only to sequence effects and hold state.
64
+
65
+ **Do:**
66
+ ```elixir
67
+ # Pure logic — easy to unit test
68
+ defmodule Cart do
69
+ def add_item(%__MODULE__{} = cart, item), do: %{cart | items: [item | cart.items]}
70
+ def total(%__MODULE__{items: items}), do: Enum.sum_by(items, & &1.price)
71
+ end
72
+
73
+ # Effect boundary — wraps state + persistence
74
+ defmodule CartServer do
75
+ use GenServer
76
+ def handle_call({:add, item}, _from, cart), do: {:reply, :ok, Cart.add_item(cart, item)}
77
+ def handle_call(:total, _from, cart), do: {:reply, Cart.total(cart), cart}
78
+ end
79
+ ```
80
+
81
+ **Don't:** put `Repo.insert/1` or HTTP calls inside the pure module — separate computation from effects so core logic is testable without starting a process.
82
+
83
+ ## 5. Structs as Immutable Values
84
+
85
+ Transformations return new structs; the original is never mutated.
86
+
87
+ **Do:**
88
+ ```elixir
89
+ defmodule Invoice do
90
+ defstruct [:id, :amount, :status]
91
+ def mark_paid(%__MODULE__{} = inv), do: %{inv | status: :paid}
92
+ def apply_tax(%__MODULE__{} = inv, r), do: %{inv | amount: inv.amount * (1 + r)}
93
+ end
94
+
95
+ invoice
96
+ |> Invoice.apply_tax(0.2)
97
+ |> Invoice.mark_paid()
98
+ ```
99
+
100
+ **Don't:** use plain maps with string keys for structured domain data — structs catch misspelled keys at compile time and make pipelines self-documenting.
101
+
102
+ ## 6. Higher-Order Functions with Enum / Stream
103
+
104
+ Prefer `Enum`/`Stream` over explicit recursion. Use `Stream` for lazy evaluation of large sequences.
105
+
106
+ **Do:**
107
+ ```elixir
108
+ active_totals =
109
+ orders
110
+ |> Stream.filter(& &1.status == :active)
111
+ |> Stream.map(& &1.total)
112
+ |> Enum.sum()
113
+ ```
114
+
115
+ **Don't:** accumulate manually with `Enum.reduce` when a `filter |> map |> sum` chain expresses the intent clearly.
@@ -0,0 +1,119 @@
1
+ # JavaScript / TypeScript — FP Patterns
2
+
3
+ JS/TS-specific patterns for the five FP rules in SKILL.md.
4
+
5
+ ## 1. Array Method Chains
6
+
7
+ Express multi-step data transformations as chained array methods rather than loops with accumulated state.
8
+
9
+ **Do:**
10
+ ```js
11
+ const activeUserNames = users
12
+ .filter(u => u.active)
13
+ .map(u => u.name)
14
+ .sort();
15
+ ```
16
+
17
+ **Don't:** accumulate with a `for` loop and a mutable `result` array — that hides transformation intent and makes each stage invisible.
18
+
19
+ ## 2. Immutable Object Patterns
20
+
21
+ Produce new values instead of mutating inputs. Use spread for shallow copies; `structuredClone` for deep clones.
22
+
23
+ **Do:**
24
+ ```js
25
+ const updated = { ...user, lastLogin: Date.now() };
26
+
27
+ const deepCopy = structuredClone(nestedConfig);
28
+ deepCopy.server.port = 9000;
29
+ ```
30
+
31
+ **Don't:** mutate function arguments — callers cannot reason about their object after the call.
32
+
33
+ Use `Object.freeze` for module-level constants:
34
+
35
+ ```js
36
+ export const DEFAULT_OPTIONS = Object.freeze({ timeout: 5000, retries: 3 });
37
+ ```
38
+
39
+ ## 3. TypeScript Readonly and `as const`
40
+
41
+ Encode immutability in the type system so mutations are caught at compile time.
42
+
43
+ **Do:**
44
+ ```ts
45
+ function process(items: readonly string[]): readonly string[] {
46
+ return items.map(s => s.trim());
47
+ }
48
+
49
+ type Config = Readonly<{
50
+ host: string;
51
+ port: number;
52
+ }>;
53
+ ```
54
+
55
+ Use `as const` to narrow literal types and prevent widening:
56
+
57
+ ```ts
58
+ const DIRECTIONS = ['north', 'south', 'east', 'west'] as const;
59
+ type Direction = typeof DIRECTIONS[number]; // 'north' | 'south' | 'east' | 'west'
60
+ ```
61
+
62
+ **Don't:** accept `string[]` when the function only reads — use `readonly string[]` so TS enforces the contract at every call site.
63
+
64
+ ## 4. Higher-Order Functions
65
+
66
+ Pass behavior as arguments to keep logic composable and independently testable.
67
+
68
+ **Do:**
69
+ ```ts
70
+ const applyDiscount =
71
+ (rate: number) =>
72
+ (price: number): number =>
73
+ price * (1 - rate);
74
+
75
+ const tenPercent = applyDiscount(0.1);
76
+ const prices = [100, 200, 300].map(tenPercent); // [90, 180, 270]
77
+ ```
78
+
79
+ **Don't:** repeat the discount formula at every call site — a curried function is reusable and the rate is explicit in the name.
80
+
81
+ ## 5. Composition Utilities
82
+
83
+ Build complex transforms from small, single-purpose functions rather than nesting calls.
84
+
85
+ **Do:**
86
+ ```ts
87
+ const pipe =
88
+ <T>(...fns: Array<(x: T) => T>) =>
89
+ (x: T): T =>
90
+ fns.reduce((acc, fn) => fn(acc), x);
91
+
92
+ const normalizeEmail = pipe(
93
+ (s: string) => s.trim(),
94
+ (s: string) => s.toLowerCase(),
95
+ (s: string) => s.replace(/\s+/g, ''),
96
+ );
97
+ ```
98
+
99
+ **Don't:** nest calls three-deep — `f(g(h(x)))` obscures application order and makes inserting steps awkward.
100
+
101
+ ## 6. Command-Query Separation in Async Code
102
+
103
+ Pure data transformations are synchronous functions. Async functions contain I/O effects. Keep them separate.
104
+
105
+ **Do:**
106
+ ```ts
107
+ // Query (pure, sync)
108
+ function buildPayload(order: Order): ApiPayload {
109
+ return { id: order.id, total: order.lines.reduce((s, l) => s + l.price, 0) };
110
+ }
111
+
112
+ // Command (effectful, async)
113
+ async function submitOrder(order: Order): Promise<void> {
114
+ const payload = buildPayload(order); // pure step first
115
+ await api.post('/orders', payload);
116
+ }
117
+ ```
118
+
119
+ **Don't:** mix computation and I/O in a single async function — the pure transform cannot be unit-tested without mocking the network.