chiltepin 0.47.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/dist/bin.js +3582 -0
  4. package/dist/bin.js.map +1 -0
  5. package/package.json +93 -0
  6. package/templates/chiltepin.config.json +5 -0
  7. package/templates/demo.md +2161 -0
  8. package/templates/docs/getting-started.md +155 -0
  9. package/templates/docs/tutorial.md +559 -0
  10. package/templates/skill/SKILL.md +172 -0
  11. package/templates/skill/reference/blocks/INDEX.md +141 -0
  12. package/templates/skill/reference/blocks/agentic.md +63 -0
  13. package/templates/skill/reference/blocks/algorithms.md +49 -0
  14. package/templates/skill/reference/blocks/api.md +40 -0
  15. package/templates/skill/reference/blocks/architecture.md +94 -0
  16. package/templates/skill/reference/blocks/business.md +70 -0
  17. package/templates/skill/reference/blocks/charts-overviews.md +74 -0
  18. package/templates/skill/reference/blocks/data-model.md +34 -0
  19. package/templates/skill/reference/blocks/design-system.md +50 -0
  20. package/templates/skill/reference/blocks/flows.md +74 -0
  21. package/templates/skill/reference/blocks/narrative.md +65 -0
  22. package/templates/skill/reference/blocks/planning.md +74 -0
  23. package/templates/skill/reference/blocks/quality.md +43 -0
  24. package/templates/skill/reference/blocks/tables-data.md +55 -0
  25. package/templates/skill/reference/check.md +62 -0
  26. package/templates/skill/reference/decks.md +198 -0
  27. package/templates/skill/reference/exemplars/adr.md +87 -0
  28. package/templates/skill/reference/exemplars/agent-system.md +113 -0
  29. package/templates/skill/reference/exemplars/api-reference.md +110 -0
  30. package/templates/skill/reference/exemplars/backend-arch.md +117 -0
  31. package/templates/skill/reference/exemplars/data-pipeline.md +107 -0
  32. package/templates/skill/reference/exemplars/frontend-arch.md +93 -0
  33. package/templates/skill/reference/exemplars/incident-postmortem.md +93 -0
  34. package/templates/skill/reference/exemplars/migration-plan.md +95 -0
  35. package/templates/skill/reference/exemplars/onboarding.md +78 -0
  36. package/templates/skill/reference/exemplars/product-spec.md +81 -0
  37. package/templates/skill/reference/intake.md +140 -0
  38. package/templates/skill/reference/mermaid.md +216 -0
  39. package/templates/skill/reference/organizing.md +118 -0
  40. package/templates/skill/reference/patterns-design.md +59 -0
  41. package/templates/skill/reference/patterns.md +167 -0
  42. package/templates/skill/reference/recipes.md +153 -0
  43. package/templates/skill/reference/style-ste.md +119 -0
  44. package/templates/skill/reference/system-design.md +161 -0
  45. package/templates/skill/reference/writing.md +132 -0
