skilldrop-cli 0.16.21 → 0.16.22

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 (33) hide show
  1. package/bin/skilldrop.js +5 -3
  2. package/catalogue.json +1 -0
  3. package/model-routing.json +4 -0
  4. package/package.json +1 -1
  5. package/packs/solution-architect/skills/interactive-diagram/SKILL.md +132 -0
  6. package/packs/solution-architect/skills/interactive-diagram/evals/eval_queries.json +12 -0
  7. package/packs/solution-architect/skills/interactive-diagram/evals/evals.json +74 -0
  8. package/packs/solution-architect/skills/interactive-diagram/evals/files/1/README.md +11 -0
  9. package/packs/solution-architect/skills/interactive-diagram/evals/files/1/api/app.py +43 -0
  10. package/packs/solution-architect/skills/interactive-diagram/evals/files/1/docker-compose.yml +20 -0
  11. package/packs/solution-architect/skills/interactive-diagram/evals/files/1/web/checkout.js +9 -0
  12. package/packs/solution-architect/skills/interactive-diagram/evals/files/1/worker/worker.py +23 -0
  13. package/packs/solution-architect/skills/interactive-diagram/evals/files/3/schema.sql +35 -0
  14. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/README.md +26 -0
  15. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/config/default.json +29 -0
  16. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/clients/transcription.js +25 -0
  17. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/db.js +16 -0
  18. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/handlers/storageWebhook.js +43 -0
  19. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/jobs/transcribeEpisode.js +28 -0
  20. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/mailer.js +18 -0
  21. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/server.js +32 -0
  22. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/worker.js +32 -0
  23. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.html +186 -0
  24. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.json +48 -0
  25. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.md +31 -0
  26. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/api/checkout.py +41 -0
  27. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/docker-compose.yml +20 -0
  28. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/worker/fulfil.py +23 -0
  29. package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.html +186 -0
  30. package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.json +28 -0
  31. package/packs/solution-architect/skills/interactive-diagram/manifest.json +39 -0
  32. package/packs/solution-architect/skills/interactive-diagram/reference.md +151 -0
  33. package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +1682 -0
