@ai-agent-lead/skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/bin/install.js +272 -0
- package/package.json +34 -0
- package/skills/LANGUAGE.md +72 -0
- package/skills/README.md +156 -0
- package/skills/SKILL-TEMPLATE.md +120 -0
- package/skills/TRIGGERS.md +64 -0
- package/skills/WORKFLOWS.md +369 -0
- package/skills/bench/SKILL.md +40 -0
- package/skills/bench/templates/benchmark-report.md +26 -0
- package/skills/bootstrap/BOOTSTRAP.md +13 -0
- package/skills/bootstrap/SKILL.md +47 -0
- package/skills/code-hygiene/SKILL.md +92 -0
- package/skills/debug/SKILL.md +122 -0
- package/skills/design/DEEP-MODULES.md +76 -0
- package/skills/design/FUNCTIONAL-CORE.md +121 -0
- package/skills/design/ILLEGAL-STATES.md +102 -0
- package/skills/design/OBSERVABILITY.md +49 -0
- package/skills/design/PERSONAS.md +41 -0
- package/skills/design/SKILL.md +139 -0
- package/skills/design/TESTABILITY.md +84 -0
- package/skills/feature-doc/SKILL.md +113 -0
- package/skills/feature-doc/templates/feature-template.md +52 -0
- package/skills/formats/ADR-FORMAT.md +51 -0
- package/skills/formats/CONTEXT-FORMAT.md +109 -0
- package/skills/formats/CONTEXT-MAP-FORMAT.md +6 -0
- package/skills/grill-plan/SKILL.md +112 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +41 -0
- package/skills/improve-codebase-architecture/SKILL.md +115 -0
- package/skills/investigate/SKILL.md +97 -0
- package/skills/investigate/templates/research-note.md +84 -0
- package/skills/pr-review/SKILL.md +197 -0
- package/skills/prod-ready/SKILL.md +88 -0
- package/skills/security-review/SKILL.md +145 -0
- package/skills/simplify/SKILL.md +105 -0
- package/skills/sync-check/SKILL.md +69 -0
- package/skills/system-design/SKILL.md +160 -0
- package/skills/tdd/SKILL.md +121 -0
- package/skills/tdd/TESTS.md +93 -0
- package/skills/tdd-rounds/COMMITS.md +122 -0
- package/skills/tdd-rounds/SKILL.md +96 -0
- package/skills/tdd-rounds/templates/builder-brief.md +73 -0
- package/skills/tdd-rounds/templates/builder-report.md +21 -0
- package/skills/verify-real-deps/MOTIVATION.md +18 -0
- package/skills/verify-real-deps/SKILL.md +118 -0
- package/skills/verify-real-deps/templates/known-issues.md +45 -0
- package/skills/zoom-out/SKILL.md +104 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bootstrap
|
|
3
|
+
description: Initializes a greenfield repository. Creates the docs/ directory, the initial CONTEXT.md, and the first ADR. Triggered by phrases like "new project", "initialize", "bootstrap".
|
|
4
|
+
complexity: low
|
|
5
|
+
expected_duration: 10 minutes
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Bootstrap
|
|
9
|
+
|
|
10
|
+
This skill makes starting a new project a first-class workflow, establishing the durable artifacts that other skills rely on. It initializes the `docs/` structure and core terminology.
|
|
11
|
+
|
|
12
|
+
## Why this skill exists
|
|
13
|
+
|
|
14
|
+
Starting from a blank slate often leads to inconsistent documentation structure. This skill enforces a canonical starting point for vocabulary and architectural decisions.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- Starting a new repository or service.
|
|
19
|
+
- Initializing the skills framework in an existing repository that lacks `docs/CONTEXT.md`.
|
|
20
|
+
|
|
21
|
+
## When to skip
|
|
22
|
+
|
|
23
|
+
- The repository already has `docs/CONTEXT.md` and an established `docs/` structure.
|
|
24
|
+
|
|
25
|
+
## Process
|
|
26
|
+
|
|
27
|
+
### 1. Initialize docs/
|
|
28
|
+
|
|
29
|
+
Create the standard directory structure:
|
|
30
|
+
- `docs/`
|
|
31
|
+
- `docs/adr/`
|
|
32
|
+
- `docs/features/`
|
|
33
|
+
- `docs/research/`
|
|
34
|
+
|
|
35
|
+
### 2. Seed CONTEXT.md
|
|
36
|
+
|
|
37
|
+
Ask the user for 3-7 core domain terms. Create `docs/CONTEXT.md` using the canonical format.
|
|
38
|
+
|
|
39
|
+
### 3. Record ADR-0000
|
|
40
|
+
|
|
41
|
+
If any major architectural decisions are made during initialization, record them in `docs/adr/0000-architectural-overview.md`.
|
|
42
|
+
|
|
43
|
+
## Done when
|
|
44
|
+
|
|
45
|
+
- `docs/` directory exists with the required subdirectories.
|
|
46
|
+
- `docs/CONTEXT.md` is seeded with core domain terms.
|
|
47
|
+
- (Optional) `docs/adr/0000-architectural-overview.md` exists.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-hygiene
|
|
3
|
+
description: Day-to-day coding discipline at the line and function level — boring code, naming as primary refactor, YAGNI, rule of 3, locality of behavior. Use when reviewing or writing code, when names feel wrong, when tempted to abstract too early, when a solution looks clever, when the simplify pass after `tdd` runs, or when the user mentions "simpler", "boring", "naming", "YAGNI", "premature abstraction", "over-engineered". Skip for module-level interface design — use `design` instead. Skip for whole-codebase architectural sweeps — use `improve-codebase-architecture`.
|
|
4
|
+
complexity: low
|
|
5
|
+
expected_duration: 5 minutes
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Code Hygiene
|
|
9
|
+
|
|
10
|
+
Day-to-day discipline that keeps a codebase readable, navigable, and easy to change. Smaller in scope than `design` (which shapes module interfaces) — these are line-level and function-level habits.
|
|
11
|
+
|
|
12
|
+
Five principles.
|
|
13
|
+
|
|
14
|
+
1. **Boring code beats clever code** — prefer the obvious solution over the elegant trick.
|
|
15
|
+
2. **Naming is the primary refactor** — a bad name misleads longer than a bad implementation.
|
|
16
|
+
3. **YAGNI** — don't build for hypothetical futures.
|
|
17
|
+
4. **Rule of 3 before extracting** — duplicate twice; extract on the third occurrence, not the second.
|
|
18
|
+
5. **Locality of behavior** — related code lives together; don't split by category.
|
|
19
|
+
|
|
20
|
+
## When to use
|
|
21
|
+
|
|
22
|
+
- Writing new code, line by line — keep these in mind as you type.
|
|
23
|
+
- Reviewing a PR — these are five common smell categories.
|
|
24
|
+
- After `tdd` reaches green, during the [`simplify`](../simplify/SKILL.md) sweep — `code-hygiene` is the lens you apply.
|
|
25
|
+
- When you read code and pause to figure out what it's doing — that pause is a smell.
|
|
26
|
+
|
|
27
|
+
## When to skip
|
|
28
|
+
|
|
29
|
+
- Module-level shape (interface, depth, dependencies) — use `design`.
|
|
30
|
+
- Whole-codebase sweeps for shallow modules — use `improve-codebase-architecture`.
|
|
31
|
+
- The horizontal-vs-vertical TDD failure mode — that's `tdd`'s territory.
|
|
32
|
+
|
|
33
|
+
## Principle 1: Boring code beats clever code
|
|
34
|
+
|
|
35
|
+
When there's an obvious solution and a clever one, pick the obvious. Cleverness is a tax on every reader who comes after.
|
|
36
|
+
|
|
37
|
+
**Smell**: a one-liner using bit manipulation, regex acrobatics, or chained ternaries to do what a four-line `if` would do clearly.
|
|
38
|
+
|
|
39
|
+
**Rule of thumb**: if reading the code feels like solving a puzzle, that's a smell — even when the puzzle has a satisfying answer. Save cleverness for places where it earns its cost (a hot loop you've actually profiled, a parser, a constraint solver).
|
|
40
|
+
|
|
41
|
+
## Principle 2: Naming is the primary refactor
|
|
42
|
+
|
|
43
|
+
Bad code with great names is debuggable; great code with bad names misleads forever. Names live longer than implementations.
|
|
44
|
+
|
|
45
|
+
**Smells**:
|
|
46
|
+
- A variable named `data`, `result`, `tmp`, `value`, or `item` that survives more than ~5 lines.
|
|
47
|
+
- A function named `process`, `handle`, `run`, or `do` that does anything specific.
|
|
48
|
+
- A boolean named `flag`, or a name with `Manager` / `Helper` / `Util` suffix that hides what the thing actually is.
|
|
49
|
+
- A type named after the *shape* of the data (`UserData`, `OrderInfo`) instead of its *meaning* (`UnverifiedUser`, `PendingOrder`).
|
|
50
|
+
- A function name that doesn't match what it does (especially: `getX` that mutates, or `isX` that returns non-boolean).
|
|
51
|
+
|
|
52
|
+
**Fix the name first.** Even before fixing the implementation. The name is the documentation everyone reads.
|
|
53
|
+
|
|
54
|
+
## Principle 3: YAGNI — You Aren't Gonna Need It
|
|
55
|
+
|
|
56
|
+
Don't build for hypothetical futures. Don't add a parameter "in case we need it later". Don't extract an interface "in case there's a second implementation". Don't write the configurable version of a thing that has one configuration.
|
|
57
|
+
|
|
58
|
+
**Why**: hypothetical futures rarely arrive in the shape you predicted. Code written for them ages worse than code added when the need is real.
|
|
59
|
+
|
|
60
|
+
**Exception**: when the cost of *not* designing for it later is provably much higher than the cost of designing for it now (e.g. schema migrations under load, public APIs with downstream consumers, security-sensitive surfaces). The bar is *provably* — not "I have a feeling".
|
|
61
|
+
|
|
62
|
+
## Principle 4: Rule of 3 before extracting
|
|
63
|
+
|
|
64
|
+
Duplicate twice; extract on the third occurrence — not the second.
|
|
65
|
+
|
|
66
|
+
The first occurrence is unique. The second might be coincidence. The third is a pattern. Extracting at two reveals only one axis of variation; extracting at three reveals the *real* axis.
|
|
67
|
+
|
|
68
|
+
**Why**: premature abstractions calcify. Once a wrong abstraction exists, callers shape themselves to it, and rewriting becomes expensive. Three concrete copies are cheap; one wrong abstraction is not.
|
|
69
|
+
|
|
70
|
+
**Smell**: a helper function with one caller, or a base class with one subclass. That's an abstraction in search of a use.
|
|
71
|
+
|
|
72
|
+
## Principle 5: Locality of behavior
|
|
73
|
+
|
|
74
|
+
Related code lives close together. Don't split a system by *type of code* (`controllers/`, `services/`, `repositories/`) — split by *responsibility* (`orders/`, `billing/`, `auth/`).
|
|
75
|
+
|
|
76
|
+
**Why**: a new contributor should be able to read one folder and understand one feature, not bounce across five folders to follow one request.
|
|
77
|
+
|
|
78
|
+
**Smell**: changing one feature requires editing 5 files in 5 directories. That's a sign the structure separates *type* of code, not *responsibility*. (This is a `improve-codebase-architecture` issue at scale, but at smaller scale you can fix it inline by colocating files.)
|
|
79
|
+
|
|
80
|
+
## Done when
|
|
81
|
+
|
|
82
|
+
- Names communicate intent — a stranger reads them and forms the right mental model.
|
|
83
|
+
- The clever shortcut is replaced with the obvious version (or its cleverness is justified by a comment naming the constraint).
|
|
84
|
+
- No "in case we need it" parameters, classes, or interfaces remain.
|
|
85
|
+
- Duplications either survived the 2-occurrence test (left as-is) or proved themselves at the 3rd occurrence (extracted).
|
|
86
|
+
- Related code lives near related code.
|
|
87
|
+
|
|
88
|
+
## Pairing with other skills
|
|
89
|
+
|
|
90
|
+
- **`design`** sets module shape; `code-hygiene` polishes within the module. Different scopes; both apply.
|
|
91
|
+
- **`tdd`** reaches green; `code-hygiene` is part of the simplify sweep that follows.
|
|
92
|
+
- **`improve-codebase-architecture`** finds shallow modules; if the diagnosis is "shallow" but the fix is line-level (rename, inline, delete dead helper), this skill applies. If the fix is structural (deepen the module), that one does.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: debug
|
|
3
|
+
description: Disciplined reproduction, isolation, and hypothesis-testing for non-trivial bugs — runs BEFORE `tdd` when the failing assertion isn't yet known. Use when the user reports a bug whose root cause is not obvious from the symptom — triggered by phrases like "it's broken", "this is failing", "intermittent", "flaky", "regression", "not sure why", "production issue", "doesn't work in <env>". Skip for typos, clear stack traces with one-step fixes, or bugs whose fix is obvious from reading the message. Pairs with `tdd` (downstream — the failing test crystallises once the bug is reproduced) and `zoom-out` (upstream, when the area is unfamiliar).
|
|
4
|
+
complexity: high
|
|
5
|
+
expected_duration: 45 minutes
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Debug
|
|
9
|
+
|
|
10
|
+
The discipline of finding a root cause before writing a fix. TDD says "write a failing test"; for a non-trivial bug, you don't yet know what the failing test should assert. This skill is the step between *symptom* and *test*.
|
|
11
|
+
|
|
12
|
+
## Why this skill exists
|
|
13
|
+
|
|
14
|
+
Jumping to a fix without a clean reproduction risks fixing the wrong thing — or fixing the right thing for the wrong reason. Both leave the bug latent. The discipline of reproducing → isolating → hypothesis-testing produces:
|
|
15
|
+
|
|
16
|
+
- A **minimum reproduction** the future failing test can assert on.
|
|
17
|
+
- A **named root cause** distinct from the symptom — the thing that has to change.
|
|
18
|
+
- A **bisected blast radius** — what else this might affect, what else might be affected by the fix.
|
|
19
|
+
|
|
20
|
+
Without this, "fixed in production" often means "symptom no longer visible from the angle we looked at."
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- Bug whose root cause is not obvious from the message or stack trace.
|
|
25
|
+
- Intermittent / flaky failure — passes locally, fails in CI; passes most of the time, fails sometimes.
|
|
26
|
+
- Regression — worked yesterday, broken today, unclear what changed.
|
|
27
|
+
- "Doesn't work in production" / "doesn't work in <env>" — environment-specific behaviour.
|
|
28
|
+
- Concurrency, timing, or ordering-dependent symptoms.
|
|
29
|
+
- The user says "I don't know what's wrong" — that's the trigger.
|
|
30
|
+
|
|
31
|
+
## When to skip
|
|
32
|
+
|
|
33
|
+
- Typo / off-by-one / null-check fixes obvious from the stack trace. Just fix it.
|
|
34
|
+
- A test you wrote that fails with a clear assertion message — fix the code, the test already pinned the behaviour.
|
|
35
|
+
- Bugs covered by an existing failing test — go straight to `tdd`'s green step.
|
|
36
|
+
- "Bug" that's actually a feature request / unclear requirements — that's a `feature-doc` problem, not a `debug` one.
|
|
37
|
+
|
|
38
|
+
## Phases
|
|
39
|
+
|
|
40
|
+
Each phase is a stop. Don't start the next until the previous is grounded.
|
|
41
|
+
|
|
42
|
+
### 1. Reproduce — find the smallest input that triggers it
|
|
43
|
+
|
|
44
|
+
Without a reliable reproduction, every "fix" is a guess. The reproduction is the contract you're buying.
|
|
45
|
+
|
|
46
|
+
- **Capture the symptom precisely.** What's the exact error / output / observable behaviour? What's the expected? Quote it; don't paraphrase.
|
|
47
|
+
- **Capture the environment.** OS, language version, dependency versions, database version, env vars in play, time of day if relevant. Bugs hide in unstated context.
|
|
48
|
+
- **Find the smallest input that triggers it.** Trim until removing one more piece makes the bug disappear. The minimum repro is the seed of the failing test.
|
|
49
|
+
- **Make it reliable.** If it's intermittent, is it really 50/50, or 5%, or "only when run after test X"? Quantify or you can't tell when you've fixed it.
|
|
50
|
+
|
|
51
|
+
If you cannot reproduce, **stop and say so.** "Can't reproduce" is a valid debug outcome that warrants better instrumentation, not a guess at a fix.
|
|
52
|
+
|
|
53
|
+
### 2. Isolate — narrow to the failing region
|
|
54
|
+
|
|
55
|
+
Don't read the whole codebase. Bisect.
|
|
56
|
+
|
|
57
|
+
- **`git bisect`** for regressions. Find the commit that introduced the change.
|
|
58
|
+
- **Logs / tracing** — add structured logs at suspect boundaries; don't read code that hasn't been confirmed to execute.
|
|
59
|
+
- **Diff your assumptions against the code.** If you believe path A executes, prove it. Print, log, breakpoint.
|
|
60
|
+
- **Walk the data, not the code.** Trace one specific input through the system; see where the actual value diverges from the expected. The divergence point is the bug's region.
|
|
61
|
+
|
|
62
|
+
The output of this phase is a **named region**: a function, a config key, a boundary between two modules. Not "somewhere in the auth code" — `validateToken` at `auth.go:142`.
|
|
63
|
+
|
|
64
|
+
### 3. Hypothesis test — one variable at a time
|
|
65
|
+
|
|
66
|
+
For each suspected root cause, form a falsifiable hypothesis and test it.
|
|
67
|
+
|
|
68
|
+
- **State the hypothesis.** "I think the bug is that X happens when Y." If you can't state it, you don't have one.
|
|
69
|
+
- **Predict.** If the hypothesis is true, what should happen when I change Z? If it's false, what should happen?
|
|
70
|
+
- **Test.** Change *one thing*. Observe.
|
|
71
|
+
- **Update.** Hypothesis confirmed → proceed to fix. Falsified → form another. Don't try to confirm two hypotheses at once; you'll learn nothing from the result.
|
|
72
|
+
|
|
73
|
+
Most "I don't know what's wrong" bugs are debugger-friendly with this discipline. Most "I tried five things" sessions skipped it.
|
|
74
|
+
|
|
75
|
+
### 4. Name the root cause
|
|
76
|
+
|
|
77
|
+
State the root cause in one sentence, distinct from the symptom.
|
|
78
|
+
|
|
79
|
+
- **Symptom**: "checkout returns 500 on Tuesdays."
|
|
80
|
+
- **Root cause**: "discount-rule cache TTL is 24h but the rule table is rebuilt nightly at 03:00 UTC; Tuesday-morning requests hit the stale cache because Monday's TTL hasn't expired."
|
|
81
|
+
|
|
82
|
+
The root cause names *what has to change*. If the sentence is fuzzy, the bug isn't isolated yet — go back to phase 2.
|
|
83
|
+
|
|
84
|
+
### 5. Hand off to TDD
|
|
85
|
+
|
|
86
|
+
Once the root cause is named:
|
|
87
|
+
|
|
88
|
+
- The **minimum reproduction** is the seed of the failing test (step 1 of `tdd`'s red phase).
|
|
89
|
+
- The **named region** is where the fix lands.
|
|
90
|
+
- The **hypothesis** describes what behaviour the fix changes.
|
|
91
|
+
|
|
92
|
+
Run `tdd`: write a failing test that captures the reproduction, fix, refactor with the test as a safety net.
|
|
93
|
+
|
|
94
|
+
## Optional artifact: bug research note
|
|
95
|
+
|
|
96
|
+
For non-trivial bugs whose investigation produced real signal — bisected commits, environment-specific findings, surprising cross-module interactions — capture a research note at `docs/research/<bug-slug>.md` (use the `investigate` template's shape). The note reads as the post-mortem: what was symptom, what was root cause, why was it not caught earlier, what test would have caught it.
|
|
97
|
+
|
|
98
|
+
Skip for bugs with one-paragraph stories. Capture for bugs with real lessons.
|
|
99
|
+
|
|
100
|
+
## Anti-patterns
|
|
101
|
+
|
|
102
|
+
- **Fix-then-verify.** "I think this is the issue, let me change it and see." That's hypothesis-testing without the discipline — every change becomes a confounder. Reproduce reliably first.
|
|
103
|
+
- **Shotgun debugging.** Changing several things at once. If any one fixes it, you don't know which.
|
|
104
|
+
- **Reading without running.** "I read the code and I think the bug is X." Read confirms hypothesis; running falsifies it. Run.
|
|
105
|
+
- **Symptom-as-bug.** "Fixing" the visible error without naming the root cause. The fix might suppress the symptom while leaving the cause to surface elsewhere.
|
|
106
|
+
- **Skipping the minimum repro.** A 1000-line repro is not a repro; it's an environment. Trim.
|
|
107
|
+
- **Calling it intermittent without quantifying.** "Sometimes it fails" is not actionable. "9/100 runs in CI, 0/100 locally" is.
|
|
108
|
+
|
|
109
|
+
## Pairing with other skills
|
|
110
|
+
|
|
111
|
+
- **`tdd`** runs *after*. The reproduction becomes the failing test. The named region is where the fix lands.
|
|
112
|
+
- **`zoom-out`** runs *before* if the area is unfamiliar. Map first, then debug — easier to bisect when you know the topology.
|
|
113
|
+
- **`investigate`** runs *instead* if the "bug" turns out to be an unclear requirement (no obvious correct behaviour to assert). Don't force a debug session on what's actually a design question.
|
|
114
|
+
- **`prod-ready`** Section 7's doc-map: if the bug surfaced a missed invariant, decision, or domain term, capture it on the way out (ADR / CONTEXT.md update).
|
|
115
|
+
- **`verify-real-deps`** is the upstream prevention layer for wire-shape bugs against third-party APIs. If `debug` finds one of those, log it in `docs/known-issues.md` and tighten the fake.
|
|
116
|
+
|
|
117
|
+
## Done when
|
|
118
|
+
|
|
119
|
+
- A minimum, reliable reproduction exists.
|
|
120
|
+
- The root cause is named in one sentence, distinct from the symptom.
|
|
121
|
+
- The blast radius is known (what else this affects; what else could be affected by the fix).
|
|
122
|
+
- Handoff to `tdd` is unambiguous: the test to write is clear from the reproduction.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Deep Modules
|
|
2
|
+
|
|
3
|
+
From John Ousterhout's *A Philosophy of Software Design*.
|
|
4
|
+
|
|
5
|
+
> The principle is language-agnostic. Examples below use TypeScript for readability — translate naturally to Python (`requests`-style), Go (struct + methods), Rust (trait + impl), Kotlin / Java (interface + class), etc. The shape "small interface, deep implementation" matters; the syntax doesn't.
|
|
6
|
+
|
|
7
|
+
## The idea
|
|
8
|
+
|
|
9
|
+
A module's value is **functionality minus interface complexity**. Deep modules give you a lot of functionality behind a simple interface. Shallow modules give you little functionality and force the caller to deal with the complexity anyway.
|
|
10
|
+
|
|
11
|
+
## Shallow vs deep
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
SHALLOW (avoid) DEEP (prefer)
|
|
15
|
+
|
|
16
|
+
┌───────────────────────┐ ┌──────────────┐
|
|
17
|
+
│ open, read, seek, │ │ readFile │
|
|
18
|
+
│ close, lock, unlock, │ └──────┬───────┘
|
|
19
|
+
│ flush, ... │ │
|
|
20
|
+
├───────────────────────┤ ┌──────┴───────┐
|
|
21
|
+
│ thin pass-through │ │ open, read, │
|
|
22
|
+
└───────────────────────┘ │ close, retry,│
|
|
23
|
+
│ buffer, ... │
|
|
24
|
+
└──────────────┘
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The shallow version makes the *caller* manage file lifecycles. The deep version handles it internally and exposes one method.
|
|
28
|
+
|
|
29
|
+
## Concrete example
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// SHALLOW — caller does most of the work
|
|
33
|
+
class HttpClient {
|
|
34
|
+
buildUrl(base: string, path: string, params: object): string;
|
|
35
|
+
buildHeaders(auth: string, contentType: string): object;
|
|
36
|
+
serialize(body: any): string;
|
|
37
|
+
parse(response: string): any;
|
|
38
|
+
send(method: string, url: string, headers: object, body: string): Promise<string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// caller code:
|
|
42
|
+
const url = client.buildUrl(BASE, "/users", { id: 123 });
|
|
43
|
+
const headers = client.buildHeaders(token, "application/json");
|
|
44
|
+
const body = client.serialize({ name: "Alice" });
|
|
45
|
+
const raw = await client.send("POST", url, headers, body);
|
|
46
|
+
const result = client.parse(raw);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// DEEP — interface hides the orchestration
|
|
51
|
+
class HttpClient {
|
|
52
|
+
request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// caller code:
|
|
56
|
+
const result = await client.request("POST", "/users", {
|
|
57
|
+
query: { id: 123 },
|
|
58
|
+
body: { name: "Alice" },
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Same functionality. The deep version moved complexity from every caller into one place.
|
|
63
|
+
|
|
64
|
+
## Questions to ask when designing
|
|
65
|
+
|
|
66
|
+
- Can I merge two methods into one?
|
|
67
|
+
- Can a parameter become an internal detail?
|
|
68
|
+
- Does the caller need this option, or am I exposing it because it was easy?
|
|
69
|
+
- If I removed this method, what would break — and could the remaining methods cover it?
|
|
70
|
+
|
|
71
|
+
## Warning signs of shallow modules
|
|
72
|
+
|
|
73
|
+
- Methods that are one or two lines of pass-through.
|
|
74
|
+
- Callers who always call methods in the same sequence (that sequence belongs inside the module).
|
|
75
|
+
- Many methods with very similar names (`getUserById`, `getUserByEmail`, `getUserByName`) — consider one `getUser(query)`.
|
|
76
|
+
- Configuration options that no caller actually varies.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Functional Core, Imperative Shell
|
|
2
|
+
|
|
3
|
+
Coined by Gary Bernhardt ("Boundaries", 2012). Push pure logic to the center; keep side effects at the edges.
|
|
4
|
+
|
|
5
|
+
> Language-agnostic. Examples use TypeScript; the same split works in Python (pure functions + dataclasses, side effects in the calling layer), Go (pure funcs + struct returns, side effects in the handler), Rust (`fn` returning `Result<Decision, _>`, effects in the binary's `main`-side), Kotlin (sealed classes for decisions, effects in coroutines / handlers). What matters: pure functions return values; the shell is the only place that touches the world.
|
|
6
|
+
|
|
7
|
+
## The shape
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────────────┐
|
|
11
|
+
│ Imperative shell │
|
|
12
|
+
│ - HTTP / RPC handlers │
|
|
13
|
+
│ - DB queries │
|
|
14
|
+
│ - File I/O │
|
|
15
|
+
│ - Time, randomness, env │
|
|
16
|
+
│ - Network calls, queues │
|
|
17
|
+
│ │
|
|
18
|
+
│ ┌──────────────────────────────┐ │
|
|
19
|
+
│ │ Functional core │ │
|
|
20
|
+
│ │ - Pure transformations │ │
|
|
21
|
+
│ │ - Decisions │ │
|
|
22
|
+
│ │ - Validations │ │
|
|
23
|
+
│ │ - Calculations │ │
|
|
24
|
+
│ │ - State derivations │ │
|
|
25
|
+
│ └──────────────────────────────┘ │
|
|
26
|
+
└─────────────────────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The shell:
|
|
30
|
+
- Reads inputs from the world
|
|
31
|
+
- Calls the core
|
|
32
|
+
- Writes outputs back to the world
|
|
33
|
+
|
|
34
|
+
The core never touches the world directly.
|
|
35
|
+
|
|
36
|
+
## Refactoring example
|
|
37
|
+
|
|
38
|
+
WEAK — mixed
|
|
39
|
+
```ts
|
|
40
|
+
async function processOrder(orderId: string) {
|
|
41
|
+
const order = await db.orders.get(orderId);
|
|
42
|
+
const stock = await api.checkStock(order.items);
|
|
43
|
+
|
|
44
|
+
if (stock.allAvailable) {
|
|
45
|
+
order.status = "confirmed";
|
|
46
|
+
order.confirmedAt = new Date();
|
|
47
|
+
await db.orders.save(order);
|
|
48
|
+
await emailer.send(order.userId, "confirmed");
|
|
49
|
+
return order;
|
|
50
|
+
} else {
|
|
51
|
+
order.status = "backordered";
|
|
52
|
+
await db.orders.save(order);
|
|
53
|
+
return order;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To test this you must mock `db`, `api`, `emailer`, AND the date. The test mostly verifies the mocks.
|
|
59
|
+
|
|
60
|
+
STRONG — split
|
|
61
|
+
```ts
|
|
62
|
+
// Functional core — pure
|
|
63
|
+
type OrderDecision =
|
|
64
|
+
| { kind: "confirm"; newStatus: "confirmed"; confirmedAt: Date }
|
|
65
|
+
| { kind: "backorder"; newStatus: "backordered" };
|
|
66
|
+
|
|
67
|
+
function decideOrderStatus(
|
|
68
|
+
order: Order,
|
|
69
|
+
stock: StockReport,
|
|
70
|
+
now: Date,
|
|
71
|
+
): OrderDecision {
|
|
72
|
+
if (stock.allAvailable) {
|
|
73
|
+
return { kind: "confirm", newStatus: "confirmed", confirmedAt: now };
|
|
74
|
+
}
|
|
75
|
+
return { kind: "backorder", newStatus: "backordered" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Imperative shell — thin
|
|
79
|
+
async function processOrder(orderId: string) {
|
|
80
|
+
const order = await db.orders.get(orderId);
|
|
81
|
+
const stock = await api.checkStock(order.items);
|
|
82
|
+
const decision = decideOrderStatus(order, stock, new Date());
|
|
83
|
+
|
|
84
|
+
order.status = decision.newStatus;
|
|
85
|
+
if (decision.kind === "confirm") {
|
|
86
|
+
order.confirmedAt = decision.confirmedAt;
|
|
87
|
+
}
|
|
88
|
+
await db.orders.save(order);
|
|
89
|
+
|
|
90
|
+
if (decision.kind === "confirm") {
|
|
91
|
+
await emailer.send(order.userId, "confirmed");
|
|
92
|
+
}
|
|
93
|
+
return order;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Now `decideOrderStatus` is pure: trivially tested with literal inputs and outputs, no mocks. The shell is short enough to verify by reading.
|
|
98
|
+
|
|
99
|
+
## How to find your core
|
|
100
|
+
|
|
101
|
+
Ask: *"If I removed all the awaits and the side-effecting calls, what's left?"* That residue is the candidate for the core. Lift it out as a pure function. Then ask: *"What is each side-effect call doing — fetching input, or writing output?"* Group them at the top and bottom of the shell respectively.
|
|
102
|
+
|
|
103
|
+
A useful test: a pure core function should be safe to call ten thousand times in a tight loop with no consequences. If that's not safe, it's not pure yet.
|
|
104
|
+
|
|
105
|
+
## Pairings with other principles
|
|
106
|
+
|
|
107
|
+
- **With Principle 2 (testability):** the core is what your tests target. Mocks shrink from "everywhere" to "at the shell's edge only".
|
|
108
|
+
- **With Principle 3 (illegal states):** the core often returns sum types (Decision, Result, Outcome) that the shell pattern-matches on. Bad transitions become uncompileable.
|
|
109
|
+
- **With Principle 1 (deep modules):** the core is "deep" — many decisions hidden behind a single function call. The shell is "shallow" *by design* — its thinness is the point.
|
|
110
|
+
|
|
111
|
+
## Limits — where this gets harder
|
|
112
|
+
|
|
113
|
+
- **Streaming / long-running processes.** Purity is easier when inputs are bounded. For streams, lift transformations into pure operators (map, filter, fold) and keep the orchestration impure but small.
|
|
114
|
+
- **Performance-critical paths.** If creating intermediate values is expensive, you may push more state into the shell. Profile first; don't preemptively sacrifice clarity.
|
|
115
|
+
- **Logic that depends on intermediate API calls.** When the decision needs results from external calls partway through, the core/shell split moves *inside* each call boundary. The pattern still helps; the core just gets smaller chunks.
|
|
116
|
+
|
|
117
|
+
## The smell that points to this principle
|
|
118
|
+
|
|
119
|
+
If your function under test needs more mocks than there are lines of business logic, the logic is buried inside the shell. Lift it out.
|
|
120
|
+
|
|
121
|
+
If two test cases have nearly-identical setup but assert on different decisions, the decision is a pure function in disguise. Lift it out.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Make Illegal States Unrepresentable
|
|
2
|
+
|
|
3
|
+
The single highest-leverage application of the type system: prevent bad states at compile time so you don't have to defend against them at runtime.
|
|
4
|
+
|
|
5
|
+
The pattern: take a runtime invariant ("a verified user must have a verification timestamp") and encode it in the types. The bad state can no longer be constructed.
|
|
6
|
+
|
|
7
|
+
## Example 1 — Optional fields that should move together
|
|
8
|
+
|
|
9
|
+
WEAK
|
|
10
|
+
```ts
|
|
11
|
+
type User = {
|
|
12
|
+
email: string;
|
|
13
|
+
verifiedAt?: Date;
|
|
14
|
+
verificationToken?: string;
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This permits four combinations but only three are valid:
|
|
19
|
+
|
|
20
|
+
| State | Valid? |
|
|
21
|
+
|---|---|
|
|
22
|
+
| email + neither | ✓ unverified |
|
|
23
|
+
| email + token only | ✓ pending |
|
|
24
|
+
| email + verifiedAt only | ✓ verified |
|
|
25
|
+
| email + token + verifiedAt | ✗ incoherent |
|
|
26
|
+
|
|
27
|
+
STRONG
|
|
28
|
+
```ts
|
|
29
|
+
type User =
|
|
30
|
+
| { kind: "unverified"; email: string }
|
|
31
|
+
| { kind: "pending"; email: string; token: string }
|
|
32
|
+
| { kind: "verified"; email: string; verifiedAt: Date };
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each state names itself. Pattern matching is exhaustive. The fourth (incoherent) state cannot compile.
|
|
36
|
+
|
|
37
|
+
## Example 2 — Phantom types for pipeline stages
|
|
38
|
+
|
|
39
|
+
When the same data flows through stages (e.g. `RawInput` → `Validated` → `Sanitized`), encode the stage in the type:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
type Validated<T> = T & { _validated: true };
|
|
43
|
+
type Sanitized<T> = T & { _sanitized: true };
|
|
44
|
+
|
|
45
|
+
function validate(input: RawInput): Validated<RawInput> { ... }
|
|
46
|
+
function sanitize(input: Validated<RawInput>): Sanitized<RawInput> { ... }
|
|
47
|
+
function persist(input: Sanitized<RawInput>): void { ... }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`persist` cannot be called with raw input. The compiler enforces the order — no runtime guard needed.
|
|
51
|
+
|
|
52
|
+
## Example 3 — Non-empty collections
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// WEAK — runtime check forever, easy to forget
|
|
56
|
+
function firstUser(users: User[]): User {
|
|
57
|
+
if (users.length === 0) throw new Error("empty");
|
|
58
|
+
return users[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// STRONG — the type enforces non-emptiness
|
|
62
|
+
type NonEmpty<T> = [T, ...T[]];
|
|
63
|
+
function firstUser(users: NonEmpty<User>): User {
|
|
64
|
+
return users[0]; // type-safe, no check needed
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Callers can't pass an empty array — it doesn't satisfy the type.
|
|
69
|
+
|
|
70
|
+
## When the language can't fully encode
|
|
71
|
+
|
|
72
|
+
In languages without sum types (older Java, plain JS):
|
|
73
|
+
|
|
74
|
+
- **Use enum + private constructor + factory methods.** Each factory returns a value that's already in a valid state.
|
|
75
|
+
- **Use builders that only expose `build()` once required fields are set.** The build method's signature changes as fields are populated (in TypeScript, this is doable via fluent builders + conditional types).
|
|
76
|
+
- **Document the invariant in one place, validate at construction, never internally.** The closer construction lives to the type, the fewer scattered checks survive.
|
|
77
|
+
|
|
78
|
+
The principle still holds: the closer the invariant lives to the type system, the fewer bugs survive.
|
|
79
|
+
|
|
80
|
+
## Limits — when not to encode
|
|
81
|
+
|
|
82
|
+
- **Invariants that change frequently.** If the rule is in flux, encoding it ossifies it. Use runtime validation until the rule stabilises, then encode.
|
|
83
|
+
- **Invariants that span systems.** If the rule lives across multiple services, types in one service can't enforce it. Validate at the boundary.
|
|
84
|
+
- **Invariants the type system can't express.** *"An order's total equals the sum of its line items"* — most type systems can't encode this. Validate at construction; treat the constructor as a quarantine.
|
|
85
|
+
|
|
86
|
+
## The smell that points to this principle
|
|
87
|
+
|
|
88
|
+
If you find yourself writing comments like:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
// invariant: if status === "verified", verifiedAt must be set
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
That's the type system asking to be used. The comment will rot. The type won't.
|
|
95
|
+
|
|
96
|
+
If you write defensive checks like:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
if (!user.verifiedAt) throw new Error("user must be verified");
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
…and that check appears in more than one place, the type can carry that obligation instead. Lift the invariant.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Observability by Design
|
|
2
|
+
|
|
3
|
+
Principles for ensuring modules are transparent, debuggable, and "production-ready" at the architectural level.
|
|
4
|
+
|
|
5
|
+
## The Principle: Deep Observability
|
|
6
|
+
|
|
7
|
+
A **Deep Module** should hide its implementation complexity but **expose its operational health**. Observability is not a "sidecar" added later; it is a primary concern of the interface design.
|
|
8
|
+
|
|
9
|
+
## 1. The Telemetry Port
|
|
10
|
+
|
|
11
|
+
Every deep module should have a way to emit telemetry (metrics, logs, traces) without depending on a specific infrastructure provider.
|
|
12
|
+
|
|
13
|
+
- **The Port**: The module defines a `Telemetry` interface (or a set of "Probes") within its own package.
|
|
14
|
+
- **The Dependency**: Telemetry is a **required dependency** of the module, injected at instantiation.
|
|
15
|
+
- **The Adapter**: The Imperative Shell (infrastructure layer) implements the interface and injects the concrete provider (e.g., Datadog, Prometheus, or a structured logger).
|
|
16
|
+
- **The Benefit**: The core logic remains pure and testable; the operations team gets the data they need without the core knowing how it's collected.
|
|
17
|
+
|
|
18
|
+
## 2. No Silent Failures
|
|
19
|
+
|
|
20
|
+
If a module cannot satisfy its contract, it must fail loudly and descriptively.
|
|
21
|
+
|
|
22
|
+
- **Descriptive Errors**: Errors must name the failing operation and the specific input that caused it.
|
|
23
|
+
- **Contextual Wrapping**: As errors move from the core to the shell, wrap them with context (e.g., `"failed to process order: <reason>"`).
|
|
24
|
+
- **Internal Health Probes**: For long-lived modules, provide a `Health()` check that verifies internal invariants or critical dependencies.
|
|
25
|
+
|
|
26
|
+
## 3. The Traceable Path
|
|
27
|
+
|
|
28
|
+
In asynchronous or distributed flows, ensure the module preserves and propagates **Correlation IDs**.
|
|
29
|
+
|
|
30
|
+
- Every entry point should accept a context/correlation carrier.
|
|
31
|
+
- Every internal log line should include the ID.
|
|
32
|
+
- This allows a single user request to be traced through multiple deep modules.
|
|
33
|
+
|
|
34
|
+
## 4. Performance Transparency
|
|
35
|
+
|
|
36
|
+
Expose the "Boring" metrics that matter:
|
|
37
|
+
- **Latency**: How long the deep implementation takes.
|
|
38
|
+
- **Throughput**: How many requests are being handled.
|
|
39
|
+
- **Error Rate**: Percentage of calls that return a failure.
|
|
40
|
+
- **Saturation**: How close the module is to its internal limits (e.g., buffer sizes, connection pools).
|
|
41
|
+
|
|
42
|
+
## Integration with Simplify
|
|
43
|
+
|
|
44
|
+
During the `simplify` pass, apply the **Telemetry Lens**:
|
|
45
|
+
|
|
46
|
+
- [ ] Does every error message name the failing input?
|
|
47
|
+
- [ ] Are there any "catch-all" blocks that swallow errors?
|
|
48
|
+
- [ ] Is there a Correlation ID being propagated if the flow is non-trivial?
|
|
49
|
+
- [ ] Could a stranger debug a failure in this code using *only* the logs it emits?
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Design Personas
|
|
2
|
+
|
|
3
|
+
When designing a new system or deepening a module, use these parallel sub-agent personas to explore the design space. Based on the principle of "Design It Twice" — your first idea is rarely your best.
|
|
4
|
+
|
|
5
|
+
These personas are used by `system-design`, `improve-codebase-architecture`, and `investigate`.
|
|
6
|
+
|
|
7
|
+
## The Personas
|
|
8
|
+
|
|
9
|
+
### 1. The Minimalist
|
|
10
|
+
- **Goal**: Minimize the interface surface area.
|
|
11
|
+
- **Strategy**: 1–3 entry points max. Hide everything else. If a feature can be accomplished by combining existing primitives, don't add a new one.
|
|
12
|
+
- **Metric**: High **Leverage** (Functionality / Interface size).
|
|
13
|
+
|
|
14
|
+
### 2. The Extensible (The Architect)
|
|
15
|
+
- **Goal**: Support many use cases and future growth.
|
|
16
|
+
- **Strategy**: Use hooks, providers, or plugin-style interfaces. Focus on the "Seam" where behavior can be altered without editing the module.
|
|
17
|
+
- **Metric**: High **Flexibility** (Ease of change without breaking the contract).
|
|
18
|
+
|
|
19
|
+
### 3. The Ergonomic (The Developer Advocate)
|
|
20
|
+
- **Goal**: Make the most common caller's life trivial.
|
|
21
|
+
- **Strategy**: Design for the "Happy Path." Provide high-level defaults and "Convention over Configuration."
|
|
22
|
+
- **Metric**: Low **Cognitive Load** (Time-to-first-successful-call).
|
|
23
|
+
|
|
24
|
+
### 4. The Hardened (The Security/Robustness Expert)
|
|
25
|
+
- **Goal**: Prevent abuse and ensure reliability.
|
|
26
|
+
- **Strategy**: Focus on "Illegal States Unrepresentable" and strict trust boundaries. Explicit timeouts, retries, and validation at every entry point.
|
|
27
|
+
- **Metric**: High **Resilience** (Failure-resistance and security posture).
|
|
28
|
+
|
|
29
|
+
### 5. The Observability-First (The SRE)
|
|
30
|
+
- **Goal**: Ensure the module's internal state is transparent.
|
|
31
|
+
- **Strategy**: Design with built-in telemetry, probes, and structured error propagation. No "Silent Failures."
|
|
32
|
+
- **Metric**: High **Debuggability** (Time-to-root-cause during incidents).
|
|
33
|
+
|
|
34
|
+
## Usage Pattern
|
|
35
|
+
|
|
36
|
+
When exploring an interface:
|
|
37
|
+
|
|
38
|
+
1. **Frame the problem space**: State the constraints and dependency categories.
|
|
39
|
+
2. **Dispatch Personas**: Assign 2-3 of these personas to separate "Parallel Brains" (or sub-agent invocations).
|
|
40
|
+
3. **Compare**: Contrast the results by **Depth**, **Locality**, and **Seam placement**.
|
|
41
|
+
4. **Hybridize**: Pick the strongest elements to form the final recommendation.
|