total-dumb 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -40
- package/bin/cli.mjs +39 -25
- package/package.json +39 -7
- package/skills/dumb/SKILL.md +47 -48
- package/skills/dumb/references/context.md +1 -1
- package/skills/dumb/references/document.md +154 -0
- package/skills/dumb/references/levels.md +79 -46
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/OyakSaile/
|
|
2
|
+
<img src="https://raw.githubusercontent.com/OyakSaile/dumb/main/assets/banner.png" alt="total-dumb — the /dumb agent skill explains what your AI is doing right now and why" width="860">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<img src="https://img.shields.io/badge/npm-total--dumb-CB3837?logo=npm&logoColor=white" alt="npm package total-dumb">
|
|
7
7
|
<img src="https://img.shields.io/badge/node-%E2%89%A5%2018-5FA04E?logo=node.js&logoColor=white" alt="Node 18 or newer">
|
|
8
|
-
<img src="https://img.shields.io/badge/dependencies-0-4FD1C5" alt="zero dependencies">
|
|
9
8
|
<img src="https://img.shields.io/badge/license-MIT-A78BFA" alt="MIT license">
|
|
10
9
|
</p>
|
|
11
10
|
|
|
@@ -17,44 +16,42 @@
|
|
|
17
16
|
|
|
18
17
|
Coding agents are very good at doing the work and very bad at telling you why they are doing it. You watch a migration appear, a job get scheduled, a column get indexed, and the honest answer to "why this, now?" is somewhere in a story file you have not opened.
|
|
19
18
|
|
|
20
|
-
`/dumb` is an [Agent Skill](https://agentskills.io) that answers that question from where you actually are. It reads the story, its epic and the code around the change, then
|
|
19
|
+
`/dumb` is an [Agent Skill](https://agentskills.io) that answers that question from where you actually are. It reads the story, its epic and the code around the change, then explains in short sentences, one idea at a time, and offers to go deeper only if you want it. In your language.
|
|
21
20
|
|
|
22
|
-
It works in **Claude Code, Cursor, Codex and OpenCode**, installs with one command
|
|
21
|
+
It works in **Claude Code, Cursor, Codex and OpenCode**, and installs with one command through a small interactive installer.
|
|
23
22
|
|
|
24
23
|
## What it looks like
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
`/dumb` writes a full explainer into your repo, then keeps the chat to a few lines and a path. Depth belongs in a document you can scroll, not in a wall of chat text.
|
|
27
26
|
|
|
27
|
+
Excerpt from `docs/dumb/2026-09-22-retry-webhook-backoff.md`. The real file also carries the system design, every other part, common mistakes, a self-check and a glossary.
|
|
28
|
+
|
|
29
|
+
```mermaid
|
|
30
|
+
flowchart LR
|
|
31
|
+
PSP["Payment provider"] -->|"payment.confirmed webhook"| Handler["handlePaymentWebhook"]
|
|
32
|
+
Handler -->|"the payload"| Order["processOrder"]
|
|
33
|
+
Handler -->|"row written on failure"| Retries[("webhook_retries")]
|
|
34
|
+
Retries -->|"rows already due"| Job["Retry job, every minute"]
|
|
35
|
+
Job -->|"same payload, same key"| Order
|
|
28
36
|
```
|
|
29
|
-
🧠 What we're doing
|
|
30
|
-
We wrote the `webhook_retries` migration for story 5.6: the table holding every
|
|
31
|
-
failed payment webhook and when to retry it.
|
|
32
|
-
|
|
33
|
-
🎯 Why (the bigger picture)
|
|
34
|
-
5.6 belongs to Epic 5 (Payments): "a confirmed payment becomes a paid order,
|
|
35
|
-
always". 5.3 (webhook) and 5.4 (idempotency) guarantee the confirmation arrives
|
|
36
|
-
and is not processed twice, but not that it arrives at all if the order processor
|
|
37
|
-
is down that second. Without retry a real payment vanishes, which is why 5.7
|
|
38
|
-
(status screen) is blocked: you cannot show "paid" for something that can
|
|
39
|
-
disappear. AC 2 fixes the shape: 1m → 5m → 25m → 2h, 5 attempts.
|
|
40
|
-
|
|
41
|
-
🪄 Analogy
|
|
42
|
-
A tracked parcel: if nobody answers, the carrier does not bin it; they leave a
|
|
43
|
-
notice and come back at longer and longer intervals. After 5 attempts it goes to
|
|
44
|
-
the depot (dead letter) and someone collects it.
|
|
45
|
-
|
|
46
|
-
👀 Senior's eye
|
|
47
|
-
- `next_attempt_at` needs an index: the job asks "what is due?" every minute.
|
|
48
|
-
- Retry without 5.4's idempotency = double payment; reprocessing goes down the
|
|
49
|
-
same path.
|
|
50
|
-
|
|
51
|
-
🤔 One question
|
|
52
|
-
Why growing intervals instead of retrying every minute?
|
|
53
|
-
|
|
54
|
-
➡️ Next step: the job that reads `next_attempt_at <= now()`.
|
|
55
|
-
```
|
|
56
37
|
|
|
57
|
-
|
|
38
|
+
#### Why this exists
|
|
39
|
+
|
|
40
|
+
Epic 5 promises that a confirmed payment always becomes a paid order, because a customer whose money left their account and whose order still says "pending" will charge back and stop trusting the shop. Today the provider tells us once, and `handlePaymentWebhook` processes it immediately. If the order service is down for that one second, the message is gone and nobody retries, so the payment is lost silently. Story 5.4 already made reprocessing safe to repeat, which is what makes an automatic retry possible at all.
|
|
41
|
+
|
|
42
|
+
#### handlePaymentWebhook
|
|
43
|
+
|
|
44
|
+
**In general** — A webhook is one system calling another to announce that something happened, so the receiver never has to ask. The usual alternative is polling, where you ask "is it paid yet?" on a timer, which wastes calls and still adds delay.
|
|
45
|
+
|
|
46
|
+
**In this project** — The exported function in `src/payments/webhook.ts`. The provider posts the confirmation to it, and it calls `processOrder` straight away.
|
|
47
|
+
|
|
48
|
+
**How it connects** — The provider feeds it; it feeds `processOrder`. After this story it also feeds `webhook_retries` whenever `processOrder` throws, and the retry job picks up from there.
|
|
49
|
+
|
|
50
|
+
**❌ Doing it wrong** — Catching the error and returning 200 without storing anything. The provider sees success and never sends the message again, so the payment is lost with no trace to debug.
|
|
51
|
+
|
|
52
|
+
**✅ Doing it right** — Write the payload to `webhook_retries` before returning, because that row is the only evidence the payment arrived, and the retry job has nothing to work from without it.
|
|
53
|
+
|
|
54
|
+
Every rule, number and design choice in that file carries its reason. "AC 2 defines 1m, 5m, 25m, 2h" is a failure; the reason those intervals grow is the point.
|
|
58
55
|
|
|
59
56
|
## Install
|
|
60
57
|
|
|
@@ -75,20 +72,33 @@ npx total-dumb --dry-run # show what would happen, write nothin
|
|
|
75
72
|
Also available through the `skills` CLI:
|
|
76
73
|
|
|
77
74
|
```bash
|
|
78
|
-
npx skills add OyakSaile/
|
|
75
|
+
npx skills add OyakSaile/dumb
|
|
79
76
|
```
|
|
80
77
|
|
|
81
|
-
##
|
|
78
|
+
## Four levels
|
|
79
|
+
|
|
80
|
+
Every level except `terms` writes the document. The level sets how deep the document goes and how much the chat asks you.
|
|
82
81
|
|
|
83
82
|
| You type | You get |
|
|
84
83
|
|---|---|
|
|
85
|
-
| `/dumb` | the
|
|
86
|
-
| `/dumb zero` |
|
|
87
|
-
| `/dumb
|
|
84
|
+
| `/dumb` | the full document, then a few lines in chat and one line offering to explain a term or move on |
|
|
85
|
+
| `/dumb zero` | the full document with a wider glossary and an everyday analogy per part; the chat stops and asks which term to explain, plus one question to check you followed |
|
|
86
|
+
| `/dumb senior` | the document without glossary or analogies, with the trade-offs and the alternatives that were rejected; the chat asks nothing |
|
|
87
|
+
| `/dumb terms` | no file, just the vocabulary of this step in chat, each term with an example from your task |
|
|
88
|
+
|
|
89
|
+
Aliases: `eli5`, `beginner` and `junior` for `zero`; `pro` and `expert` for `senior`; `termos`, `jargon` and `glossary` for `terms`.
|
|
90
|
+
|
|
91
|
+
With no level given it reads your message. Use the step's terms correctly, or ask a sharp trade-off question, and you get the `senior` treatment. Say "I'm lost" and you get `dev`. It will not offer to define a word you just used correctly.
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
You do not have to use the slash command. "why are we doing this?", "what is this for?", "I don't get it", "explain this step" all trigger it mid-task. Ask in any language and both the document and the reply come back in it, headers included.
|
|
94
|
+
|
|
95
|
+
## What lands in your repo
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
docs/dumb/2026-09-22-retry-webhook-backoff.md
|
|
99
|
+
```
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
One file per feature, rewritten when you ask again. Inside: why it exists, the big picture as a labelled Mermaid flow, the system design with the current feature highlighted and the planned connections dotted, a sequence diagram of the main path including the failure branch, then every part with what it is in general, what it is here, what it connects to, a worked example, and a wrong-way and right-way pair. It closes with the common mistakes on this feature, three questions with collapsed answers, and a glossary.
|
|
92
102
|
|
|
93
103
|
## Where the "why" comes from
|
|
94
104
|
|
package/bin/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { sep } from "node:path";
|
|
5
5
|
import { stdin, stdout, stderr, exit } from "node:process";
|
|
6
|
-
import
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
7
|
import { AGENTS, AGENT_IDS } from "../src/agents.mjs";
|
|
8
8
|
import { parseArgs, USAGE } from "../src/args.mjs";
|
|
9
9
|
import { detectAgents, install, normalizeAgents, resolveTargets, uninstall } from "../src/install.mjs";
|
|
@@ -43,12 +43,38 @@ let scope = opts.scope ?? "global";
|
|
|
43
43
|
|
|
44
44
|
const interactive = Boolean(stdin.isTTY) && !opts.yes;
|
|
45
45
|
if (interactive) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
p.intro(`total-dumb ${pkg.version} — /dumb explains what we're doing and why`);
|
|
47
|
+
if (detected.length) {
|
|
48
|
+
p.log.info(`Detected: ${detected.map((id) => AGENTS[id].displayName).join(", ")}`);
|
|
49
|
+
} else {
|
|
50
|
+
p.log.warn("No agents detected on this machine. Pick where to install anyway.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!opts.agents) {
|
|
54
|
+
const picked = await p.multiselect({
|
|
55
|
+
message: "Which agents?",
|
|
56
|
+
options: AGENT_IDS.map((id) => ({
|
|
57
|
+
value: id,
|
|
58
|
+
label: AGENTS[id].displayName,
|
|
59
|
+
hint: detected.includes(id) ? "detected" : undefined,
|
|
60
|
+
})),
|
|
61
|
+
initialValues: detected.length ? detected : AGENT_IDS,
|
|
62
|
+
required: true,
|
|
63
|
+
});
|
|
64
|
+
if (p.isCancel(picked)) { p.cancel("Nothing installed."); exit(0); }
|
|
65
|
+
agents = AGENT_IDS.filter((id) => picked.includes(id));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!opts.scope) {
|
|
69
|
+
const where = await p.select({
|
|
70
|
+
message: opts.uninstall ? "Remove from where?" : "Where?",
|
|
71
|
+
options: [
|
|
72
|
+
{ value: "global", label: "Global — this machine", hint: "~/.claude/skills, ~/.cursor/skills, ~/.codex/skills, ~/.config/opencode/skills" },
|
|
73
|
+
{ value: "project", label: "Project — this repo", hint: ".claude/skills, .agents/skills" },
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
if (p.isCancel(where)) { p.cancel("Nothing installed."); exit(0); }
|
|
77
|
+
scope = where;
|
|
52
78
|
}
|
|
53
79
|
}
|
|
54
80
|
|
|
@@ -62,36 +88,24 @@ if (agents.length === 0) {
|
|
|
62
88
|
}
|
|
63
89
|
|
|
64
90
|
const targets = resolveTargets({ agents, scope });
|
|
91
|
+
const spinner = interactive ? p.spinner() : null;
|
|
92
|
+
spinner?.start(opts.uninstall ? "Removing…" : "Installing…");
|
|
65
93
|
let results;
|
|
66
94
|
try {
|
|
67
95
|
results = opts.uninstall
|
|
68
96
|
? await uninstall(targets, { dryRun: opts.dryRun })
|
|
69
97
|
: await install(targets, { dryRun: opts.dryRun });
|
|
70
98
|
} catch (error) {
|
|
99
|
+
spinner?.stop("Failed", 1);
|
|
71
100
|
stderr.write(`${error.message}\n`);
|
|
72
101
|
exit(1);
|
|
73
102
|
}
|
|
103
|
+
spinner?.stop(opts.uninstall ? "Removed" : "Installed");
|
|
74
104
|
|
|
75
105
|
printSummary(results);
|
|
106
|
+
if (interactive) p.outro(results.some((r) => r.status === "failed") ? "Some targets failed." : "Done.");
|
|
76
107
|
exit(results.some((r) => r.status === "failed") ? 1 : 0);
|
|
77
108
|
|
|
78
|
-
async function askAgents(rl, detected) {
|
|
79
|
-
stdout.write("\nWhich agents?\n");
|
|
80
|
-
AGENT_IDS.forEach((id, i) => {
|
|
81
|
-
stdout.write(` ${i + 1}) ${AGENTS[id].displayName}${detected.includes(id) ? " (detected)" : ""}\n`);
|
|
82
|
-
});
|
|
83
|
-
const fallback = detected.length ? detected : AGENT_IDS;
|
|
84
|
-
const suggested = fallback.map((id) => AGENT_IDS.indexOf(id) + 1).join(",");
|
|
85
|
-
const answer = await rl.question(`Numbers separated by commas [${suggested}]: `);
|
|
86
|
-
const picked = answer.split(",").map((n) => AGENT_IDS[Number(n.trim()) - 1]).filter(Boolean);
|
|
87
|
-
return picked.length ? AGENT_IDS.filter((id) => picked.includes(id)) : fallback;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function askScope(rl) {
|
|
91
|
-
const answer = await rl.question("Where? (g)lobal for this machine, (p)roject for this repo [g]: ");
|
|
92
|
-
return answer.trim().toLowerCase().startsWith("p") ? "project" : "global";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
109
|
function printSummary(results) {
|
|
96
110
|
const verb = opts.uninstall ? "Removed" : "Installed";
|
|
97
111
|
stdout.write(`\n${opts.dryRun ? "[dry-run] " : ""}${verb} the "dumb" skill:\n`);
|
|
@@ -102,5 +116,5 @@ function printSummary(results) {
|
|
|
102
116
|
stdout.write(` ${icon} ${names.padEnd(26)} ${where} ${r.status}${r.error ? ` (${r.error})` : ""}\n`);
|
|
103
117
|
}
|
|
104
118
|
const didWrite = results.some((r) => r.status === "installed" || r.status === "updated");
|
|
105
|
-
if (!opts.uninstall && !opts.dryRun && didWrite) stdout.write("\nMid-task, type: /dumb /dumb zero /dumb
|
|
119
|
+
if (!opts.uninstall && !opts.dryRun && didWrite) stdout.write("\nMid-task, type: /dumb /dumb zero /dumb senior\n");
|
|
106
120
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-dumb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agent skill that explains what your AI is doing right now and why, with an analogy. One npx install for Claude Code, Cursor, Codex and OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"total-dumb": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"skills",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test test/*.test.mjs"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"agent-skills",
|
|
23
|
+
"skill",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"cursor",
|
|
26
|
+
"codex",
|
|
27
|
+
"opencode",
|
|
28
|
+
"bmad",
|
|
29
|
+
"mentor",
|
|
30
|
+
"learn"
|
|
31
|
+
],
|
|
11
32
|
"author": "Kayo Elias",
|
|
12
|
-
"license": "MIT"
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/OyakSaile/dumb.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/OyakSaile/dumb#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/OyakSaile/dumb/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^1.8.1"
|
|
44
|
+
}
|
|
13
45
|
}
|
package/skills/dumb/SKILL.md
CHANGED
|
@@ -1,83 +1,82 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dumb
|
|
3
|
-
description: Use when the user invokes /dumb or, in the middle of any task (a BMAD story, a refactor, a bug fix, a migration, a config change), asks why the current step exists or what it is for — "why are we doing this?", "what is this for?", "I don't get it", "explain this step", "não entendi", "por que isso?", "explica". Optional level after the name — dev (default), zero, terms.
|
|
3
|
+
description: Use when the user invokes /dumb or, in the middle of any task (a BMAD story, a refactor, a bug fix, a migration, a config change), asks why the current step exists or what it is for — "why are we doing this?", "what is this for?", "I don't get it", "explain this step", "não entendi", "por que isso?", "explica". Optional level after the name — dev (default), zero, senior, terms.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# dumb — explain the current step and its why
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
An agent is shipping features faster than anyone can read them, and the system design disappears into the diff. This skill exists so the person actually learns it. Write a structured Markdown explainer into the project, then keep the chat short and let the user ask for more.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**Everything has a why.** Every rule, number, acceptance criterion, pattern and design choice you mention carries its reason, in the same sentence or the next. Never "AC 2 defines 1m → 5m → 25m → 2h, 5 attempts". Instead: "AC 2 spaces the retries as 1m → 5m → 25m → 2h because the first one catches a short blip cheaply, the later ones stop hammering a service that is genuinely down, and 5 attempts cap the wait near three hours before a human has to look." If the real reason is not written down anywhere, say so and give the most likely one, labelled as a guess.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
**Specific beats correct-but-generic.** Every sentence names something real: the story, the file, the table, the queue.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
**One idea at a time.** A reply the user has to decode has failed, however accurate it is.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|---|---|---|
|
|
20
|
-
| `dev` (default) | — | a developer who knows the basics and wants the why |
|
|
21
|
-
| `zero` | `eli5`, `beginner` | someone who has never seen anything like this |
|
|
22
|
-
| `terms` | `termos`, `jargon`, `glossary` | wants the vocabulary of this step |
|
|
18
|
+
## Simplicity rules — everywhere
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
What to pull out of each source is in [references/context.md](references/context.md).
|
|
30
|
-
Nothing found? The answer opens with one line saying so, before the first section header ("No planning docs found; explaining from the code", in the user's language), then reasons from the code.
|
|
20
|
+
- Short sentences, one idea each.
|
|
21
|
+
- At most one new technical term per sentence.
|
|
22
|
+
- No `term (meaning)` stacks. Definitions belong in the Glossary.
|
|
23
|
+
- No chains of story IDs. One story back, one story forward, in plain words.
|
|
24
|
+
- Analogies only where they help: everyday, 2 sentences at most, mapping one thing.
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
## Steps
|
|
33
27
|
|
|
34
|
-
|
|
28
|
+
1. **Pick the level and the language.** An explicit level after `dumb` wins; otherwise use step 2. The language is the one the user writes in this conversation; when the invocation is too short to tell (`/dumb terms`), take it from the surrounding conversation and the project's docs. Every word of the document and the reply is in that language, headers included.
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
| Level | Aliases | Document | Chat ends with |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| `dev` (default) | — | full | "read it first"; after the user has read it, one offer line |
|
|
33
|
+
| `zero` | `eli5`, `beginner`, `junior` | full, bigger Glossary, an analogy per part | "read it first"; after the user has read it, the check block, then stop |
|
|
34
|
+
| `senior` | `pro`, `expert` | no Glossary, no analogies, trade-offs in the why | nothing |
|
|
35
|
+
| `terms` | `termos`, `jargon`, `glossary` | none, chat only | nothing |
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
🧠 What we're doing
|
|
40
|
-
<1–2 sentences. Names the story / file / function / command being touched right now.>
|
|
37
|
+
2. **Adaptive default.** With no level given, read the user's message. It uses the step's technical terms correctly, or asks a precise trade-off question → `senior`. "não entendi", "I'm lost", "o que é isso", or a bare "why?" → `dev`. Never offer to define a term the user just used correctly.
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
<At most 4 sentences: the goal this serves; where it sits (epic, roadmap,
|
|
44
|
-
dependency chain); what breaks or gets harder without it; why now and not later.>
|
|
39
|
+
3. **Find the context, then the parts.** Search order and what to extract are in [references/context.md](references/context.md). Then list the parts this feature touches, by their real names: components, services, queues, workers, agents, tables, endpoints, jobs. Take them from the story or spec and from the code, never from imagination.
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
<2–3 sentences. One real-world analogy, each part mapped to one part of this step.>
|
|
41
|
+
4. **Write the document.** If `docs/dumb/*-<feature-slug>.md` already exists, overwrite that file and keep its name, so re-running `/dumb` on the same feature updates one document instead of adding another; otherwise create `docs/dumb/<yyyy-mm-dd>-<feature-slug>.md`, creating the folder if needed. The template, the diagram rules and a worked excerpt are in [references/document.md](references/document.md). Skip this step at `terms`.
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
<2 bullets, one sentence each: trade-offs or pitfalls a senior would watch for right here.>
|
|
43
|
+
5. **Reply in chat**, at most 100 words plus the path. Shapes and dialogue rules are in [references/levels.md](references/levels.md).
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
<One question the user can answer to check they got it. Omit if it would be forced.>
|
|
45
|
+
## The chat reply
|
|
54
46
|
|
|
55
|
-
➡️ Next step: <one line>
|
|
56
47
|
```
|
|
48
|
+
<3–5 short lines: what we're doing and why it matters, plain words.>
|
|
49
|
+
|
|
50
|
+
📄 <path to the file that was written>
|
|
51
|
+
Read the whole document first. When you are done, say so and we go on from there.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
At `senior` the last line is omitted. The document carries the depth. The chat carries the invitation. Never paste the document into the chat, and never ask a question or offer terms in this first reply: the user has not read the document yet.
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
## Dialogue
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
Questions come only after the user says they have read the document ("li", "read it", "done"). At that point the level's ending applies: `dev` sends the single offer line; `zero` sends the check block (terms to pick plus one comprehension question) and stops, even when the same message also says to continue, because at `zero` the check is the gate back to the task; `senior` continues the task with no questions. When the user picks a term, answer in chat, about 80 words: one sentence of meaning, then how it works in this project naming a real file or function, then one tiny example. A wrong answer to the check question gets a different and simpler explanation, then one new question. "entendi" / "got it" closes with `➡️ Next step` and nothing else. Never offer a term the user already knows or already had explained, and stop asking the moment they turn back to the task.
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
- which earlier stories it builds on;
|
|
64
|
-
- which later stories are blocked by it;
|
|
65
|
-
- the acceptance criterion that carries the motivation.
|
|
60
|
+
## When the task is a BMAD story
|
|
66
61
|
|
|
67
|
-
|
|
62
|
+
The "why" says which epic the story serves and what that epic delivers, what the most relevant earlier story already built, and the one later story that needs this one. Plain words, never ID arithmetic, and every one of them carries its reason.
|
|
68
63
|
|
|
69
64
|
## Honesty
|
|
70
65
|
|
|
71
|
-
If the motivation is weak, unclear, or the step looks unnecessary,
|
|
66
|
+
If the motivation is weak, unclear, or the step looks unnecessary, say so. Learning to judge work is part of becoming a better developer.
|
|
72
67
|
|
|
73
68
|
## Common mistakes
|
|
74
69
|
|
|
75
70
|
| Mistake | Fix |
|
|
76
71
|
|---|---|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
72
|
+
| Stating a rule, number or acceptance criterion with no reason | Every one carries its why in the same sentence or the next |
|
|
73
|
+
| Inventing a why that is not in the docs or the code | Say the reason is not recorded, then give the likely one and label it a guess |
|
|
74
|
+
| Pasting the document into the chat | The chat is 3–5 lines plus the path |
|
|
75
|
+
| A part described only as it exists here | Three layers: in general with the usual alternative, in this project, how it connects |
|
|
76
|
+
| Stacked definitions, three `term (meaning)` pairs in a row | Name the term and move on; the Glossary defines it |
|
|
77
|
+
| A chain of story IDs | One story back, one story forward, in plain words |
|
|
78
|
+
| Asking a question or offering terms before the user has read the document | The first reply ends at "read the whole document first"; questions come after they say they read it |
|
|
79
|
+
| Explaining the offered terms at `zero` before the user asks | The check block offers; the user picks; then explain |
|
|
80
|
+
| Quizzing a user who already said they get it, or who asked to continue after the check | Stop asking the moment they turn back to the task; at `zero`, "I read it" still gets the check block once, even with "continue" in the same message |
|
|
81
|
+
| Offering to define a term the user just used correctly | That user is `senior`; give trade-offs instead |
|
|
82
|
+
| Replying in English to a user who wrote in another language | Language comes from the conversation and the project's docs |
|
|
@@ -7,7 +7,7 @@ Read at most 3 files. Stop when every template slot can be filled with something
|
|
|
7
7
|
| Source | Extract |
|
|
8
8
|
|---|---|
|
|
9
9
|
| The conversation | the task in flight; the file / command just touched; what the user already knows |
|
|
10
|
-
| BMAD story file (`docs/stories/<n>.<m>.story.md`, `docs/stories/*<n>.<m>*`) | the "so that" clause;
|
|
10
|
+
| BMAD story file (`docs/stories/<n>.<m>.story.md`, `docs/stories/*<n>.<m>*`) | the "so that" clause; the single most relevant story it depends on and the single story it unblocks, to be named in plain words rather than as an ID chain; the acceptance criterion behind the current task; which task is checked and which is next |
|
|
11
11
|
| BMAD epic (`docs/epics/epic-<n>*.md`, `docs/epic-<n>*.md`, `docs/prd*.md` section) | the epic's goal in one sentence; the story list and its order (what comes before and after) |
|
|
12
12
|
| PRD (`docs/prd*.md`) | the product goal the epic serves |
|
|
13
13
|
| Architecture doc (`docs/architecture*.md`) | the technical design or constraint this step has to respect |
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# The explainer document
|
|
2
|
+
|
|
3
|
+
Write it to `docs/dumb/<yyyy-mm-dd>-<feature-slug>.md`. Create `docs/dumb/` if it does not exist. If a file matching `docs/dumb/*-<feature-slug>.md` is already there, overwrite that file and keep its name (one document per feature, updated on every run). No file is written at `terms`.
|
|
4
|
+
|
|
5
|
+
There is no length limit. The document is as long as the parts need: never shorten a part, drop a layer or skip an example to save space. A six-part feature can run past 2,000 words and that is fine. Every section is grounded in this project, with real names. The simplicity rules still apply sentence by sentence, and every rule, number and design choice carries its reason.
|
|
6
|
+
|
|
7
|
+
Headers below are shown in English, the canonical form; render them in the user's language.
|
|
8
|
+
|
|
9
|
+
## Template
|
|
10
|
+
|
|
11
|
+
````markdown
|
|
12
|
+
# <Feature or story name> — explained
|
|
13
|
+
|
|
14
|
+
## Why this exists
|
|
15
|
+
<4–8 sentences. The goal in plain words. What breaks or gets worse without it.
|
|
16
|
+
Who benefits. Every claim carries its reason. No chains of story IDs.
|
|
17
|
+
At `senior`, also the trade-offs and the alternatives that were considered.>
|
|
18
|
+
|
|
19
|
+
## The big picture
|
|
20
|
+
<2–3 sentences naming the flow, then the diagram. The diagram never replaces them.>
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
flowchart LR
|
|
24
|
+
A["<part>"] -->|"<what flows>"| B["<part>"]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`<part> → <part> → <part>`
|
|
28
|
+
|
|
29
|
+
<Then the main walk-through as a sequence diagram, including the failure branch.>
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
sequenceDiagram
|
|
33
|
+
participant A as <part>
|
|
34
|
+
participant B as <part>
|
|
35
|
+
A->>B: <what is sent>
|
|
36
|
+
alt <the thing that can fail> fails
|
|
37
|
+
B->>A: <what happens instead>
|
|
38
|
+
else it succeeds
|
|
39
|
+
B-->>A: <the happy result>
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## System design
|
|
44
|
+
<The components that exist today, how they connect, where this feature plugs in,
|
|
45
|
+
and what is planned next. If no architecture doc exists, derive it from the code
|
|
46
|
+
and say that is what you did.>
|
|
47
|
+
|
|
48
|
+
```mermaid
|
|
49
|
+
flowchart TD
|
|
50
|
+
A["<component>"] -->|"<what flows>"| B["<component>"]
|
|
51
|
+
C["<future component>"] -.->|"planned in story <n>"| B
|
|
52
|
+
classDef current fill:#fff3bf,stroke:#f08c00
|
|
53
|
+
class B current
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`<component> → <component> → <component>`
|
|
57
|
+
|
|
58
|
+
## The parts, one by one
|
|
59
|
+
|
|
60
|
+
### <Part, by its real name>
|
|
61
|
+
**In general** — <1–2 plain sentences: what this kind of thing is and how it works
|
|
62
|
+
anywhere, with the usual alternative for contrast, such as a webhook against polling,
|
|
63
|
+
a queue against a direct call, a worker against doing the work inside the request.
|
|
64
|
+
At `zero`, one everyday analogy here.>
|
|
65
|
+
**In this project** — <file, function, config, real names, and how a message actually
|
|
66
|
+
flows through it here.>
|
|
67
|
+
**How it connects** — <what feeds it and what it feeds, today, and what is planned next.>
|
|
68
|
+
**Example** — <one concrete walk-through with real values.>
|
|
69
|
+
**❌ Doing it wrong** — <a realistic mistake on this part and what breaks because of it.>
|
|
70
|
+
**✅ Doing it right** — <the correct approach and why it works.>
|
|
71
|
+
|
|
72
|
+
## Common mistakes on this feature
|
|
73
|
+
- <mistake> → <the symptom you would see> → <the fix>
|
|
74
|
+
<3 to 5 of these.>
|
|
75
|
+
|
|
76
|
+
## Check yourself
|
|
77
|
+
1. <question>
|
|
78
|
+
2. <question>
|
|
79
|
+
3. <question>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary>Answers</summary>
|
|
83
|
+
|
|
84
|
+
1. <answer>
|
|
85
|
+
2. <answer>
|
|
86
|
+
3. <answer>
|
|
87
|
+
|
|
88
|
+
</details>
|
|
89
|
+
|
|
90
|
+
## Glossary
|
|
91
|
+
- **<term>** — <one plain line>. <For a component, the three layers in one line each.>
|
|
92
|
+
|
|
93
|
+
<Omitted entirely at `senior`.>
|
|
94
|
+
|
|
95
|
+
## Next step
|
|
96
|
+
<One line: where the task resumes.>
|
|
97
|
+
````
|
|
98
|
+
|
|
99
|
+
## Diagram rules
|
|
100
|
+
|
|
101
|
+
- At most 10 nodes per diagram. If it needs more, the diagram is at the wrong altitude.
|
|
102
|
+
- Every edge in the big-picture diagram is labelled with **what flows**, not with a verb alone.
|
|
103
|
+
- Use the pipe form for labels, `A -->|"text"| B`, and quote every label. Dotted edges for planned connections, `A -.->|"planned in story 5.8"| B`.
|
|
104
|
+
- Node labels use plain words the reader has already met. A diagram never introduces a new term.
|
|
105
|
+
- One ASCII fallback line goes directly under each `flowchart` block, for readers whose viewer has no Mermaid.
|
|
106
|
+
- A diagram never replaces the sentence that explains it.
|
|
107
|
+
- Style the current feature's parts in the system-design diagram with `classDef current fill:#fff3bf,stroke:#f08c00` and a matching `class` line.
|
|
108
|
+
- Render labels in the user's language.
|
|
109
|
+
- Keep the syntax conservative and re-read every block before finishing. Nothing renders it here, so a typo ships.
|
|
110
|
+
- Use two small `flowchart` blocks for ❌ against ✅ only when the difference is structural, such as a retry loop with and without an idempotency key. Otherwise prose is better.
|
|
111
|
+
|
|
112
|
+
## By level
|
|
113
|
+
|
|
114
|
+
| Level | Glossary | Analogies | "Why this exists" | ❌/✅ examples |
|
|
115
|
+
|---|---|---|---|---|
|
|
116
|
+
| `dev` | yes | where they help | goal and consequences | realistic implementation mistakes |
|
|
117
|
+
| `zero` | expanded, every term | one per part, in "In general" | goal and consequences, extra plain | beginner mistakes |
|
|
118
|
+
| `senior` | none | none | plus trade-offs and alternatives considered | design choices, such as at-least-once against exactly-once |
|
|
119
|
+
|
|
120
|
+
## Worked excerpt
|
|
121
|
+
|
|
122
|
+
From `docs/dumb/2026-09-22-retry-webhook-backoff.md`, the why and one part.
|
|
123
|
+
|
|
124
|
+
````markdown
|
|
125
|
+
## Why this exists
|
|
126
|
+
|
|
127
|
+
Epic 5 promises that a confirmed payment always becomes a paid order, because a
|
|
128
|
+
customer whose money left their account and whose order still says "pending" will
|
|
129
|
+
charge back and stop trusting the shop. Today the payment provider tells us once,
|
|
130
|
+
and `handlePaymentWebhook` processes it immediately. If our order service is down
|
|
131
|
+
for that one second, the message is gone and nobody ever retries, so the payment is
|
|
132
|
+
lost silently. Story 5.4 already made reprocessing safe to repeat, which is what
|
|
133
|
+
makes an automatic retry possible at all. This story adds the place to keep a failed
|
|
134
|
+
message and the schedule for trying it again.
|
|
135
|
+
|
|
136
|
+
### handlePaymentWebhook
|
|
137
|
+
**In general** — A webhook is one system calling another to announce that something
|
|
138
|
+
happened, so the receiver does not have to ask repeatedly. The usual alternative is
|
|
139
|
+
polling, where you ask "is it paid yet?" on a timer, which wastes calls and still
|
|
140
|
+
adds delay.
|
|
141
|
+
**In this project** — It is the exported function in `src/payments/webhook.ts`. The
|
|
142
|
+
provider posts the confirmation to it, and it calls `processOrder` straight away.
|
|
143
|
+
**How it connects** — The payment provider feeds it, and it feeds `processOrder`
|
|
144
|
+
today. After this story it also feeds the `webhook_retries` table whenever
|
|
145
|
+
`processOrder` throws, and the retry job picks up from there.
|
|
146
|
+
**Example** — A payment clears, the provider posts `{"id":"pay_123","status":"paid"}`,
|
|
147
|
+
the function calls `processOrder` with it, and the order flips to paid.
|
|
148
|
+
**❌ Doing it wrong** — Catching the error and returning 200 without storing anything.
|
|
149
|
+
The provider sees success and never sends the message again, so the payment is lost
|
|
150
|
+
with no trace to debug.
|
|
151
|
+
**✅ Doing it right** — Write the payload to `webhook_retries` before returning, because
|
|
152
|
+
the row is the only evidence that the payment arrived, and the retry job has nothing
|
|
153
|
+
to work from without it.
|
|
154
|
+
````
|
|
@@ -1,82 +1,115 @@
|
|
|
1
|
-
# Levels —
|
|
1
|
+
# Levels — the chat reply and the dialogue
|
|
2
|
+
|
|
3
|
+
The depth goes in the document ([document.md](document.md)). The chat stays short: at most 100 words plus the path. Never paste the document into the chat.
|
|
2
4
|
|
|
3
5
|
Headers below are shown in English, the canonical form; render them in the user's language.
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## The chat reply, every level except `terms`
|
|
6
8
|
|
|
7
9
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
<3–5 short lines: what we're doing and why it matters, plain words,
|
|
11
|
+
one new technical term per sentence at most.>
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
📄 <path to the file that was written>
|
|
14
|
+
Read the whole document first. When you are done, say so and we go on from there.
|
|
15
|
+
```
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
<2–3 sentences. One real-world analogy, each part mapped to one part of this step.>
|
|
17
|
+
At `senior` the last line is omitted. This first reply never asks a question and never offers terms: the user has not read the document yet. The level's ending below is sent only after the user says they have read it ("li", "read it", "done").
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
<2 bullets, one sentence each: trade-offs or pitfalls a senior would watch for right here.>
|
|
19
|
+
## `dev` (default) — ending, after the user has read the document
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
<One question the user can answer to check they got it. Omit if it would be forced.>
|
|
21
|
+
One line, nothing more:
|
|
23
22
|
|
|
24
|
-
➡️ Next step: <one line>
|
|
25
23
|
```
|
|
26
|
-
|
|
27
|
-
## `zero` — about 300 words, the analogy carries the explanation
|
|
28
|
-
|
|
24
|
+
Want me to explain <term>, <term> or <term>, or shall we continue?
|
|
29
25
|
```
|
|
30
|
-
🪄 Analogy
|
|
31
|
-
<3–4 sentences. The real-world analogy first; the rest of the answer refers back to it.>
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
<2–3 sentences. The step, told through the analogy; every technical word appears as
|
|
35
|
-
"term (plain-words meaning)" the first time.>
|
|
27
|
+
If the user says continue, say nothing further and continue the task. If the user picks a term, answer it in chat under the dialogue rules below.
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
<2–3 sentences: what goes wrong without it, in the analogy's terms, then in the
|
|
39
|
-
project's terms. On a BMAD story this slot still names the epic, the stories it
|
|
40
|
-
builds on and the story it blocks — in plain words, not numbers alone.>
|
|
29
|
+
## `zero` — ending, after the user has read the document
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
<One question answerable from the analogy.>
|
|
31
|
+
Aliases `eli5`, `beginner`, `junior`. Sent when the user says they have read it, even if that message also says "continue" (at this level the check is the gate back to the task); the reply ends here and **the turn stops**. Do not explain the offered terms yet. The task resumes after the user answers, or after they say they would rather skip the check.
|
|
44
32
|
|
|
45
|
-
|
|
33
|
+
```
|
|
34
|
+
🤔 Before we go on:
|
|
35
|
+
1. Want me to explain any of these? <term> · <term> · <term>
|
|
36
|
+
2. Quick question: <one simple comprehension question about this step>
|
|
46
37
|
```
|
|
47
38
|
|
|
48
|
-
|
|
39
|
+
## `senior` — ending
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
Aliases `pro`, `expert`. Nothing, not even the "read it first" line. No question, no offer to define terms. The document carries the trade-offs.
|
|
42
|
+
|
|
43
|
+
## `terms` — chat only, no file
|
|
44
|
+
|
|
45
|
+
Aliases `termos`, `jargon`, `glossary`.
|
|
51
46
|
|
|
52
47
|
```
|
|
53
48
|
📚 Terms in this step
|
|
54
|
-
- **<term>** — <one-sentence meaning>. Here: <one concrete example from this task>.
|
|
49
|
+
- **<term>** — <one-sentence meaning, with the reason it exists>. Here: <one concrete example from this task>.
|
|
55
50
|
- ...
|
|
56
51
|
|
|
57
52
|
➡️ Next step: <one line>
|
|
58
53
|
```
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
Three to eight items, no analogy.
|
|
56
|
+
|
|
57
|
+
## Dialogue rules
|
|
58
|
+
|
|
59
|
+
- **A term the user picked** — about 80 words: one sentence of meaning, then how it works in this project naming a real file or function, then one tiny example. Then, at `zero` only, one new question.
|
|
60
|
+
- **A wrong answer to the check question** — do not repeat the same explanation. Explain it a different and simpler way, then ask one new question.
|
|
61
|
+
- **"entendi" / "ok" / "got it"** — close with `➡️ Next step` and nothing else.
|
|
62
|
+
- Never offer a term the user already knows or already had explained.
|
|
63
|
+
- Stop asking the moment the user turns back to the task.
|
|
64
|
+
|
|
65
|
+
## Worked example — `dev` chat reply
|
|
66
|
+
|
|
67
|
+
Situation: story 5.6, the `webhook_retries` migration was just written, the user asks "why are we doing this story?".
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
We just added the table that remembers a payment confirmation we failed to
|
|
71
|
+
process, so a retry can pick it up later. Without it, one second of downtime in
|
|
72
|
+
the order service loses a real payment, because the provider only tells us once.
|
|
73
|
+
I wrote the whole thing up, parts and diagrams included.
|
|
74
|
+
|
|
75
|
+
📄 docs/dumb/2026-09-22-retry-webhook-backoff.md
|
|
76
|
+
Read the whole document first. When you are done, say so and we go on from there.
|
|
77
|
+
```
|
|
61
78
|
|
|
62
|
-
|
|
79
|
+
The user replies "read it". Only now the offer line:
|
|
63
80
|
|
|
64
81
|
```
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
Want me to explain webhook, idempotency or dead letter, or shall we continue?
|
|
83
|
+
```
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
5.6 belongs to Epic 5 (Payments): "a confirmed payment becomes a paid order, always". 5.3 (webhook) and 5.4 (idempotency) guarantee the confirmation arrives and is not processed twice, but not that it arrives at all if the order processor is down that second. Without retry a real payment vanishes, which is why 5.7 (status screen) is blocked: you cannot show "paid" for something that can disappear. AC 2 fixes the shape: 1m → 5m → 25m → 2h, 5 attempts.
|
|
85
|
+
## Worked example — `zero` chat reply, then one follow-up
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
```
|
|
88
|
+
We created a table called `webhook_retries`. It stores the payment confirmations
|
|
89
|
+
our system failed to handle the first time, so we can try again later. Without
|
|
90
|
+
it, the money leaves the customer and the order never becomes paid.
|
|
73
91
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
📄 docs/dumb/2026-09-22-retry-webhook-backoff.md
|
|
93
|
+
Read the whole document first. When you are done, say so and we go on from there.
|
|
94
|
+
```
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
The user replies "done, read it". Only now the check block:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
🤔 Before we go on:
|
|
100
|
+
1. Want me to explain any of these? webhook · idempotency · dead letter
|
|
101
|
+
2. Quick question: if we never saved that message, what would happen to the
|
|
102
|
+
customer's order?
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The user replies "I don't know what a webhook is". Only that term is answered, then one new question:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
A webhook is one system calling another to announce that something just happened,
|
|
109
|
+
so we never have to keep asking. Here the payment provider calls our
|
|
110
|
+
`handlePaymentWebhook` function in `src/payments/webhook.ts` the moment a payment
|
|
111
|
+
clears. It arrives once, which is exactly why losing it is expensive.
|
|
80
112
|
|
|
81
|
-
|
|
113
|
+
🤔 One more: that function calls `processOrder` straight away. What happens today
|
|
114
|
+
if `processOrder` fails?
|
|
82
115
|
```
|