@@ -0,0 +1,198 @@
1
+ # Slide decks — `chiltepin slides`
2
+
3
+ Part of the **chiltepin** skill (the hub is `SKILL.md`, one folder up). Read
4
+ this for any slides or deck ask.
5
+
6
+ ## Slide decks (`chiltepin slides`)
7
+
8
+ Any document renders as a deck with `chiltepin slides`. **Each top-level heading
9
+ (`#`/`##`) starts a new slide and is its title.** Everything until the next
10
+ heading — prose *and* every block — stays on that slide, so a slide can hold
11
+ several blocks. (`###`+ headings stay in the slide body; to keep things on the
12
+ same slide, just don't add a new `#`/`##`.)
13
+
14
+ ````md
15
+ # Why now
16
+ A sentence of context, then any blocks under this heading.
17
+
18
+ ```drivers
19
+ items:
20
+ - { title: Slow, body: "p95 hit 2.4s.", icon: clock, accent: amber }
21
+ ```
22
+
23
+ # The fix
24
+ Next heading → next slide. This one stacks two blocks.
25
+
26
+ ```stats
27
+ stats:
28
+ - { value: "800ms", label: New p95 target, trend: flat }
29
+ ```
30
+
31
+ ```callout
32
+ tone: success
33
+ body: Both blocks land on "The fix" slide.
34
+ ```
35
+ ````
36
+
37
+ - This means a normal Chiltepin doc (sections under `##` headings) already
38
+ presents cleanly — no special markup needed. To author *for* slides, write one
39
+ `##` heading per slide and keep each to **one idea**. A heading plus one strong
40
+ visual (a diagram, `drivers`, `stats`, `pyramid`, `quadrant`, `timeline`) reads
41
+ better than dense prose.
42
+ - **Vertical alignment is automatic** — light slides (one block, little prose)
43
+ center; heavier slides (stacked blocks or lots of prose) top-align. To force it,
44
+ add a marker to the heading: `## Title {top}`, `## Title {center}`, or
45
+ `## Title {bottom}` (the marker is stripped from the displayed title). A fourth
46
+ marker, `## Title {split}`, switches the slide to the consulting layout —
47
+ prose left, exhibit right (see *Consulting-style decks* below).
48
+ - **Builds are automatic.** A diagram with a natural order — `sequence`,
49
+ `flow`, `state`, `saga`, `spans`, `steps`, `timeline` — reveals one item per
50
+ → press before the deck moves on; earlier items stay, the newest takes the
51
+ accent, and ← walks back. Add `## Title {nobuild}` to show that slide whole.
52
+ The page and print always show everything.
53
+ - Every non-cover slide automatically gets a footer (deck title · page number).
54
+ - **`chiltepin build` — and the studio's Site link — emit both views of every
55
+ doc**: the page plus a companion deck at `<slug>.slides.html`. A Doc | Slides
56
+ toggle links the two on each page, so a doc is a deck with no extra command.
57
+ Studio's Present mode shows the current doc's deck without even saving.
58
+ - **Long sections paginate automatically** — each slide has a content budget
59
+ weighted by block item counts. A hero-scale block is one heavy enough to
60
+ fill a slide on its own — a big diagram, a many-card grid. It splits onto
61
+ its own slide with the same section title instead of sharing the stage with
62
+ its section's prose.
63
+ - **Two-part slides auto-split.** Substantial prose plus a medium exhibit that
64
+ would overflow stacked lays out side by side automatically. Prose becomes
65
+ the left message column, the exhibit the right — the same layout `{split}`
66
+ forces. Write the section naturally; the deck picks the layout.
67
+ - The `meta` block is the cover slide. A doc with **no headings at all** falls
68
+ back to one slide per block (legacy behavior).
69
+
70
+ ### Consulting-style decks
71
+
72
+ For an executive or consulting-grade deck, hold every slide to the formula
73
+ **assertion → exhibit → takeaway**:
74
+
75
+ - **Action titles.** Each `##` is a full-sentence assertion the slide proves
76
+ ("Checkout latency costs us conversions"), never a topic label
77
+ ("Latency"). Someone flipping through only the titles should get the whole
78
+ argument.
79
+ - **`{split}` layout.** `## Title {split}` puts the slide's prose in a left
80
+ *message* column and its blocks in a right *exhibit* column — the classic
81
+ consulting slide. Write 1-3 short punchy paragraphs, then exactly one strong
82
+ block.
83
+ - **One exhibit per slide.** A `chart`, `scorecard`, `heatmap`, or
84
+ a diagram — the block *is the evidence* for the title's claim. Two exhibits
85
+ means two slides.
86
+ - **Open each part with a `divider`.** A deck with 3+ parts gets an
87
+ interstitial per part — a `divider` alone under its own `##` heading
88
+ (`kicker: PART 2`, an assertion as the `title`). It renders as a clean
89
+ full-band break slide.
90
+ - **The money slide is a `bignumber`.** When one metric carries the whole
91
+ argument ("-75% checkout p95"), give it its own `{split}` slide: message
92
+ left, the `bignumber` as the exhibit right. Do not use a one-item `stats` row.
93
+ - **Close the argument with `takeaways`**: the 2-4 things the room must
94
+ remember, numbered; follow with a `callout` (`tone: success`) only if
95
+ there's a separate ask.
96
+ - **No thin slides.** A heading floating over one small block reads empty —
97
+ merge it into a neighbour, or give it a message column with `{split}`.
98
+
99
+ A three-part skeleton using all three:
100
+
101
+ ````md
102
+ ## Part 1 — checkout is bleeding conversions
103
+
104
+ ```divider
105
+ kicker: PART 1
106
+ title: Checkout is bleeding conversions
107
+ accent: navy
108
+ ```
109
+
110
+ ## One number tells the story {split}
111
+
112
+ The async capture change removed the 1.7s synchronous call from the
113
+ request path. Nothing else moved.
114
+
115
+ ```bignumber
116
+ value: "-75%"
117
+ label: Checkout p95 after the change
118
+ trend: down
119
+ accent: green
120
+ ```
121
+
122
+ ## What to remember
123
+
124
+ ```takeaways
125
+ items:
126
+ - text: The synchronous capture call was the bottleneck
127
+ - text: Moving it to a queue cut p95 by 75%
128
+ - text: Conversion recovered within two weeks
129
+ ```
130
+ ````
131
+
132
+ ````md
133
+ ## Checkout latency costs us conversions {split}
134
+
135
+ Every 100ms of checkout latency costs ~0.6% conversion. Our p95 has drifted
136
+ to 2.4s — the synchronous capture call is 71% of it.
137
+
138
+ ```chart
139
+ kind: bar
140
+ title: Where the 2.4s goes
141
+ labels: [Gateway, Fraud, Capture, Persist, Render]
142
+ series:
143
+ - { label: p95 ms, values: [120, 260, 1700, 180, 140] }
144
+ ```
145
+ ````
146
+
147
+
148
+ ### The design-review arc — the structural exemplar
149
+
150
+ For a full design-review deck, follow this **arc**. The structure is the
151
+ template — swap every exhibit and every title for the system at hand; nothing
152
+ about the topic carries over.
153
+
154
+ | # | Slide (always an action title) | Layout | Exhibit |
155
+ |---|---|---|---|
156
+ | 1 | Cover | `meta` | — |
157
+ | 2 | PART 1 · the problem | `divider` | — |
158
+ | 3 | *The pain, stated as a claim* | `{split}` | `chart` — the evidence |
159
+ | 4 | *The scale is real* | — | `envelope` — the math that sets the target |
160
+ | 5 | PART 2 · the design | `divider` | — |
161
+ | 6 | *Who touches the system* | — | `c4` (context) |
162
+ | 7 | *The design, stated as a claim* | `{split}` | `block` — the shape |
163
+ | 8 | *The decision that mattered* | `{split}` | `options` — chosen vs rejected |
164
+ | 9 | *The budget holds* | — | a waterfall `chart` against the target |
165
+ | 10 | *One request, end to end* | — | `sequence` |
166
+ | 11 | *When X degrades, …* | — | `swimlane` — the ops story |
167
+ | 12 | PART 3 · the commitment | `divider` | — |
168
+ | 13 | *What we're measured on* | — | `slo` |
169
+ | 14 | *The number that matters* | — | `bignumber` |
170
+ | 15 | Takeaways | — | `takeaways` — the close |
171
+
172
+ What the arc encodes (keep these even when you reshape it):
173
+
174
+ - **Three parts, opened by dividers**: problem → design → commitment. The
175
+ reader always knows where they are.
176
+ - **Evidence before design**: slides 3-4 earn the right to propose anything —
177
+ a complaint chart, a metric, then the envelope math. That math turns pain
178
+ into a numeric target the rest of the deck answers to.
179
+ - **One decision slide** (8): every real design had a fork; show the rejected
180
+ option honestly or the deck reads as a sales pitch.
181
+ - **Slides 9-11 are chosen by YOUR bottleneck**, not by this table: a fan-out
182
+ system shows `sequence` + `swimlane`; a storage system might show `erd` +
183
+ `heatmap`; an agent system `agentloop` + `trace`. Two or three deep dives,
184
+ never a fixed list.
185
+ - **The commitment close never changes**: objectives (`slo`) → the money
186
+ number (`bignumber`, echoing slide 4's target) → `takeaways`. The last
187
+ takeaway carries the ask (effort, flag, rollback).
188
+
189
+ ## Provenance and position
190
+
191
+ `## Title {source: production traces, 14 Oct 2026}` puts a source line in the
192
+ slide footer, where every consulting exhibit carries one. It sits in the
193
+ footer rather than under the block on purpose. The fitter scales the exhibit,
194
+ and a source line that shrinks with it stops being readable.
195
+
196
+ A deck with two or more `divider` bands also grows a **tracker** in the slide
197
+ header — the parts of the deck with the current one lit. The room always
198
+ knows where it is. One divider draws nothing: a strip of one says nothing.
@@ -0,0 +1,87 @@
1
+ ```meta
2
+ title: ADR-014 — Sync engine for co-writing
3
+ subtitle: Why Plotline replaced last-writer-wins with CRDT sync, and what it measurably changed.
4
+ tag: ADR · Accepted
5
+ ```
6
+
7
+ In March, 23% of active manuscripts had two or more writers in the same
8
+ hour, and last-writer-wins overwrote 31 reported edits that month.
9
+ "Lost my changes" became the top support tag. A ghostwriter and an editor
10
+ routinely work the same scene at the same time, so the fix had to allow
11
+ concurrent edits — not serialize them.
12
+
13
+ ## Options
14
+
15
+ ```options
16
+ id: ex-adr-options
17
+ items:
18
+ - kicker: Option 1
19
+ title: Section locking
20
+ how: One writer holds a scene at a time; others wait or fork.
21
+ pros: [No merge logic at all, Ships in two weeks]
22
+ cons: ["Blocks the ghostwriter + editor pair — our core workflow", Stale locks need a timeout policy]
23
+ verdict: "REJECTED — solves the symptom by forbidding the use case"
24
+ tone: rejected
25
+ - kicker: Option 2
26
+ title: OT server
27
+ how: A central server transforms and orders every operation.
28
+ pros: [Proven at scale by Google Docs, Server log makes debugging linear]
29
+ cons: [Every keystroke round-trips — offline writing stops working, The sequencer is a single point of failure]
30
+ verdict: "VIABLE — fallback if CRDT storage costs blow up"
31
+ tone: viable
32
+ - kicker: Option 3
33
+ title: CRDT (Yjs)
34
+ how: Every client merges; the server only relays and persists updates.
35
+ pros: [Offline edits merge on reconnect, "80 ms p50 merge in the prototype", Relay server is stateless]
36
+ cons: ["Stored docs grow ~1.6× from tombstones", Merge output is harder to debug than a server log]
37
+ verdict: "CHOSEN"
38
+ tone: chosen
39
+ ```
40
+
41
+ ## Decision
42
+
43
+ ```callout
44
+ id: ex-adr-decision
45
+ tone: note
46
+ title: Decision
47
+ body: "Plotline syncs manuscripts with Yjs CRDTs. The server relays and persists updates; it never resolves them."
48
+ ```
49
+
50
+ ## What we accepted
51
+
52
+ ```proscons
53
+ id: ex-adr-consequences
54
+ pros:
55
+ - Concurrent edits merge without a lock or a round-trip
56
+ - Writers keep working through the full offline flight test
57
+ - Relay servers scale horizontally — no sequencer to shard
58
+ cons:
59
+ - "Stored manuscript grows ~1.6× — tombstones never leave the doc"
60
+ - Deletion GC needs a weekly compaction job we now own
61
+ - A bad merge has no single log to replay
62
+ ```
63
+
64
+ The trade we made explicit: 83 KB more per manuscript is about 10 GB across
65
+ Plotline's 120,000 manuscripts — under $1 a month at $0.02/GB. Each
66
+ lost-edit ticket cost a churn-risk conversation. We bought
67
+ reliability with pennies of disk.
68
+
69
+ ## Measured outcome
70
+
71
+ ```benchmark
72
+ id: ex-adr-outcome
73
+ metricLabel: Metric
74
+ subjects:
75
+ - { label: Before, sub: last-writer-wins, tone: muted }
76
+ - { label: After, sub: Yjs rollout, featured: true }
77
+ rows:
78
+ - { label: Lost-edit tickets / month, better: low, cells: ["31", "2"] }
79
+ - { label: Sync latency p50, better: low, cells: ["140 ms", "80 ms"] }
80
+ - { label: Doc load p95, better: low, cells: ["410 ms", "460 ms"] }
81
+ - { label: Storage per manuscript (median), better: low, cells: ["148 KB", "231 KB"] }
82
+ note: "Four weeks either side of the 100% rollout, June; same manuscript cohort."
83
+ ```
84
+
85
+ Load p95 and storage went the wrong way, as the options card predicted. Both
86
+ stayed inside the budget we set before rollout (500 ms, 300 KB), so the
87
+ decision stands without amendment.
@@ -0,0 +1,113 @@
1
+ ```meta
2
+ title: Reconciliation agent
3
+ subtitle: How Caravel Freight's agent matches carrier invoices to shipments — the loop, the window, one real run.
4
+ tag: Agent system · v2
5
+ ```
6
+
7
+ Caravel Freight books about 9,000 ocean and air shipments a month. Carriers
8
+ invoice 1,400 line items a week, and roughly 30% disagree with the quoted
9
+ rate. The agent proposes a match and a verdict per line; a billing clerk
10
+ approves every proposal. The agent has no path to the ledger.
11
+
12
+ ## The loop
13
+
14
+ ```agentloop
15
+ id: ex-agent-system-loop
16
+ agent:
17
+ name: Reconciliation agent
18
+ model: claude-sonnet-4-6
19
+ note: One invoice line per episode.
20
+ env: Invoice queue
21
+ tools:
22
+ - { name: get_shipment, desc: "Booking, lane, and quoted rate" }
23
+ - { name: search_rate_card, desc: Contract rate for a lane and sail date }
24
+ - { name: get_invoice_history, desc: "The carrier's past corrections" }
25
+ - { name: post_proposal, desc: Write match + verdict to the review queue }
26
+ - { name: escalate, desc: Send the line to a clerk with a reason }
27
+ memory:
28
+ - carrier correction history
29
+ - verdicts on earlier lines
30
+ stop: proposal posted or line escalated
31
+ ```
32
+
33
+ Three conditions force `escalate` instead of a proposal: no booking
34
+ reference resolves, the rate delta exceeds $400, or the invoice currency
35
+ differs from the contract currency. Everything else — 68% of lines — ends in
36
+ a posted proposal.
37
+
38
+ ## Window budget
39
+
40
+ ```context
41
+ id: ex-agent-system-window
42
+ window: 100000
43
+ segments:
44
+ - { label: system prompt, tokens: 3500, accent: navy }
45
+ - { label: tool schemas, tokens: 4500, accent: teal }
46
+ - { label: invoice line + OCR text, tokens: 9000, accent: amber }
47
+ - { label: shipment record, tokens: 6000, accent: blue }
48
+ - { label: rate-card excerpt, tokens: 12000, accent: purple, desc: top-3 matching lanes only }
49
+ - { label: carrier history, tokens: 15000, accent: green }
50
+ ```
51
+
52
+ The free 50,000 tokens are the episode's working room — tool results and
53
+ retries append there. Carrier history is evicted first when a long episode
54
+ fills the window. That is safe because history only tunes the verdict
55
+ wording; the match depends on the shipment record and the rate card, which
56
+ are never evicted.
57
+
58
+ ## One real run
59
+
60
+ ```trace
61
+ id: ex-agent-system-run
62
+ turns:
63
+ - role: user
64
+ text: "Invoice CMA-88231-04 · Shanghai → Rotterdam · 2 × 40HC · $4,120"
65
+ - role: assistant
66
+ thinking: The quoted rate lives on the booking. Fetch the shipment before touching the rate card.
67
+ text: Resolving booking CV-59107 for this line.
68
+ - role: tool
69
+ tool: get_shipment
70
+ args: '{ "booking": "CV-59107" }'
71
+ result: "2 × 40HC · lane CNSHA→NLRTM · quoted $3,890 all-in · sailed 2026-07-14"
72
+ - role: tool
73
+ tool: search_rate_card
74
+ args: '{ "lane": "CNSHA-NLRTM", "date": "2026-07-14" }'
75
+ result: "timeout after 10s"
76
+ - role: assistant
77
+ thinking: One retry is allowed on timeout; escalating on a transient error wastes a clerk's time.
78
+ text: Rate-card lookup timed out — retrying once.
79
+ - role: tool
80
+ tool: search_rate_card
81
+ args: '{ "lane": "CNSHA-NLRTM", "date": "2026-07-14" }'
82
+ result: "contract $3,890 per pair · valid through 2026-09-30 · no peak surcharge clause"
83
+ - role: assistant
84
+ text: "Invoice exceeds contract by $230 with no surcharge clause in force. Proposing: match to CV-59107, dispute $230."
85
+ - role: tool
86
+ tool: post_proposal
87
+ args: '{ "booking": "CV-59107", "verdict": "dispute", "amount": 230 }'
88
+ result: "queued for review · PR-20441"
89
+ ```
90
+
91
+ ## Eight weeks in production
92
+
93
+ ```stats
94
+ id: ex-agent-system-outcomes
95
+ stats:
96
+ - { value: 68%, label: Lines auto-proposed, delta: "+68pp vs manual", trend: up }
97
+ - { value: 45s, label: Clerk time per line, delta: "-87%", trend: down }
98
+ - { value: 0.4%, label: Proposals reversed after approval, trend: flat }
99
+ - { value: $18k, label: Disputed overcharges recovered / month, trend: up }
100
+ ```
101
+
102
+ Reversals are the number to watch: each one means a clerk approved a wrong
103
+ verdict. That makes 0.4% the agent's real error rate as the business sees
104
+ it.
105
+
106
+ ## The boundary
107
+
108
+ ```callout
109
+ id: ex-agent-system-boundary
110
+ tone: danger
111
+ title: Propose, never post
112
+ body: "`post_proposal` writes to the review queue only. Ledger writes require a clerk's approval click, and the agent's service account holds no ledger credential — the boundary is IAM, not prompt text."
113
+ ```
@@ -0,0 +1,110 @@
1
+ ```meta
2
+ title: Skerry API
3
+ subtitle: Render any URL to PNG, WebP, or PDF in three calls.
4
+ tag: API · v1
5
+ ```
6
+
7
+ The base URL is `https://api.skerry.dev/v1`; authenticate every call with
8
+ `Authorization: Bearer sk_live_…`. Captures run asynchronously — a POST returns
9
+ `202` with an id, and the file arrives seconds later via webhook or polling. A
10
+ capture spends one credit only when it succeeds; rate limits are 10 requests
11
+ per second with bursts to 50.
12
+
13
+ ## Create a capture
14
+
15
+ ```endpoint
16
+ id: ex-api-create
17
+ method: POST
18
+ path: /captures
19
+ auth: Bearer sk_live_…
20
+ body:
21
+ - { name: url, type: string, required: true, desc: "Page to render; must be reachable from the public internet" }
22
+ - { name: format, type: string, desc: "png | webp | pdf — default png" }
23
+ - { name: width, type: integer, desc: "Viewport width in px; default 1280" }
24
+ - { name: full_page, type: boolean, desc: Capture the full scroll height }
25
+ - { name: webhook_url, type: string, desc: Receives capture.finished / capture.failed }
26
+ responses:
27
+ - { status: 202, desc: Capture queued }
28
+ - { status: 402, desc: Credit balance is zero }
29
+ - { status: 422, desc: URL malformed or scheme not http(s) }
30
+ request: |
31
+ { "url": "https://example.com/pricing", "format": "png", "full_page": true }
32
+ response: |
33
+ { "id": "cap_8fk2", "status": "queued" }
34
+ ```
35
+
36
+ ## Fetch a capture
37
+
38
+ ```endpoint
39
+ id: ex-api-fetch
40
+ method: GET
41
+ path: /captures/{id}
42
+ description: result_url is a signed link that expires 24 hours after the capture finishes.
43
+ params:
44
+ - { name: id, in: path, type: string, required: true, desc: Capture id from the create call }
45
+ responses:
46
+ - { status: 200, desc: Capture in any status }
47
+ - { status: 404, desc: Unknown or expired id }
48
+ response: |
49
+ { "id": "cap_8fk2", "status": "done",
50
+ "result_url": "https://files.skerry.dev/cap_8fk2.png?sig=…" }
51
+ ```
52
+
53
+ ## List captures
54
+
55
+ ```endpoint
56
+ id: ex-api-list
57
+ method: GET
58
+ path: /captures
59
+ params:
60
+ - { name: cursor, in: query, type: string, desc: Opaque cursor from the previous page }
61
+ - { name: limit, in: query, type: integer, desc: "1–100, default 25" }
62
+ - { name: status, in: query, type: string, desc: "Filter: queued | rendering | done | failed" }
63
+ responses:
64
+ - { status: 200, desc: "Newest first, with next_cursor when more exist" }
65
+ ```
66
+
67
+ ## One capture, end to end
68
+
69
+ ```sequence
70
+ id: ex-api-seq
71
+ actors:
72
+ - { id: Client, name: Your server }
73
+ - { id: Skerry, name: Skerry API }
74
+ - { id: Hook, name: Your webhook, sub: webhook_url, external: true }
75
+ messages:
76
+ - Client -> Skerry: POST /captures
77
+ - Skerry --> Client: 202 · cap_8fk2 queued
78
+ - { from: Skerry, to: Skerry, kind: note, label: "render, 2–8 s typical" }
79
+ - { from: Skerry, to: Hook, label: POST capture.finished, kind: async, summary: "Signed with X-Skerry-Signature; non-2xx responses are redelivered 5 times over 30 minutes." }
80
+ - Hook --> Skerry: 2xx ack
81
+ - Client -> Skerry: GET /captures/cap_8fk2
82
+ - Skerry --> Client: 200 · done + result_url
83
+ foot:
84
+ - { label: Webhook delivery, value: at-least-once }
85
+ - { label: result_url TTL, value: 24 h }
86
+ ```
87
+
88
+ Webhook delivery is at-least-once, so make the handler idempotent on the
89
+ capture id. Polling is the fallback, not a race: `result_url` appears on
90
+ `GET /captures/{id}` the moment status is `done`, whether or not any webhook
91
+ was delivered.
92
+
93
+ ## Errors
94
+
95
+ ```table
96
+ columns: [Status, Meaning, What to do]
97
+ rows:
98
+ - [401, Missing or revoked key, "Rotate the key in the dashboard; do not retry."]
99
+ - [402, Credit balance is zero, "Top up; already-queued captures still finish."]
100
+ - [422, URL invalid or scheme not http(s), Retrying identical input fails identically.]
101
+ - [429, Rate limit exceeded, "Back off for Retry-After seconds, then retry."]
102
+ - [500, Skerry fault, "Retry with backoff; the capture id stays valid."]
103
+ note: "A render that fails is not an HTTP error: the capture ends as status failed with a failure_reason, and spends no credit."
104
+ ```
105
+
106
+ ```callout
107
+ tone: warn
108
+ title: Verify X-Skerry-Signature
109
+ body: "Every webhook carries an HMAC-SHA256 of the raw body, keyed with your signing secret, plus a timestamp. Reject anything unsigned or older than five minutes — an unverified handler lets anyone mark your captures done."
110
+ ```
@@ -0,0 +1,117 @@
1
+ ```meta
2
+ title: Marram — payout engine
3
+ subtitle: How Fenwick moves seller money from captured charge to bank account.
4
+ tag: Backend · v2
5
+ ```
6
+
7
+ Fenwick is a marketplace for used camera gear; buyers pay Fenwick, and Fenwick
8
+ pays its sellers once a day. Marram owns that gap. It holds seller balances
9
+ between capture and payout and splits each charge into seller net plus an 8%
10
+ platform fee. Every afternoon it executes about 38,000 SEPA transfers.
11
+
12
+ The constraint that shaped the design is auditability: a regulator can ask for
13
+ any seller's position on any past date. So every money movement is a
14
+ double-entry journal, and balances are derived, never stored.
15
+
16
+ ## Boundaries
17
+
18
+ ```c4
19
+ id: ex-backend-c4
20
+ level: container
21
+ boundary: { label: Marram }
22
+ nodes:
23
+ - { id: core, kind: external, name: marketplace-core, desc: Checkout and charge capture. }
24
+ - { id: settler, kind: container, family: service, name: settler, tech: Go, desc: Turns captures into ledger journals. }
25
+ - { id: api, kind: container, family: service, name: payout-api, tech: Go, desc: Balances and payout status. }
26
+ - { id: batcher, kind: container, family: service, name: batcher, tech: Go, desc: Runs the daily payout batch. }
27
+ - { id: ledger, kind: store, name: ledger-db, tech: Postgres 16, desc: Journals and entries. }
28
+ - { id: vaultic, kind: external, name: Vaultic, desc: Acquirer; executes SEPA transfers. }
29
+ edges:
30
+ - { from: core, to: settler, label: publishes charge.captured, tech: Kafka, kind: dashed }
31
+ - { from: settler, to: ledger, label: posts capture journals, tech: SQL }
32
+ - { from: api, to: ledger, label: reads balances, tech: SQL }
33
+ - { from: batcher, to: ledger, label: posts payout journals, tech: SQL }
34
+ - { from: batcher, to: vaultic, label: creates transfers, tech: REST }
35
+ - { from: vaultic, to: api, label: transfer status webhooks, tech: HTTPS, kind: dashed }
36
+ ```
37
+
38
+ Charge capture stays in marketplace-core, so a Marram outage delays payouts but
39
+ never blocks checkout. Vaultic's webhooks land on payout-api, not the batcher —
40
+ a batch crash therefore never loses a settlement status.
41
+
42
+ ## The ledger
43
+
44
+ ```erd
45
+ id: ex-backend-erd
46
+ entities:
47
+ - name: journals
48
+ columns:
49
+ - { name: id, type: uuid, pk: true }
50
+ - { name: kind, type: text }
51
+ - { name: created_at, type: timestamptz }
52
+ - name: entries
53
+ columns:
54
+ - { name: id, type: uuid, pk: true }
55
+ - { name: journal_id, type: uuid, fk: true }
56
+ - { name: account, type: text }
57
+ - { name: amount_cents, type: bigint }
58
+ - name: transfers
59
+ columns:
60
+ - { name: id, type: uuid, pk: true }
61
+ - { name: seller_id, type: uuid }
62
+ - { name: journal_id, type: uuid, fk: true }
63
+ - { name: status, type: text }
64
+ relations:
65
+ - journals ||--o{ entries: contains
66
+ - journals ||--o{ transfers: funds
67
+ ```
68
+
69
+ A seller's balance is `SUM(amount_cents)` over their entries — there is no
70
+ balance column to drift. The read costs about 30 ms across the 90-day hot
71
+ partitions, and payout-api pays that price on every balance call.
72
+
73
+ ## The daily payout run
74
+
75
+ ```sequence
76
+ id: ex-backend-seq
77
+ endpoint: { method: POST, path: /transfers }
78
+ actors:
79
+ - { id: Batcher, name: batcher, sub: "daily 14:00 UTC" }
80
+ - { id: PG, name: ledger-db, sub: Postgres }
81
+ - { id: Vaultic, name: Vaultic, sub: acquirer, external: true }
82
+ messages:
83
+ - { from: Batcher, to: PG, label: SELECT due balances, summary: "Sellers with settled balance of 10 EUR or more; about 38k rows." }
84
+ - { from: PG, to: Batcher, label: due sellers, kind: response }
85
+ - { from: Batcher, to: Batcher, kind: note, label: derive transfer_id, summary: "transfer_id = date + seller_id, so every retry names the same transfer." }
86
+ - { from: Batcher, to: Vaultic, label: POST /transfers, summary: "One transfer per seller, transfer_id as the external reference." }
87
+ - { from: Vaultic, to: Batcher, label: 504 timeout, kind: error, summary: "On timeout the batcher retries the same transfer_id; Vaultic dedupes on it." }
88
+ - { from: Batcher, to: Vaultic, label: retry POST /transfers }
89
+ - { from: Vaultic, to: Batcher, label: 201 accepted, kind: response }
90
+ - { from: Batcher, to: PG, label: post payout journal, summary: "The journal posts only after Vaultic accepts — the ledger never claims money that did not move." }
91
+ foot:
92
+ - { label: Batch window, value: "14:00–14:40 UTC" }
93
+ - { label: Idempotency, value: "transfer_id, replay-safe" }
94
+ ```
95
+
96
+ Retries reuse the transfer_id and Vaultic deduplicates on it, so a seller
97
+ receives at most one transfer per day. A crashed batch is rerun whole, with
98
+ no reconciliation step.
99
+
100
+ ## Who owns what
101
+
102
+ ```table
103
+ columns: [Service, Responsibility, On-call]
104
+ rows:
105
+ - [payout-api, "Balance reads, payout status, Vaultic webhooks", Payments Core]
106
+ - [settler, "charge.captured events → capture journals", Payments Core]
107
+ - [batcher, "Daily selection, transfer execution, payout journals", Money Movement]
108
+ - [ledger-db, "Journals and entries — the source of truth", Payments Core]
109
+ ```
110
+
111
+ ## The invariant
112
+
113
+ ```callout
114
+ tone: danger
115
+ title: Journals balance; entries never change
116
+ body: "Every journal's entries must sum to zero — a trigger rejects the whole insert otherwise. Posted entries (#ex-backend-erd) are never updated or deleted; a correction is a new reversing journal. Any code path that edits an entry in place is a bug, whatever it fixes."
117
+ ```