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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/bin.js +3582 -0
- package/dist/bin.js.map +1 -0
- package/package.json +93 -0
- package/templates/chiltepin.config.json +5 -0
- package/templates/demo.md +2161 -0
- package/templates/docs/getting-started.md +155 -0
- package/templates/docs/tutorial.md +559 -0
- package/templates/skill/SKILL.md +172 -0
- package/templates/skill/reference/blocks/INDEX.md +141 -0
- package/templates/skill/reference/blocks/agentic.md +63 -0
- package/templates/skill/reference/blocks/algorithms.md +49 -0
- package/templates/skill/reference/blocks/api.md +40 -0
- package/templates/skill/reference/blocks/architecture.md +94 -0
- package/templates/skill/reference/blocks/business.md +70 -0
- package/templates/skill/reference/blocks/charts-overviews.md +74 -0
- package/templates/skill/reference/blocks/data-model.md +34 -0
- package/templates/skill/reference/blocks/design-system.md +50 -0
- package/templates/skill/reference/blocks/flows.md +74 -0
- package/templates/skill/reference/blocks/narrative.md +65 -0
- package/templates/skill/reference/blocks/planning.md +74 -0
- package/templates/skill/reference/blocks/quality.md +43 -0
- package/templates/skill/reference/blocks/tables-data.md +55 -0
- package/templates/skill/reference/check.md +62 -0
- package/templates/skill/reference/decks.md +198 -0
- package/templates/skill/reference/exemplars/adr.md +87 -0
- package/templates/skill/reference/exemplars/agent-system.md +113 -0
- package/templates/skill/reference/exemplars/api-reference.md +110 -0
- package/templates/skill/reference/exemplars/backend-arch.md +117 -0
- package/templates/skill/reference/exemplars/data-pipeline.md +107 -0
- package/templates/skill/reference/exemplars/frontend-arch.md +93 -0
- package/templates/skill/reference/exemplars/incident-postmortem.md +93 -0
- package/templates/skill/reference/exemplars/migration-plan.md +95 -0
- package/templates/skill/reference/exemplars/onboarding.md +78 -0
- package/templates/skill/reference/exemplars/product-spec.md +81 -0
- package/templates/skill/reference/intake.md +140 -0
- package/templates/skill/reference/mermaid.md +216 -0
- package/templates/skill/reference/organizing.md +118 -0
- package/templates/skill/reference/patterns-design.md +59 -0
- package/templates/skill/reference/patterns.md +167 -0
- package/templates/skill/reference/recipes.md +153 -0
- package/templates/skill/reference/style-ste.md +119 -0
- package/templates/skill/reference/system-design.md +161 -0
- 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
|
+
```
|