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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/agents/rnd-builder.md +98 -0
- package/agents/rnd-integrator.md +104 -0
- package/agents/rnd-planner.md +208 -0
- package/agents/rnd-verifier.md +164 -0
- package/dist/doctor.js +166 -0
- package/dist/doctor.js.map +1 -0
- package/dist/gates/bash-discipline.js +27 -0
- package/dist/gates/bash-discipline.js.map +1 -0
- package/dist/gates/read-evidence-pack.js +23 -0
- package/dist/gates/read-evidence-pack.js.map +1 -0
- package/dist/gates/registry.js +24 -0
- package/dist/gates/registry.js.map +1 -0
- package/dist/gates/rnd-dir-required.js +31 -0
- package/dist/gates/rnd-dir-required.js.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/prompts.js +58 -0
- package/dist/orchestrator/prompts.js.map +1 -0
- package/dist/orchestrator/rnd-dir.js +20 -0
- package/dist/orchestrator/rnd-dir.js.map +1 -0
- package/dist/orchestrator/spawn.js +67 -0
- package/dist/orchestrator/spawn.js.map +1 -0
- package/dist/orchestrator/start.js +195 -0
- package/dist/orchestrator/start.js.map +1 -0
- package/dist/orchestrator/state.js +15 -0
- package/dist/orchestrator/state.js.map +1 -0
- package/dist/orchestrator/types.js +2 -0
- package/dist/orchestrator/types.js.map +1 -0
- package/docs/PI-API.md +574 -0
- package/docs/PORTING.md +105 -0
- package/package.json +57 -0
- package/skills/fp-practices/SKILL.md +128 -0
- package/skills/fp-practices/bash.md +114 -0
- package/skills/fp-practices/duckdb.md +116 -0
- package/skills/fp-practices/elixir.md +115 -0
- package/skills/fp-practices/javascript.md +119 -0
- package/skills/fp-practices/koka.md +120 -0
- package/skills/fp-practices/lean.md +120 -0
- package/skills/fp-practices/postgresql.md +120 -0
- package/skills/fp-practices/python.md +120 -0
- package/skills/fp-practices/svelte.md +114 -0
- package/skills/kiss-practices/SKILL.md +41 -0
- package/skills/kiss-practices/bash.md +70 -0
- package/skills/kiss-practices/duckdb.md +30 -0
- package/skills/kiss-practices/elixir.md +38 -0
- package/skills/kiss-practices/javascript.md +43 -0
- package/skills/kiss-practices/koka.md +34 -0
- package/skills/kiss-practices/lean.md +45 -0
- package/skills/kiss-practices/markdown.md +20 -0
- package/skills/kiss-practices/postgresql.md +31 -0
- package/skills/kiss-practices/python.md +64 -0
- package/skills/kiss-practices/svelte.md +59 -0
- package/skills/rnd-building/SKILL.md +256 -0
- package/skills/rnd-decomposition/SKILL.md +188 -0
- package/skills/rnd-experiments/SKILL.md +197 -0
- package/skills/rnd-failure-modes/SKILL.md +222 -0
- package/skills/rnd-iteration/SKILL.md +170 -0
- package/skills/rnd-orchestration/SKILL.md +314 -0
- package/skills/rnd-scaling/SKILL.md +188 -0
- package/skills/rnd-verification/SKILL.md +248 -0
- 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.
|