@@ -0,0 +1,32 @@
1
+ const express = require('express');
2
+ const config = require('../config/default.json');
3
+ const { query } = require('./db');
4
+ const { handleStorageWebhook } = require('./handlers/storageWebhook');
5
+
6
+ const app = express();
7
+
8
+ // Raw body is needed so the storage webhook signature can be checked.
9
+ app.post('/webhooks/storage', express.raw({ type: 'application/json' }), handleStorageWebhook);
10
+
11
+ app.use(express.json());
12
+
13
+ app.get('/feeds/:showId.xml', async (req, res) => {
14
+ const [show] = await query('SELECT id, title, description FROM shows WHERE id = $1', [req.params.showId]);
15
+ if (!show) return res.status(404).end();
16
+ const episodes = await query(
17
+ "SELECT title, audio_url, published_at FROM episodes WHERE show_id = $1 AND status = 'published' ORDER BY published_at DESC",
18
+ [show.id]
19
+ );
20
+ res.type('application/rss+xml').send(renderRss(show, episodes));
21
+ });
22
+
23
+ app.get('/healthz', (req, res) => res.send('ok'));
24
+
25
+ function renderRss(show, episodes) {
26
+ const items = episodes
27
+ .map((e) => `<item><title>${e.title}</title><enclosure url="${e.audio_url}" type="audio/mpeg"/></item>`)
28
+ .join('');
29
+ return `<?xml version="1.0"?><rss version="2.0"><channel><title>${show.title}</title>${items}</channel></rss>`;
30
+ }
31
+
32
+ app.listen(config.port, () => console.log(`api listening on ${config.port}`));
@@ -0,0 +1,32 @@
1
+ const { Worker } = require('bullmq');
2
+ const config = require('../config/default.json');
3
+ const { query, mediaQueue, connection } = require('./db');
4
+ const { transcribeEpisode } = require('./jobs/transcribeEpisode');
5
+
6
+ async function reindexSearch() {
7
+ const rows = await query(
8
+ "SELECT id, title, transcript FROM episodes WHERE status = 'published' AND updated_at > now() - interval '1 day'"
9
+ );
10
+ if (rows.length === 0) return;
11
+ await fetch(`${config.search.url}/indexes/${config.search.index}/documents`, {
12
+ method: 'POST',
13
+ headers: { 'content-type': 'application/json' },
14
+ body: JSON.stringify(rows),
15
+ });
16
+ }
17
+
18
+ const handlers = {
19
+ 'transcribe-episode': transcribeEpisode,
20
+ 'nightly-reindex': reindexSearch,
21
+ };
22
+
23
+ const worker = new Worker('media', (job) => handlers[job.name](job), { connection });
24
+
25
+ worker.on('failed', async (job, err) => {
26
+ console.error(`job ${job.name} ${job.id} failed: ${err.message}`);
27
+ if (job.name === 'transcribe-episode' && job.attemptsMade >= job.opts.attempts) {
28
+ await query("UPDATE episodes SET status = 'failed' WHERE id = $1", [job.data.episodeId]);
29
+ }
30
+ });
31
+
32
+ mediaQueue.add('nightly-reindex', {}, { repeat: { pattern: '0 3 * * *' } });
@@ -0,0 +1,186 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme-default="auto"><head><meta charset="utf-8">
3
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-x8uMUP6tcCdtva9qPkw2RN4FPyZ3BQ4hbm/v2UzDBzA='; style-src 'unsafe-inline'; img-src data: blob:; base-uri 'none'; form-action 'none'">
4
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <!-- One self-contained file: no external scripts, styles, fonts or requests. The policy above makes the browser refuse any. -->
6
+ <title>What a checkout request touches</title><meta name="generator" content="interactive-diagram (skilldrop)"></head>
7
+ <body>
8
+ <header class="top"><h1>What a checkout request touches</h1><p class="q">What happens between a shopper pressing Pay and a courier being booked?</p><p class="sum">The API prices the cart from Redis (falling back to Postgres), charges Stripe, records the order and puts it on a RabbitMQ queue; a separate worker books the courier and marks the order booked. The shopper&#x27;s response returns before the courier is booked.</p><p class="muted" style="font-size:12px;margin:6px 0 0">22 of 22 file citations checked against the repository · 2 items drawn dashed as assumptions</p></header>
9
+ <section class="text" id="outline" aria-label="The diagram as text">
10
+ <details open><summary>The diagram as text</summary><h2>Parts</h2><ul><li><b>Shopper&#x27;s browser</b> <span class="tag warn">assumption</span></li><li><b>Checkout API</b> — FastAPI · POST /checkout <span class="muted">in docker-compose stack</span> <span class="ev">(<code>api/checkout.py:29-41</code> ✓, <code>docker-compose.yml:2-8</code> ✓)</span></li><li><b>Price cache</b> — Redis, 5 min TTL <span class="muted">in Data</span> <span class="ev">(<code>api/checkout.py:20-24</code> ✓, <code>docker-compose.yml:17-18</code> ✓)</span></li><li><b>Orders database</b> — Postgres 16 <span class="muted">in Data</span> <span class="ev">(<code>api/checkout.py:22</code> ✓, <code>api/checkout.py:36</code> ✓, <code>docker-compose.yml:15-16</code> ✓)</span></li><li><b>fulfilment queue</b> — RabbitMQ <span class="muted">in docker-compose stack</span> <span class="ev">(<code>api/checkout.py:39-40</code> ✓, <code>docker-compose.yml:19-20</code> ✓)</span></li><li><b>Fulfilment worker</b> <span class="muted">in docker-compose stack</span> <span class="ev">(<code>worker/fulfil.py:13-19</code> ✓, <code>docker-compose.yml:9-14</code> ✓)</span></li><li><b>Stripe</b> — PaymentIntent <span class="ev">(<code>api/checkout.py:32-35</code> ✓)</span></li><li><b>Courier API</b> — courier.example <span class="ev">(<code>worker/fulfil.py:16</code> ✓)</span></li></ul><h2>Connections</h2><ul><li>Shopper&#x27;s browser → Checkout API: POST /checkout <span class="tag warn">assumption</span></li><li>Checkout API → Price cache: get price:&lt;sku&gt; <span class="ev">(<code>api/checkout.py:20</code> ✓)</span></li><li>Checkout API → Orders database: price on cache miss, insert order <span class="ev">(<code>api/checkout.py:22</code> ✓, <code>api/checkout.py:36-38</code> ✓)</span></li><li>Checkout API → Stripe: create + confirm payment <span class="ev">(<code>api/checkout.py:32-35</code> ✓)</span></li><li>Checkout API → fulfilment queue: publish order_id <span class="ev">(<code>api/checkout.py:39-40</code> ✓)</span></li><li>fulfilment queue → Fulfilment worker: consume <span class="ev">(<code>worker/fulfil.py:22</code> ✓)</span></li><li>Fulfilment worker → Courier API: book courier <span class="ev">(<code>worker/fulfil.py:16</code> ✓)</span></li><li>Fulfilment worker → Orders database: read address, mark booked <span class="ev">(<code>worker/fulfil.py:15</code> ✓, <code>worker/fulfil.py:17</code> ✓)</span></li></ul><h2>Walkthrough</h2><ol><li>The shopper&#x27;s browser posts the cart to the Checkout API (the client itself is not in this repo).</li><li>Each line is priced from Redis; a miss reads Postgres and caches the price for five minutes.</li><li>Stripe is charged and confirmed in the same call; anything but &#x27;succeeded&#x27; returns 402.</li><li>The order is inserted, then its id is published to the fulfilment queue, and the API answers the shopper.</li><li>Later, the worker takes the id, reads the address, books the courier and marks the order booked.</li></ol><h2>Notes</h2><ul><li>The worker reads orders.address, but the API&#x27;s insert does not write an address. Where it comes from is not in this repo.</li><li>Left out: the products table&#x27;s other readers, and anything after the courier booking.</li></ul></details></section>
11
+ <nav class="bar" aria-label="Diagram controls">
12
+ <input id="find" type="search" placeholder="Find a part…" aria-label="Find a part">
13
+ <button id="zin" title="Zoom in">+</button><button id="zout" title="Zoom out">−</button><button id="zfit" title="Fit to screen">Fit</button>
14
+ <span class="sep"></span><button id="tour">Walk through</button><button id="prev" title="Previous step">←</button><button id="next" title="Next step">→</button>
15
+ <span class="sep"></span><button id="xsvg">SVG</button><button id="xpng">PNG</button><button id="xmmd" title="Copy and download Mermaid">Mermaid</button>
16
+ <button id="theme" title="Light or dark">◐</button>
17
+ <span class="hint">Click a part for detail and evidence · shift-click a second part for the path · drag to pan · scroll to zoom</span></nav>
18
+ <div class="legend"><span><i></i>connection</span><span><i class="d"></i>async / optional</span><span><i class="a"></i>assumption (not shown by the evidence)</span></div>
19
+ <div class="caption" id="caption" aria-live="polite"></div>
20
+ <div class="wrap"><div id="stage"><svg id="map" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1177 406" width="1177" height="406" role="group" aria-label="What a checkout request touches"><defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" class="arrowhead"/></marker><marker id="arrow-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" class="arrowhead"/></marker></defs><rect class="canvas" x="0" y="0" width="1177" height="406"/><g class="group d1" data-group="compose"><rect x="223.0" y="114.0" width="914.0" height="252.0" rx="10"/><text class="glabel" x="235.0" y="132.0">docker-compose stack</text></g><g class="group d2" data-group="data"><rect x="506.0" y="230.0" width="613.0" height="118.0" rx="10"/><text class="glabel" x="518.0" y="248.0">Data</text></g><g class="edge s-solid assumed emph" id="e-pay" data-id="pay" data-from="shopper" data-to="api"><title>POST /checkout</title><path class="hit" d="M187.0,57.0 L216.0,57.0 Q223.0,57.0 223.0,64.0 L223.0,166.0 Q223.0,173.0 230.0,173.0 L259.0,173.0"/><path class="line" d="M187.0,57.0 L216.0,57.0 Q223.0,57.0 223.0,64.0 L223.0,166.0 Q223.0,173.0 230.0,173.0 L259.0,173.0" marker-end="url(#arrow)"/><rect class="lbg" x="173.7" y="82.8" width="98.7" height="18.0" rx="3"/><text class="elabel" x="223.0" y="95.8" text-anchor="middle">POST /checkout</text></g><g class="edge s-solid" id="e-price" data-id="price" data-from="api" data-to="cache"><title>get price:&lt;sku&gt;</title><path class="hit" d="M446.0,178.0 L479.5,178.0 Q486.5,178.0 486.5,185.0 L486.5,289.0 Q486.5,296.0 493.5,296.0 L524.0,296.0"/><path class="line" d="M446.0,178.0 L479.5,178.0 Q486.5,178.0 486.5,185.0 L486.5,289.0 Q486.5,296.0 493.5,296.0 L524.0,296.0" marker-end="url(#arrow)"/><rect class="lbg" x="438.9" y="204.4" width="95.2" height="18.0" rx="3"/><text class="elabel" x="486.5" y="217.4" text-anchor="middle">get price:&lt;sku&gt;</text></g><g class="edge s-solid" id="e-miss" data-id="miss" data-from="api" data-to="db"><title>price on cache miss, insert order</title><path class="hit" d="M446.0,188.1 L470.5,188.1 Q477.5,188.1 477.5,195.1 L477.5,208.0 Q477.5,215.0 484.5,215.0 L920.5,215.0 Q927.5,215.0 927.5,222.0 L927.5,270.0 Q927.5,277.0 934.5,277.0 L959.0,277.0"/><path class="line" d="M446.0,188.1 L470.5,188.1 Q477.5,188.1 477.5,195.1 L477.5,208.0 Q477.5,215.0 484.5,215.0 L920.5,215.0 Q927.5,215.0 927.5,222.0 L927.5,270.0 Q927.5,277.0 934.5,277.0 L959.0,277.0" marker-end="url(#arrow)"/><rect class="lbg" x="729.4" y="199.0" width="126.2" height="32.0" rx="3"/><text class="elabel" x="792.5" y="212.0" text-anchor="middle">price on cache miss,</text><text class="elabel" x="792.5" y="226.0" text-anchor="middle">insert order</text></g><g class="edge s-solid emph" id="e-charge" data-id="charge" data-from="api" data-to="stripe"><title>create + confirm payment</title><path class="hit" d="M446.0,157.9 L470.5,157.9 Q477.5,157.9 477.5,150.9 L477.5,64.0 Q477.5,57.0 484.5,57.0 L529.0,57.0"/><path class="line" d="M446.0,157.9 L470.5,157.9 Q477.5,157.9 477.5,150.9 L477.5,64.0 Q477.5,57.0 484.5,57.0 L529.0,57.0" marker-end="url(#arrow)"/><rect class="lbg" x="397.9" y="98.4" width="159.3" height="18.0" rx="3"/><text class="elabel" x="477.5" y="111.4" text-anchor="middle">create + confirm payment</text></g><g class="edge s-dashed" id="e-publish" data-id="publish" data-from="api" data-to="broker"><title>publish order_id</title><path class="hit" d="M446.0,168.0 L475.0,168.0 Q477.5,168.0 477.5,170.5 L477.5,170.5 Q477.5,173.0 480.0,173.0 L518.0,173.0"/><path class="line" d="M446.0,168.0 L475.0,168.0 Q477.5,168.0 477.5,170.5 L477.5,170.5 Q477.5,173.0 480.0,173.0 L518.0,173.0" marker-end="url(#arrow)"/><rect class="lbg" x="448.5" y="164.0" width="98.5" height="18.0" rx="3"/><text class="elabel" x="497.8" y="177.0" text-anchor="middle">publish order_id</text></g><g class="edge s-dashed" id="e-consume" data-id="consume" data-from="broker" data-to="worker"><title>consume</title><path class="hit" d="M660.0,173.0 L689.0,173.0 Q696.0,173.0 703.0,173.0 L732.0,173.0"/><path class="line" d="M660.0,173.0 L689.0,173.0 Q696.0,173.0 703.0,173.0 L732.0,173.0" marker-end="url(#arrow)"/><rect class="lbg" x="662.7" y="164.0" width="55.8" height="18.0" rx="3"/><text class="elabel" x="690.6" y="177.0" text-anchor="middle">consume</text></g><g class="edge s-solid" id="e-book" data-id="book" data-from="worker" data-to="courier"><title>book courier</title><path class="hit" d="M887.0,159.0 L911.5,159.0 Q918.5,159.0 918.5,152.0 L918.5,64.0 Q918.5,57.0 925.5,57.0 L970.0,57.0"/><path class="line" d="M887.0,159.0 L911.5,159.0 Q918.5,159.0 918.5,152.0 L918.5,64.0 Q918.5,57.0 925.5,57.0 L970.0,57.0" marker-end="url(#arrow)"/><rect class="lbg" x="878.6" y="99.0" width="79.8" height="18.0" rx="3"/><text class="elabel" x="918.5" y="112.0" text-anchor="middle">book courier</text></g><g class="edge s-solid" id="e-mark" data-id="mark" data-from="worker" data-to="db"><title>read address, mark booked</title><path class="hit" d="M887.0,187.0 L911.5,187.0 Q918.5,187.0 918.5,194.0 L918.5,308.0 Q918.5,315.0 925.5,315.0 L959.0,315.0"/><path class="line" d="M887.0,187.0 L911.5,187.0 Q918.5,187.0 918.5,194.0 L918.5,308.0 Q918.5,315.0 925.5,315.0 L959.0,315.0" marker-end="url(#arrow)"/><rect class="lbg" x="858.7" y="209.4" width="119.6" height="32.0" rx="3"/><text class="elabel" x="918.5" y="222.4" text-anchor="middle">read address, mark</text><text class="elabel" x="918.5" y="236.4" text-anchor="middle">booked</text></g><g class="node c-violet k-client assumed" id="n-shopper" data-id="shopper" tabindex="0" role="button" aria-label="Shopper&#x27;s browser (assumption)"><title>Shopper&#x27;s browser</title><rect class="shape" x="30.0" y="32.0" width="157.0" height="50.0" rx="6"/><text class="label" x="108.5" y="61.5" text-anchor="middle">Shopper&#x27;s browser</text><text class="badge warn" x="182.0" y="28.0" text-anchor="end">assumed</text></g><g class="node c-blue k-service" id="n-api" data-id="api" tabindex="0" role="button" aria-label="Checkout API"><title>Checkout API — FastAPI · POST /checkout</title><rect class="shape" x="259.0" y="146.0" width="187.0" height="54.0" rx="6"/><text class="label" x="352.5" y="170.0" text-anchor="middle">Checkout API</text><text class="note" x="352.5" y="186.0" text-anchor="middle">FastAPI · POST /checkout</text><text class="badge" x="441.0" y="142.0" text-anchor="end">✓ src</text></g><g class="node c-teal k-cache" id="n-cache" data-id="cache" tabindex="0" role="button" aria-label="Price cache"><title>Price cache — Redis, 5 min TTL</title><path class="shape" d="M524.0,269.0 a65.0,7 0 0,1 130.0,0 v54.0 a65.0,7 0 0,1 -130.0,0 z"/><path class="shape-line" d="M524.0,269.0 a65.0,7 0 0,0 130.0,0"/><text class="label" x="589.0" y="297.0" text-anchor="middle">Price cache</text><text class="note" x="589.0" y="313.0" text-anchor="middle">Redis, 5 min TTL</text><text class="badge" x="649.0" y="258.0" text-anchor="end">✓ src</text></g><g class="node c-green k-database" id="n-db" data-id="db" tabindex="0" role="button" aria-label="Orders database"><title>Orders database — Postgres 16</title><path class="shape" d="M959.0,269.0 a71.0,7 0 0,1 142.0,0 v54.0 a71.0,7 0 0,1 -142.0,0 z"/><path class="shape-line" d="M959.0,269.0 a71.0,7 0 0,0 142.0,0"/><text class="label" x="1030.0" y="297.0" text-anchor="middle">Orders database</text><text class="note" x="1030.0" y="313.0" text-anchor="middle">Postgres 16</text><text class="badge" x="1096.0" y="258.0" text-anchor="end">✓ src</text></g><g class="node c-amber k-queue" id="n-broker" data-id="broker" tabindex="0" role="button" aria-label="fulfilment queue"><title>fulfilment queue — RabbitMQ</title><rect class="shape" x="518.0" y="146.0" width="142.0" height="54.0" rx="10"/><path class="shape-line" d="M528.0,149.0 v48.0 M650.0,149.0 v48.0"/><text class="label" x="589.0" y="170.0" text-anchor="middle">fulfilment queue</text><text class="note" x="589.0" y="186.0" text-anchor="middle">RabbitMQ</text><text class="badge" x="655.0" y="142.0" text-anchor="end">✓ src</text></g><g class="node c-indigo k-worker" id="n-worker" data-id="worker" tabindex="0" role="button" aria-label="Fulfilment worker"><title>Fulfilment worker</title><rect class="shape" x="732.0" y="148.0" width="155.0" height="50.0" rx="6"/><text class="label" x="809.5" y="177.5" text-anchor="middle">Fulfilment worker</text><text class="badge" x="882.0" y="144.0" text-anchor="end">✓ src</text></g><g class="node c-slate k-external" id="n-stripe" data-id="stripe" tabindex="0" role="button" aria-label="Stripe"><title>Stripe — PaymentIntent</title><rect class="shape" x="529.0" y="30.0" width="120.0" height="54.0" rx="4"/><text class="label" x="589.0" y="54.0" text-anchor="middle">Stripe</text><text class="note" x="589.0" y="70.0" text-anchor="middle">PaymentIntent</text><text class="badge" x="644.0" y="26.0" text-anchor="end">✓ src</text></g><g class="node c-slate k-external" id="n-courier" data-id="courier" tabindex="0" role="button" aria-label="Courier API"><title>Courier API — courier.example</title><rect class="shape" x="970.0" y="30.0" width="120.0" height="54.0" rx="4"/><text class="label" x="1030.0" y="54.0" text-anchor="middle">Courier API</text><text class="note" x="1030.0" y="70.0" text-anchor="middle">courier.example</text><text class="badge" x="1085.0" y="26.0" text-anchor="end">✓ src</text></g></svg></div><aside id="panel" aria-live="polite"></aside></div>
21
+ <style id="diagram-css">
22
+ :root{--bg:#f7f8fa;--panel:#ffffff;--ink:#16181d;--muted:#5b6170;--line:#d5d9e0;--edge:#6b7385;--accent:#2563eb;
23
+ --warn:#b45309;--ok:#15803d;--bad:#b91c1c;--hl:#f59e0b;--canvas:#fbfcfd;--group:#eef1f6;--group2:#e6eaf2;--shadow:0 1px 2px rgba(0,0,0,.06)}
24
+ :root[data-theme=dark]{--bg:#0f1115;--panel:#171a21;--ink:#e7e9ee;--muted:#9aa1b2;--line:#2b303b;--edge:#8b93a7;--accent:#60a5fa;
25
+ --warn:#f59e0b;--ok:#4ade80;--bad:#f87171;--hl:#fbbf24;--canvas:#12151b;--group:#1a1e27;--group2:#20252f;--shadow:none}
26
+ *{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
27
+ body{display:flex;flex-direction:column}body>*{flex:none}section.text{order:10}
28
+ header.top{padding:18px 24px 10px;max-width:1400px}header.top h1{margin:0 0 4px;font-size:21px}
29
+ .q{color:var(--muted);margin:0 0 6px}.sum{margin:0;max-width:900px}
30
+ .bar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:8px 24px;position:sticky;top:0;background:var(--bg);z-index:5;border-bottom:1px solid var(--line)}
31
+ .bar button,.bar input{font:inherit;font-size:13px;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:6px;padding:4px 9px;cursor:pointer}
32
+ .bar input{cursor:text;width:180px}.bar button:hover{border-color:var(--accent)}.bar .sep{width:1px;height:20px;background:var(--line);margin:0 4px}
33
+ .bar .hint{color:var(--muted);font-size:12px;margin-left:auto}
34
+ .wrap{display:flex;gap:0;height:calc(100vh - 150px);min-height:420px;padding:10px 24px 0}
35
+ #stage{flex:1;position:relative;overflow:hidden;border:1px solid var(--line);border-radius:10px;background:var(--canvas);touch-action:none;cursor:grab}
36
+ #stage.drag{cursor:grabbing}#stage svg{position:absolute;left:0;top:0;width:100%;height:100%}
37
+ aside#panel{width:340px;margin-left:12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:auto;padding:14px 16px;display:none}
38
+ aside#panel.open{display:block}aside h3{margin:0 0 2px;font-size:16px}aside .kind{color:var(--muted);font-size:12px;margin-bottom:8px}
39
+ aside .close{float:right;border:0;background:none;color:var(--muted);font-size:18px;cursor:pointer}
40
+ aside pre{background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:8px;overflow:auto;font-size:12px;max-height:260px}
41
+ aside ul{padding-left:18px;margin:4px 0}aside a.jump{color:var(--accent);cursor:pointer;text-decoration:none}
42
+ aside .acts button{font:inherit;font-size:12px;margin:2px 4px 2px 0;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:5px;padding:3px 7px;cursor:pointer}
43
+ .caption{display:none;margin:8px 24px 0;padding:8px 12px;border-left:3px solid var(--hl);background:var(--panel);border-radius:4px}
44
+ .caption.on{display:block}
45
+ section.text{max-width:1100px;padding:10px 24px 40px}section.text h2{font-size:15px;margin:18px 0 4px}
46
+ .tag{font-size:11px;padding:0 5px;border-radius:4px;border:1px solid var(--line);margin-left:4px}.tag.warn{color:var(--warn);border-color:var(--warn)}
47
+ .tag.st-added{color:var(--ok);border-color:var(--ok)}.tag.st-removed{color:var(--bad);border-color:var(--bad)}.tag.st-changed{color:var(--accent);border-color:var(--accent)}
48
+ .muted{color:var(--muted)}.ev{color:var(--muted);font-size:12px}code{font-size:12px}
49
+ .legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:6px 24px 0}
50
+ .legend i{display:inline-block;width:22px;height:0;border-top:2px solid var(--edge);vertical-align:middle;margin-right:5px}
51
+ .legend i.d{border-top-style:dashed}.legend i.a{border-top:2px dashed var(--warn)}
52
+ .issues{margin:8px 24px 0;padding:8px 12px;border:1px solid var(--warn);border-radius:6px;color:var(--warn);font-size:13px}
53
+ /* diagram */
54
+ .canvas{fill:var(--canvas)}
55
+ .group rect{fill:var(--group);stroke:var(--line);stroke-width:1}.group.d2 rect{fill:var(--group2)}.group.d3 rect{fill:var(--group)}
56
+ .glabel{font-size:12px;font-weight:600;fill:var(--muted);letter-spacing:.02em}
57
+ .group.st-removed rect{stroke:var(--bad);stroke-dasharray:6 4;fill:none}
58
+ .node{cursor:pointer;outline:none}.node .shape{stroke-width:1.5;fill:var(--panel)}.node .shape.solid{fill:var(--ink);stroke:var(--ink)}
59
+ .node .shape-line{fill:none;stroke-width:1.2}.node .glyph{fill:var(--muted)}
60
+ .node .label{font-size:13px;font-weight:600;fill:var(--ink)}.node .note{font-size:11px;fill:var(--muted)}
61
+ .node .badge{font-size:10px;fill:var(--ok)}.node .badge.warn{fill:var(--warn)}
62
+ .table-head{fill:var(--group2)}.fname{font-size:12px;fill:var(--ink)}.ftype{font-size:11px;fill:var(--muted)}.fkey{font-size:9px;font-weight:700;fill:var(--warn)}
63
+ .c-blue .shape,.c-blue .shape-line{stroke:#3b82f6}.c-violet .shape{stroke:#8b5cf6}.c-green .shape,.c-green .shape-line{stroke:#16a34a}
64
+ .c-teal .shape,.c-teal .shape-line{stroke:#0d9488}.c-amber .shape,.c-amber .shape-line{stroke:#d97706}.c-slate .shape{stroke:#64748b}
65
+ .c-rose .shape{stroke:#e11d48}.c-indigo .shape{stroke:#4f46e5}.k-external .shape{stroke-dasharray:5 3}
66
+ .node.assumed .shape{stroke:var(--warn)!important;stroke-dasharray:6 4}
67
+ .node.st-added .shape{stroke:var(--ok)!important;stroke-width:2.5}.node.st-changed .shape{stroke:var(--accent)!important;stroke-width:2.5}
68
+ .node.st-removed{opacity:.55}.node.st-removed .shape{stroke:var(--bad)!important;stroke-dasharray:6 4}.node.st-removed .label{text-decoration:line-through}
69
+ .edge .line{fill:none;stroke:var(--edge);stroke-width:1.5}.edge .hit{fill:none;stroke:transparent;stroke-width:12;cursor:pointer}
70
+ .edge.s-dashed .line{stroke-dasharray:7 5}.edge.s-dotted .line{stroke-dasharray:2 4}.edge.emph .line{stroke-width:2.6}
71
+ .edge.assumed .line{stroke:var(--warn);stroke-dasharray:6 4}.edge.st-added .line{stroke:var(--ok);stroke-width:2.2}
72
+ .edge.st-removed .line{stroke:var(--bad);stroke-dasharray:6 4;opacity:.7}
73
+ .arrowhead{fill:var(--edge)}.lbg{fill:var(--canvas);opacity:.92}.elabel{font-size:11px;fill:var(--muted)}.card{font-size:11px;font-weight:700;fill:var(--muted)}
74
+ .lifeline{stroke:var(--line);stroke-width:1.2;stroke-dasharray:4 4}.seqno{font-size:10px;fill:var(--muted)}
75
+ .frame rect{fill:none;stroke:var(--muted);stroke-width:1}.frame .ftab{fill:var(--group2);stroke:var(--muted)}.frame text{font-size:11px;fill:var(--ink);font-weight:600}
76
+ svg.focus .node,svg.focus .edge,svg.focus .group{opacity:.18;transition:opacity .15s}
77
+ svg.focus .on,svg.focus .group.on{opacity:1}svg .node.on .shape{stroke-width:2.6}svg .edge.on .line{stroke:var(--hl);stroke-width:2.6}
78
+ svg .node.sel .shape{stroke:var(--hl)!important;stroke-width:3}svg .node:focus-visible .shape{stroke:var(--accent)!important;stroke-width:3}
79
+ @media (prefers-reduced-motion:no-preference){svg .edge.on.flow .line{stroke-dasharray:8 6;animation:flow 1s linear infinite}}
80
+ @keyframes flow{to{stroke-dashoffset:-28}}
81
+ @media print{.bar,aside,.caption{display:none!important}.wrap{height:auto}#stage{border:0}#stage svg{position:static;height:auto}}
82
+ </style>
83
+ <script type="application/json" id="diagram-data">{"type": "architecture", "slug": "what-a-checkout-request-touches", "source_url": null, "story": [{"text": "The shopper's browser posts the cart to the Checkout API (the client itself is not in this repo).", "edges": ["pay"]}, {"text": "Each line is priced from Redis; a miss reads Postgres and caches the price for five minutes.", "edges": ["price", "miss"]}, {"text": "Stripe is charged and confirmed in the same call; anything but 'succeeded' returns 402.", "edges": ["charge"]}, {"text": "The order is inserted, then its id is published to the fulfilment queue, and the API answers the shopper.", "edges": ["miss", "publish"]}, {"text": "Later, the worker takes the id, reads the address, books the courier and marks the order booked.", "edges": ["consume", "book", "mark"]}], "nodes": [{"id": "shopper", "label": "Shopper's browser", "kind": "client", "note": null, "detail": "Nothing in this repo shows the client; the API's POST /checkout is the entry point.", "fields": [], "evidence": [], "assumed": true, "status": null, "group_label": null}, {"id": "api", "label": "Checkout API", "kind": "service", "note": "FastAPI · POST /checkout", "detail": null, "fields": [], "evidence": [{"file": "api/checkout.py", "start": 29, "end": 41, "quote": "def checkout", "verified": true, "snippet": "@app.post(\"/checkout\")\ndef checkout(cart: dict):\n total = price_cart(cart)\n intent = stripe.PaymentIntent.create(amount=total, currency=\"gbp\", confirm=True,\n payment_method=cart[\"payment_method\"])\n if intent.status != \"succeeded\":\n raise HTTPException(402, \"payment declined\")\n order_id = db.execute(\"insert into orders (email, total, payment_id) values (%s, %s, %s) returning id\",\n (cart[\"email\"], total, intent.id)).fetchone()[0]\n db.commit()\n broker.basic_publish(exchange=\"\", routing_key=\"fulfilment\",\n body=json.dumps({\"order_id\": order_id}))\n return {\"order_id\": order_id}"}, {"file": "docker-compose.yml", "start": 2, "end": 8, "verified": true, "snippet": " api:\n build: ./api\n environment:\n DATABASE_URL: postgresql://shop@db/shop\n REDIS_URL: redis://cache:6379/0\n BROKER_URL: amqp://broker\n depends_on: [db, cache, broker]"}], "assumed": false, "status": null, "group_label": "docker-compose stack"}, {"id": "cache", "label": "Price cache", "kind": "cache", "note": "Redis, 5 min TTL", "detail": null, "fields": [], "evidence": [{"file": "api/checkout.py", "start": 20, "end": 24, "quote": "ex=300", "verified": true, "snippet": " cached = prices.get(f\"price:{line['sku']}\")\n if cached is None:\n row = db.execute(\"select price from products where sku = %s\", (line[\"sku\"],)).fetchone()\n cached = row[0]\n prices.set(f\"price:{line['sku']}\", cached, ex=300)"}, {"file": "docker-compose.yml", "start": 17, "end": 18, "verified": true, "snippet": " cache:\n image: redis:7"}], "assumed": false, "status": null, "group_label": "Data"}, {"id": "db", "label": "Orders database", "kind": "database", "note": "Postgres 16", "detail": null, "fields": [], "evidence": [{"file": "api/checkout.py", "start": 22, "end": 22, "quote": "from products", "verified": true, "snippet": " row = db.execute(\"select price from products where sku = %s\", (line[\"sku\"],)).fetchone()"}, {"file": "api/checkout.py", "start": 36, "end": 36, "quote": "insert into orders", "verified": true, "snippet": " order_id = db.execute(\"insert into orders (email, total, payment_id) values (%s, %s, %s) returning id\","}, {"file": "docker-compose.yml", "start": 15, "end": 16, "verified": true, "snippet": " db:\n image: postgres:16"}], "assumed": false, "status": null, "group_label": "Data"}, {"id": "broker", "label": "fulfilment queue", "kind": "queue", "note": "RabbitMQ", "detail": null, "fields": [], "evidence": [{"file": "api/checkout.py", "start": 39, "end": 40, "quote": "routing_key=\"fulfilment\"", "verified": true, "snippet": " broker.basic_publish(exchange=\"\", routing_key=\"fulfilment\",\n body=json.dumps({\"order_id\": order_id}))"}, {"file": "docker-compose.yml", "start": 19, "end": 20, "verified": true, "snippet": " broker:\n image: rabbitmq:3"}], "assumed": false, "status": null, "group_label": "docker-compose stack"}, {"id": "worker", "label": "Fulfilment worker", "kind": "worker", "note": null, "detail": null, "fields": [], "evidence": [{"file": "worker/fulfil.py", "start": 13, "end": 19, "quote": "def on_message", "verified": true, "snippet": "def on_message(ch, method, properties, body):\n order_id = json.loads(body)[\"order_id\"]\n address = db.execute(\"select address from orders where id = %s\", (order_id,)).fetchone()[0]\n booking = requests.post(\"https://courier.example/v2/bookings\", json={\"ref\": order_id, \"to\": address}, timeout=10)\n db.execute(\"update orders set status = 'booked', courier_ref = %s where id = %s\", (booking.json()[\"ref\"], order_id))\n db.commit()\n ch.basic_ack(method.delivery_tag)"}, {"file": "docker-compose.yml", "start": 9, "end": 14, "verified": true, "snippet": " worker:\n build: ./worker\n environment:\n DATABASE_URL: postgresql://shop@db/shop\n BROKER_URL: amqp://broker\n depends_on: [db, broker]"}], "assumed": false, "status": null, "group_label": "docker-compose stack"}, {"id": "stripe", "label": "Stripe", "kind": "external", "note": "PaymentIntent", "detail": null, "fields": [], "evidence": [{"file": "api/checkout.py", "start": 32, "end": 35, "quote": "stripe.PaymentIntent.create", "verified": true, "snippet": " intent = stripe.PaymentIntent.create(amount=total, currency=\"gbp\", confirm=True,\n payment_method=cart[\"payment_method\"])\n if intent.status != \"succeeded\":\n raise HTTPException(402, \"payment declined\")"}], "assumed": false, "status": null, "group_label": null}, {"id": "courier", "label": "Courier API", "kind": "external", "note": "courier.example", "detail": null, "fields": [], "evidence": [{"file": "worker/fulfil.py", "start": 16, "end": 16, "quote": "courier.example/v2/bookings", "verified": true, "snippet": " booking = requests.post(\"https://courier.example/v2/bookings\", json={\"ref\": order_id, \"to\": address}, timeout=10)"}], "assumed": false, "status": null, "group_label": null}], "edges": [{"id": "pay", "from": "shopper", "to": "api", "label": "POST /checkout", "detail": null, "evidence": [], "assumed": true, "status": null}, {"id": "price", "from": "api", "to": "cache", "label": "get price:\u003csku\u003e", "detail": null, "evidence": [{"file": "api/checkout.py", "start": 20, "end": 20, "verified": true, "snippet": " cached = prices.get(f\"price:{line['sku']}\")"}], "assumed": false, "status": null}, {"id": "miss", "from": "api", "to": "db", "label": "price on cache miss, insert order", "detail": null, "evidence": [{"file": "api/checkout.py", "start": 22, "end": 22, "verified": true, "snippet": " row = db.execute(\"select price from products where sku = %s\", (line[\"sku\"],)).fetchone()"}, {"file": "api/checkout.py", "start": 36, "end": 38, "verified": true, "snippet": " order_id = db.execute(\"insert into orders (email, total, payment_id) values (%s, %s, %s) returning id\",\n (cart[\"email\"], total, intent.id)).fetchone()[0]\n db.commit()"}], "assumed": false, "status": null}, {"id": "charge", "from": "api", "to": "stripe", "label": "create + confirm payment", "detail": null, "evidence": [{"file": "api/checkout.py", "start": 32, "end": 35, "verified": true, "snippet": " intent = stripe.PaymentIntent.create(amount=total, currency=\"gbp\", confirm=True,\n payment_method=cart[\"payment_method\"])\n if intent.status != \"succeeded\":\n raise HTTPException(402, \"payment declined\")"}], "assumed": false, "status": null}, {"id": "publish", "from": "api", "to": "broker", "label": "publish order_id", "detail": null, "evidence": [{"file": "api/checkout.py", "start": 39, "end": 40, "verified": true, "snippet": " broker.basic_publish(exchange=\"\", routing_key=\"fulfilment\",\n body=json.dumps({\"order_id\": order_id}))"}], "assumed": false, "status": null}, {"id": "consume", "from": "broker", "to": "worker", "label": "consume", "detail": null, "evidence": [{"file": "worker/fulfil.py", "start": 22, "end": 22, "quote": "basic_consume", "verified": true, "snippet": "channel.basic_consume(queue=\"fulfilment\", on_message_callback=on_message)"}], "assumed": false, "status": null}, {"id": "book", "from": "worker", "to": "courier", "label": "book courier", "detail": null, "evidence": [{"file": "worker/fulfil.py", "start": 16, "end": 16, "verified": true, "snippet": " booking = requests.post(\"https://courier.example/v2/bookings\", json={\"ref\": order_id, \"to\": address}, timeout=10)"}], "assumed": false, "status": null}, {"id": "mark", "from": "worker", "to": "db", "label": "read address, mark booked", "detail": null, "evidence": [{"file": "worker/fulfil.py", "start": 15, "end": 15, "verified": true, "snippet": " address = db.execute(\"select address from orders where id = %s\", (order_id,)).fetchone()[0]"}, {"file": "worker/fulfil.py", "start": 17, "end": 17, "verified": true, "snippet": " db.execute(\"update orders set status = 'booked', courier_ref = %s where id = %s\", (booking.json()[\"ref\"], order_id))"}], "assumed": false, "status": null}], "mermaid": "flowchart LR\n shopper[\"Shopper's browser\"]\n stripe[\"Stripe\"]\n courier[\"Courier API\"]\n subgraph compose[\"docker-compose stack\"]\n api[\"Checkout API\"]\n broker[[\"fulfilment queue\"]]\n worker[\"Fulfilment worker\"]\n subgraph data[\"Data\"]\n cache[(\"Price cache\")]\n db[(\"Orders database\")]\n end\n end\n shopper -.-\u003e|\"POST /checkout\"| api\n api --\u003e|\"get price:\u003csku\u003e\"| cache\n api --\u003e|\"price on cache miss, insert order\"| db\n api ==\u003e|\"create + confirm payment\"| stripe\n api -.-\u003e|\"publish order_id\"| broker\n broker -.-\u003e|\"consume\"| worker\n worker --\u003e|\"book courier\"| courier\n worker --\u003e|\"read address, mark booked\"| db\n classDef assumed stroke-dasharray: 5 4\n class shopper assumed\n"}</script>
84
+ <script>
85
+ (function(){
86
+ var DATA=JSON.parse(document.getElementById('diagram-data').textContent);
87
+ var svg=document.getElementById('map'),stage=document.getElementById('stage'),panel=document.getElementById('panel');
88
+ var root=document.documentElement;
89
+ var pref=localStorage.getItem('idg-theme')||root.getAttribute('data-theme-default');
90
+ if(pref==='auto'||!pref){pref=window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}
91
+ root.setAttribute('data-theme',pref);
92
+ var W=+svg.getAttribute('width'),H=+svg.getAttribute('height');svg.removeAttribute('width');svg.removeAttribute('height');
93
+ var vb={x:0,y:0,w:W,h:H};
94
+ function apply(){svg.setAttribute('viewBox',vb.x+' '+vb.y+' '+vb.w+' '+vb.h)}
95
+ function fit(){var r=stage.getBoundingClientRect();var s=Math.max(W/r.width,H/r.height,0.6);vb.w=r.width*s;vb.h=r.height*s;vb.x=(W-vb.w)/2;vb.y=(H-vb.h)/2;apply()}
96
+ function zoom(f,cx,cy){var r=stage.getBoundingClientRect();if(cx==null){cx=r.width/2;cy=r.height/2}
97
+ var px=vb.x+cx/r.width*vb.w,py=vb.y+cy/r.height*vb.h;var nw=Math.min(Math.max(vb.w*f,120),W*6);var k=nw/vb.w;
98
+ vb.x=px-(px-vb.x)*k;vb.y=py-(py-vb.y)*k;vb.w=nw;vb.h=vb.h*k;apply()}
99
+ stage.addEventListener('wheel',function(e){e.preventDefault();var r=stage.getBoundingClientRect();zoom(e.deltaY>0?1.12:1/1.12,e.clientX-r.left,e.clientY-r.top)},{passive:false});
100
+ var drag=null;
101
+ stage.addEventListener('pointerdown',function(e){if(e.target.closest('.node')||e.target.closest('.edge'))return;drag={x:e.clientX,y:e.clientY,vx:vb.x,vy:vb.y};stage.classList.add('drag');stage.setPointerCapture(e.pointerId)});
102
+ stage.addEventListener('pointermove',function(e){if(!drag)return;var r=stage.getBoundingClientRect();vb.x=drag.vx-(e.clientX-drag.x)*vb.w/r.width;vb.y=drag.vy-(e.clientY-drag.y)*vb.h/r.height;apply()});
103
+ stage.addEventListener('pointerup',function(){drag=null;stage.classList.remove('drag')});
104
+ // Lookups are Maps, never plain objects: ids come from the spec and the URL, and a key such as
105
+ // __proto__ must stay a key.
106
+ var nodes=new Map(),edges=new Map(),outE=new Map(),inE=new Map();
107
+ DATA.nodes.forEach(function(n){nodes.set(n.id,n);outE.set(n.id,[]);inE.set(n.id,[])});
108
+ DATA.edges.forEach(function(e){edges.set(e.id,e);if(outE.has(e.from))outE.get(e.from).push(e.id);if(inE.has(e.to))inE.get(e.to).push(e.id)});
109
+ function el(kind,id){return document.getElementById(kind+'-'+id)}
110
+ function clear(){svg.classList.remove('focus');svg.querySelectorAll('.on,.sel,.flow').forEach(function(x){x.classList.remove('on','sel','flow')})}
111
+ function light(ns,es,flow){clear();svg.classList.add('focus');ns.forEach(function(i){var x=el('n',i);x&&x.classList.add('on')});
112
+ es.forEach(function(i){var x=el('e',i);if(x){x.classList.add('on');flow&&x.classList.add('flow')}})}
113
+ function reach(id,dir){if(!nodes.has(id))return {n:[],e:[]};var seen=new Set([id]),es=[],q=[id];while(q.length){var u=q.shift();(dir>0?outE.get(u):inE.get(u)).forEach(function(eid){var e=edges.get(eid);es.push(eid);var v=dir>0?e.to:e.from;if(!seen.has(v)){seen.add(v);q.push(v)}})}
114
+ return {n:Array.from(seen),e:es}}
115
+ function path(a,b,directed){if(!nodes.has(a)||!nodes.has(b))return null;var prev=new Map([[a,null]]),q=[a];while(q.length){var u=q.shift();if(u===b)break;
116
+ var nb=outE.get(u).map(function(i){return [edges.get(i).to,i]});if(!directed)nb=nb.concat(inE.get(u).map(function(i){return [edges.get(i).from,i]}));
117
+ nb.forEach(function(p){if(!prev.has(p[0])){prev.set(p[0],[u,p[1]]);q.push(p[0])}})}
118
+ if(!prev.has(b))return null;var ns=[b],es=[],c=b;while(prev.get(c)){es.push(prev.get(c)[1]);c=prev.get(c)[0];ns.push(c)}return {n:ns.reverse(),e:es.reverse()}}
119
+ function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
120
+ function evHtml(list){if(!list||!list.length)return '<p class="muted">No evidence cited.</p>';
121
+ return '<ul>'+list.map(function(e){var t=e.said!=null?(esc(e.source||'user')+': “'+esc(e.said)+'”'):esc(e.file+(e.start?':'+e.start+(e.end!==e.start?'-'+e.end:''):''));
122
+ if(e.file&&DATA.source_url){t='<a href="'+esc(DATA.source_url.replace(/\/?$/,'/')+e.file+(e.start?'#L'+e.start+(e.end!==e.start?'-L'+e.end:''):''))+'" target="_blank" rel="noopener">'+t+'</a>'}
123
+ var v=e.verified===true?' <span class="tag" style="color:var(--ok)">checked</span>':(e.verified===null&&e.file?' <span class="tag">not checked</span>':'');
124
+ return '<li>'+t+v+(e.snippet?'<pre>'+esc(e.snippet)+'</pre>':'')+'</li>'}).join('')+'</ul>'}
125
+ var selected=null;
126
+ function show(kind,id){var x=kind==='n'?nodes.get(id):edges.get(id);if(!x)return;var h='<button class="close" aria-label="Close">×</button>';
127
+ if(kind==='n'){h+='<h3>'+esc(x.label)+'</h3><div class="kind">'+esc(x.kind)+(x.group_label?' · '+esc(x.group_label):'')+(x.assumed?' · <span style="color:var(--warn)">assumption</span>':'')+(x.status?' · '+esc(x.status):'')+'</div>';
128
+ if(x.note)h+='<p>'+esc(x.note)+'</p>';if(x.detail)h+='<p>'+esc(x.detail)+'</p>';
129
+ if(x.fields&&x.fields.length)h+='<ul>'+x.fields.map(function(f){return '<li><code>'+esc(f.name)+'</code> '+esc(f.type||'')+(f.key?' <b>'+esc([].concat(f.key).join('/').toUpperCase())+'</b>':'')+(f.references?' → '+esc(f.references):'')+(f.note?' — '+esc(f.note):'')+'</li>'}).join('')+'</ul>';
130
+ h+='<div class="acts"><button data-act="up">What it depends on</button><button data-act="down">What depends on it</button><button data-act="near">Neighbours</button></div>';
131
+ h+='<p class="muted" style="font-size:12px">Shift-click another part to see the path between them.</p>';
132
+ var o=outE.get(id).map(function(i){var e=edges.get(i);return '<li><a class="jump" data-n="'+esc(e.to)+'">'+esc(nodes.get(e.to).label)+'</a>'+(e.label?' — '+esc(e.label):'')+'</li>'}).join('');
133
+ var n=inE.get(id).map(function(i){var e=edges.get(i);return '<li><a class="jump" data-n="'+esc(e.from)+'">'+esc(nodes.get(e.from).label)+'</a>'+(e.label?' — '+esc(e.label):'')+'</li>'}).join('');
134
+ if(o)h+='<b>Goes to</b><ul>'+o+'</ul>';if(n)h+='<b>Comes from</b><ul>'+n+'</ul>';
135
+ h+='<b>Evidence</b>'+evHtml(x.evidence);
136
+ var nb=new Set([id]);outE.get(id).concat(inE.get(id)).forEach(function(i){nb.add(edges.get(i).from);nb.add(edges.get(i).to)});light(Array.from(nb),outE.get(id).concat(inE.get(id)));el('n',id).classList.add('sel');
137
+ }else{h+='<h3>'+esc(nodes.get(x.from).label)+' → '+esc(nodes.get(x.to).label)+'</h3><div class="kind">'+(x.label?esc(x.label):'connection')+(x.assumed?' · <span style="color:var(--warn)">assumption</span>':'')+(x.status?' · '+esc(x.status):'')+'</div>';
138
+ if(x.detail)h+='<p>'+esc(x.detail)+'</p>';h+='<b>Evidence</b>'+evHtml(x.evidence);light([x.from,x.to],[id],true)}
139
+ panel.innerHTML=h;panel.classList.add('open');selected=kind==='n'?id:null;
140
+ panel.querySelector('.close').onclick=function(){panel.classList.remove('open');clear();selected=null};
141
+ panel.querySelectorAll('.jump').forEach(function(a){a.onclick=function(){show('n',a.getAttribute('data-n'))}});
142
+ panel.querySelectorAll('[data-act]').forEach(function(b){b.onclick=function(){var a=b.getAttribute('data-act');
143
+ if(a==='near'){show('n',id);return}var r=reach(id,a==='down'?1:-1);light(r.n,r.e,true);el('n',id).classList.add('sel')}})}
144
+ svg.addEventListener('click',function(e){var g=e.target.closest('.node');if(g){var id=g.getAttribute('data-id');
145
+ if(e.shiftKey&&selected&&selected!==id){var p=path(selected,id,true)||path(selected,id,false);if(p){light(p.n,p.e,true)}return}show('n',id);return}
146
+ var ed=e.target.closest('.edge');if(ed){show('e',ed.getAttribute('data-id'));return}});
147
+ svg.addEventListener('keydown',function(e){var g=e.target.closest&&e.target.closest('.node');if(g&&(e.key==='Enter'||e.key===' ')){e.preventDefault();show('n',g.getAttribute('data-id'))}});
148
+ document.addEventListener('keydown',function(e){if(e.key==='Escape'){panel.classList.remove('open');clear();selected=null}
149
+ if(e.target.tagName==='INPUT')return;if(e.key==='ArrowRight'&&tour)step(1);if(e.key==='ArrowLeft'&&tour)step(-1)});
150
+ var q=document.getElementById('find');q.addEventListener('input',function(){var v=q.value.trim().toLowerCase();if(!v){clear();return}
151
+ var ns=DATA.nodes.filter(function(n){return (n.label+' '+(n.note||'')+' '+(n.fields||[]).map(function(f){return f.name}).join(' ')).toLowerCase().indexOf(v)>=0}).map(function(n){return n.id});light(ns,[])});
152
+ document.getElementById('zin').onclick=function(){zoom(1/1.25)};document.getElementById('zout').onclick=function(){zoom(1.25)};document.getElementById('zfit').onclick=fit;
153
+ document.getElementById('theme').onclick=function(){var t=root.getAttribute('data-theme')==='dark'?'light':'dark';root.setAttribute('data-theme',t);try{localStorage.setItem('idg-theme',t)}catch(e){}};
154
+ function download(name,type,data){var a=document.createElement('a');a.href=URL.createObjectURL(new Blob([data],{type:type}));a.download=name;document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(a.href);a.remove()},500)}
155
+ function standalone(){var c=svg.cloneNode(true);c.setAttribute('viewBox','0 0 '+W+' '+H);c.setAttribute('width',W);c.setAttribute('height',H);c.classList.remove('focus');
156
+ c.querySelectorAll('.on,.sel,.flow').forEach(function(x){x.classList.remove('on','sel','flow')});
157
+ var cs=getComputedStyle(root),vars=['--bg','--panel','--ink','--muted','--line','--edge','--accent','--warn','--ok','--bad','--hl','--canvas','--group','--group2'].map(function(v){return v+':'+cs.getPropertyValue(v)}).join(';');
158
+ var st=document.createElementNS('http://www.w3.org/2000/svg','style');st.textContent='svg{'+vars+';font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}'+document.getElementById('diagram-css').textContent.split('/* diagram */')[1];
159
+ c.insertBefore(st,c.firstChild);return new XMLSerializer().serializeToString(c)}
160
+ var base=(DATA.slug||'diagram');window.interactiveDiagram={svg:standalone,mermaid:function(){return DATA.mermaid}};
161
+ document.getElementById('xsvg').onclick=function(){download(base+'.svg','image/svg+xml',standalone())};
162
+ document.getElementById('xpng').onclick=function(){var img=new Image();img.onload=function(){var k=2,cv=document.createElement('canvas');cv.width=W*k;cv.height=H*k;var ctx=cv.getContext('2d');ctx.scale(k,k);ctx.drawImage(img,0,0);cv.toBlob(function(b){download(base+'.png','image/png',b)})};
163
+ img.src='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(standalone())};
164
+ document.getElementById('xmmd').onclick=function(){var t=DATA.mermaid;if(navigator.clipboard)navigator.clipboard.writeText(t).catch(function(){});download(base+'.mmd','text/plain',t)};
165
+ var tour=null,cap=document.getElementById('caption');
166
+ var steps=DATA.story&&DATA.story.length?DATA.story:(DATA.type==='sequence'?DATA.edges.map(function(m,i){return {text:(i+1)+'. '+nodes.get(m.from).label+' → '+nodes.get(m.to).label+(m.label?': '+m.label:''),edges:[m.id],nodes:[m.from,m.to]}}):[]);
167
+ var tb=document.getElementById('tour');if(!steps.length){tb.style.display='none'}
168
+ function step(d){tour.i=Math.max(0,Math.min(steps.length-1,tour.i+d));var s=steps[tour.i];
169
+ light((s.nodes||[]).concat((s.edges||[]).map(function(i){return edges.get(i)?edges.get(i).from:null}).filter(Boolean)).concat((s.edges||[]).map(function(i){return edges.get(i)?edges.get(i).to:null}).filter(Boolean)),s.edges||[],true);
170
+ cap.innerHTML='<b>Step '+(tour.i+1)+' of '+steps.length+'</b> — '+esc(s.text||'')+' <span class="muted">(← → to move, Esc to stop)</span>';cap.classList.add('on')}
171
+ tb.onclick=function(){if(tour){tour=null;cap.classList.remove('on');clear();tb.textContent='Walk through';return}tour={i:-1};tb.textContent='Stop';step(1)};
172
+ document.getElementById('prev').onclick=function(){if(tour)step(-1)};document.getElementById('next').onclick=function(){if(!tour){tb.onclick()}else step(1)};
173
+ if(!steps.length){document.getElementById('prev').style.display='none';document.getElementById('next').style.display='none'}
174
+ function size(){var wr=document.querySelector('.wrap');var avail=Math.max(window.innerHeight-150,360);var r=stage.getBoundingClientRect();
175
+ var s=Math.max(W/Math.max(r.width,1),0.6);wr.style.height=Math.min(avail,Math.max(H/s+24,300))+'px'}
176
+ size();fit();window.addEventListener('resize',function(){size();fit()});
177
+ function hash(){var h=decodeURIComponent(location.hash.slice(1));if(!h)return;var kv=new Map();h.split('&').forEach(function(p){var i=p.indexOf('=');if(i>0)kv.set(p.slice(0,i),p.slice(i+1))});
178
+ if(kv.get('theme'))root.setAttribute('data-theme',kv.get('theme'));
179
+ if(kv.get('n')&&nodes.get(kv.get('n')))show('n',kv.get('n'));if(kv.get('e')&&edges.get(kv.get('e')))show('e',kv.get('e'));
180
+ if(kv.get('path')){var ab=kv.get('path').split(',');var p=path(ab[0],ab[1],true)||path(ab[0],ab[1],false);if(p)light(p.n,p.e,true)}
181
+ if(kv.get('reach')){var ar=kv.get('reach').split(',');var r=reach(ar[0],ar[1]==='up'?-1:1);light(r.n,r.e,true)}
182
+ if(kv.get('step')&&steps.length){tour={i:-1};tb.textContent='Stop';step(+kv.get('step'))}}
183
+ hash();window.addEventListener('hashchange',hash);
184
+ })();
185
+ </script>
186
+ </body></html>
@@ -0,0 +1,48 @@
1
+ {
2
+ "type": "architecture",
3
+ "title": "What a checkout request touches",
4
+ "question": "What happens between a shopper pressing Pay and a courier being booked?",
5
+ "summary": "The API prices the cart from Redis (falling back to Postgres), charges Stripe, records the order and puts it on a RabbitMQ queue; a separate worker books the courier and marks the order booked. The shopper's response returns before the courier is booked.",
6
+ "groups": [
7
+ {"id": "compose", "label": "docker-compose stack"},
8
+ {"id": "data", "label": "Data", "parent": "compose"}
9
+ ],
10
+ "nodes": [
11
+ {"id": "shopper", "label": "Shopper's browser", "kind": "client", "assumed": true, "detail": "Nothing in this repo shows the client; the API's POST /checkout is the entry point."},
12
+ {"id": "api", "label": "Checkout API", "kind": "service", "note": "FastAPI · POST /checkout", "group": "compose",
13
+ "evidence": ["api/checkout.py:29-41#def checkout", "docker-compose.yml:2-8"]},
14
+ {"id": "cache", "label": "Price cache", "kind": "cache", "note": "Redis, 5 min TTL", "group": "data",
15
+ "evidence": ["api/checkout.py:20-24#ex=300", "docker-compose.yml:17-18"]},
16
+ {"id": "db", "label": "Orders database", "kind": "database", "note": "Postgres 16", "group": "data",
17
+ "evidence": ["api/checkout.py:22#from products", "api/checkout.py:36#insert into orders", "docker-compose.yml:15-16"]},
18
+ {"id": "broker", "label": "fulfilment queue", "kind": "queue", "note": "RabbitMQ", "group": "compose",
19
+ "evidence": ["api/checkout.py:39-40#routing_key=\"fulfilment\"", "docker-compose.yml:19-20"]},
20
+ {"id": "worker", "label": "Fulfilment worker", "kind": "worker", "group": "compose",
21
+ "evidence": ["worker/fulfil.py:13-19#def on_message", "docker-compose.yml:9-14"]},
22
+ {"id": "stripe", "label": "Stripe", "kind": "external", "note": "PaymentIntent",
23
+ "evidence": ["api/checkout.py:32-35#stripe.PaymentIntent.create"]},
24
+ {"id": "courier", "label": "Courier API", "kind": "external", "note": "courier.example",
25
+ "evidence": ["worker/fulfil.py:16#courier.example/v2/bookings"]}
26
+ ],
27
+ "edges": [
28
+ {"id": "pay", "from": "shopper", "to": "api", "label": "POST /checkout", "emphasis": true, "assumed": true},
29
+ {"id": "price", "from": "api", "to": "cache", "label": "get price:<sku>", "evidence": ["api/checkout.py:20"]},
30
+ {"id": "miss", "from": "api", "to": "db", "label": "price on cache miss, insert order", "evidence": ["api/checkout.py:22", "api/checkout.py:36-38"]},
31
+ {"id": "charge", "from": "api", "to": "stripe", "label": "create + confirm payment", "emphasis": true, "evidence": ["api/checkout.py:32-35"]},
32
+ {"id": "publish", "from": "api", "to": "broker", "label": "publish order_id", "style": "dashed", "evidence": ["api/checkout.py:39-40"]},
33
+ {"id": "consume", "from": "broker", "to": "worker", "label": "consume", "style": "dashed", "evidence": ["worker/fulfil.py:22#basic_consume"]},
34
+ {"id": "book", "from": "worker", "to": "courier", "label": "book courier", "evidence": ["worker/fulfil.py:16"]},
35
+ {"id": "mark", "from": "worker", "to": "db", "label": "read address, mark booked", "evidence": ["worker/fulfil.py:15", "worker/fulfil.py:17"]}
36
+ ],
37
+ "story": [
38
+ {"text": "The shopper's browser posts the cart to the Checkout API (the client itself is not in this repo).", "edges": ["pay"]},
39
+ {"text": "Each line is priced from Redis; a miss reads Postgres and caches the price for five minutes.", "edges": ["price", "miss"]},
40
+ {"text": "Stripe is charged and confirmed in the same call; anything but 'succeeded' returns 402.", "edges": ["charge"]},
41
+ {"text": "The order is inserted, then its id is published to the fulfilment queue, and the API answers the shopper.", "edges": ["miss", "publish"]},
42
+ {"text": "Later, the worker takes the id, reads the address, books the courier and marks the order booked.", "edges": ["consume", "book", "mark"]}
43
+ ],
44
+ "notes": [
45
+ "The worker reads orders.address, but the API's insert does not write an address. Where it comes from is not in this repo.",
46
+ "Left out: the products table's other readers, and anything after the courier booking."
47
+ ]
48
+ }
@@ -0,0 +1,31 @@
1
+ # Worked example — what a checkout request touches
2
+
3
+ ## Input
4
+
5
+ > Map what happens in this repo between a shopper pressing Pay and a courier being booked. One
6
+ > HTML file I can send to the team.
7
+
8
+ The repository: [`inputs/checkout/`](inputs/checkout/) — `docker-compose.yml`, `api/checkout.py`
9
+ (FastAPI), `worker/fulfil.py`.
10
+
11
+ ## Output
12
+
13
+ [What a checkout request touches](checkout-map.html) — one page, opens offline.
14
+
15
+ The API prices the cart from Redis (falling back to Postgres), charges Stripe, records the order
16
+ and puts its id on a RabbitMQ queue; a separate worker books the courier and marks the order
17
+ booked. The shopper gets their answer before the courier is booked.
18
+
19
+ - **Assumed (dashed):** the shopper's browser and its POST — no client is in this repo.
20
+ - **Worth a look:** the worker reads `orders.address`, but the API's insert never writes one.
21
+ - **Left out:** other readers of the products table, and anything after the courier booking.
22
+
23
+ 22 of 22 file citations checked against the repository.
24
+
25
+ ## Why this is the right output
26
+
27
+ - Every box and arrow on the page cites the file and lines it came from; click one to see them.
28
+ - The two things the code does not show are drawn dashed, not stated as fact.
29
+ - The map answers the one question asked, and says what it left out.
30
+ - The page was built from [`checkout-map.json`](checkout-map.json) with
31
+ `python3 scripts/build_diagram.py examples/checkout-map.json -o examples/checkout-map.html --repo-root examples/inputs/checkout`.
@@ -0,0 +1,41 @@
1
+ """POST /checkout: price the cart, take payment, record the order, hand off fulfilment."""
2
+ import json
3
+ import os
4
+
5
+ import pika
6
+ import psycopg
7
+ import redis
8
+ import stripe
9
+ from fastapi import FastAPI, HTTPException
10
+
11
+ app = FastAPI()
12
+ db = psycopg.connect(os.environ["DATABASE_URL"])
13
+ prices = redis.Redis.from_url(os.environ["REDIS_URL"])
14
+ broker = pika.BlockingConnection(pika.URLParameters(os.environ["BROKER_URL"])).channel()
15
+
16
+
17
+ def price_cart(cart):
18
+ total = 0
19
+ for line in cart["lines"]:
20
+ cached = prices.get(f"price:{line['sku']}")
21
+ if cached is None:
22
+ row = db.execute("select price from products where sku = %s", (line["sku"],)).fetchone()
23
+ cached = row[0]
24
+ prices.set(f"price:{line['sku']}", cached, ex=300)
25
+ total += int(cached) * line["qty"]
26
+ return total
27
+
28
+
29
+ @app.post("/checkout")
30
+ def checkout(cart: dict):
31
+ total = price_cart(cart)
32
+ intent = stripe.PaymentIntent.create(amount=total, currency="gbp", confirm=True,
33
+ payment_method=cart["payment_method"])
34
+ if intent.status != "succeeded":
35
+ raise HTTPException(402, "payment declined")
36
+ order_id = db.execute("insert into orders (email, total, payment_id) values (%s, %s, %s) returning id",
37
+ (cart["email"], total, intent.id)).fetchone()[0]
38
+ db.commit()
39
+ broker.basic_publish(exchange="", routing_key="fulfilment",
40
+ body=json.dumps({"order_id": order_id}))
41
+ return {"order_id": order_id}
@@ -0,0 +1,20 @@
1
+ services:
2
+ api:
3
+ build: ./api
4
+ environment:
5
+ DATABASE_URL: postgresql://shop@db/shop
6
+ REDIS_URL: redis://cache:6379/0
7
+ BROKER_URL: amqp://broker
8
+ depends_on: [db, cache, broker]
9
+ worker:
10
+ build: ./worker
11
+ environment:
12
+ DATABASE_URL: postgresql://shop@db/shop
13
+ BROKER_URL: amqp://broker
14
+ depends_on: [db, broker]
15
+ db:
16
+ image: postgres:16
17
+ cache:
18
+ image: redis:7
19
+ broker:
20
+ image: rabbitmq:3
@@ -0,0 +1,23 @@
1
+ """Consumes the fulfilment queue and books a courier for each paid order."""
2
+ import json
3
+ import os
4
+
5
+ import pika
6
+ import psycopg
7
+ import requests
8
+
9
+ db = psycopg.connect(os.environ["DATABASE_URL"])
10
+ channel = pika.BlockingConnection(pika.URLParameters(os.environ["BROKER_URL"])).channel()
11
+
12
+
13
+ def on_message(ch, method, properties, body):
14
+ order_id = json.loads(body)["order_id"]
15
+ address = db.execute("select address from orders where id = %s", (order_id,)).fetchone()[0]
16
+ booking = requests.post("https://courier.example/v2/bookings", json={"ref": order_id, "to": address}, timeout=10)
17
+ db.execute("update orders set status = 'booked', courier_ref = %s where id = %s", (booking.json()["ref"], order_id))
18
+ db.commit()
19
+ ch.basic_ack(method.delivery_tag)
20
+
21
+
22
+ channel.basic_consume(queue="fulfilment", on_message_callback=on_message)
23
+ channel.start_consuming()