skilldrop-cli 0.16.21 → 0.16.23

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 (55) 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 +160 -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 +164 -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/10/build-4127-timing.json +179 -0
  14. package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/AbstractSensor.java +36 -0
  15. package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/AlertService.java +15 -0
  16. package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/Greenhouse.java +36 -0
  17. package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/Sensor.java +10 -0
  18. package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/SoilMoistureSensor.java +25 -0
  19. package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/TemperatureSensor.java +19 -0
  20. package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/Zone.java +26 -0
  21. package/packs/solution-architect/skills/interactive-diagram/evals/files/3/schema.sql +35 -0
  22. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/README.md +26 -0
  23. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/config/default.json +29 -0
  24. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/clients/transcription.js +25 -0
  25. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/db.js +16 -0
  26. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/handlers/storageWebhook.js +43 -0
  27. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/jobs/transcribeEpisode.js +28 -0
  28. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/mailer.js +18 -0
  29. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/server.js +32 -0
  30. package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/worker.js +32 -0
  31. package/packs/solution-architect/skills/interactive-diagram/evals/files/6/notes.txt +7 -0
  32. package/packs/solution-architect/skills/interactive-diagram/evals/files/6/trace-quote-slow.jsonl +13 -0
  33. package/packs/solution-architect/skills/interactive-diagram/evals/files/7/oncall-notes.md +16 -0
  34. package/packs/solution-architect/skills/interactive-diagram/evals/files/7/system-log.txt +14 -0
  35. package/packs/solution-architect/skills/interactive-diagram/evals/files/8/run-4471.json +151 -0
  36. package/packs/solution-architect/skills/interactive-diagram/evals/files/9/chat-export.txt +16 -0
  37. package/packs/solution-architect/skills/interactive-diagram/evals/files/9/lesson-sync.log +24 -0
  38. package/packs/solution-architect/skills/interactive-diagram/evals/files/9/ticket-8812.csv +6 -0
  39. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.html +203 -0
  40. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.json +48 -0
  41. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.md +31 -0
  42. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-trace.html +203 -0
  43. package/packs/solution-architect/skills/interactive-diagram/examples/checkout-trace.json +110 -0
  44. package/packs/solution-architect/skills/interactive-diagram/examples/incident-timeline.html +203 -0
  45. package/packs/solution-architect/skills/interactive-diagram/examples/incident-timeline.json +21 -0
  46. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/api/checkout.py +41 -0
  47. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/docker-compose.yml +20 -0
  48. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/worker/fulfil.py +23 -0
  49. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/incident/oncall-notes.md +14 -0
  50. package/packs/solution-architect/skills/interactive-diagram/examples/inputs/trace/checkout-trace.jsonl +9 -0
  51. package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.html +203 -0
  52. package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.json +28 -0
  53. package/packs/solution-architect/skills/interactive-diagram/manifest.json +39 -0
  54. package/packs/solution-architect/skills/interactive-diagram/reference.md +231 -0
  55. package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +2605 -0
@@ -0,0 +1,13 @@
1
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50002", "parent_span_id": "a1f0c3d2e4b50001", "service": "auth-svc", "name": "verify_token", "kind": "SERVER", "start_time": "2026-10-14T09:41:07.312Z", "end_time": "2026-10-14T09:41:07.358Z", "duration_ms": 46, "status": "OK", "attributes": {"auth.scheme": "bearer"}}
2
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50005", "parent_span_id": "a1f0c3d2e4b50004", "service": "postgres", "name": "SELECT contracts", "kind": "CLIENT", "start_time": "2026-10-14T09:41:07.375Z", "end_time": "2026-10-14T09:41:07.600Z", "duration_ms": 225, "status": "OK", "attributes": {"db.system": "postgresql", "db.table": "contracts", "db.rows": 14}}
3
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50004", "parent_span_id": "a1f0c3d2e4b50003", "service": "quote-svc", "name": "load_customer_contract", "kind": "INTERNAL", "start_time": "2026-10-14T09:41:07.370Z", "end_time": "2026-10-14T09:41:07.610Z", "duration_ms": 240, "status": "OK", "attributes": {}}
4
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50007", "parent_span_id": "a1f0c3d2e4b50003", "service": "rates-svc", "name": "carrier_rates coastal", "kind": "CLIENT", "start_time": "2026-10-14T09:41:07.625Z", "end_time": "2026-10-14T09:41:10.010Z", "duration_ms": 2385, "status": "OK", "attributes": {"carrier": "coastal", "peer.service": "coastal-carrier-api", "http.status_code": 200}}
5
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50006", "parent_span_id": "a1f0c3d2e4b50003", "service": "rates-svc", "name": "carrier_rates north-line", "kind": "CLIENT", "start_time": "2026-10-14T09:41:07.620Z", "end_time": "2026-10-14T09:41:10.220Z", "duration_ms": 2600, "status": "OK", "attributes": {"carrier": "north-line", "peer.service": "north-line-carrier-api", "http.status_code": 200}}
6
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50009", "parent_span_id": "a1f0c3d2e4b50008", "service": "cache", "name": "GET fx:EUR-USD", "kind": "CLIENT", "start_time": "2026-10-14T09:41:10.240Z", "end_time": "2026-10-14T09:41:10.248Z", "duration_ms": 8, "status": "OK", "attributes": {"cache.hit": true}}
7
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50008", "parent_span_id": "a1f0c3d2e4b50003", "service": "quote-svc", "name": "price_and_rank", "kind": "INTERNAL", "start_time": "2026-10-14T09:41:10.230Z", "end_time": "2026-10-14T09:41:11.280Z", "duration_ms": 1050, "status": "OK", "attributes": {"rank.candidates": 1840, "rank.algorithm": "rank_v2"}}
8
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b5000b", "parent_span_id": "a1f0c3d2e4b5000a", "service": "postgres", "name": "INSERT quotes", "kind": "CLIENT", "start_time": "2026-10-14T09:41:11.290Z", "end_time": "2026-10-14T09:41:11.430Z", "duration_ms": 140, "status": "OK", "attributes": {"db.system": "postgresql", "db.table": "quotes"}}
9
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b5000a", "parent_span_id": "a1f0c3d2e4b50003", "service": "quote-svc", "name": "persist_quote", "kind": "INTERNAL", "start_time": "2026-10-14T09:41:11.285Z", "end_time": "2026-10-14T09:41:11.440Z", "duration_ms": 155, "status": "OK", "attributes": {}}
10
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50003", "parent_span_id": "a1f0c3d2e4b50001", "service": "quote-svc", "name": "build_quote", "kind": "SERVER", "start_time": "2026-10-14T09:41:07.360Z", "end_time": "2026-10-14T09:41:11.450Z", "duration_ms": 4090, "status": "OK", "attributes": {"quote.lanes": 3, "quote.mode": "FTL"}}
11
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b5000d", "parent_span_id": "a1f0c3d2e4b50001", "service": "api-gateway", "name": "serialize_response", "kind": "INTERNAL", "start_time": "2026-10-14T09:41:11.452Z", "end_time": "2026-10-14T09:41:11.495Z", "duration_ms": 43, "status": "OK", "attributes": {"http.response_size_bytes": 18233}}
12
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b50001", "parent_span_id": null, "service": "api-gateway", "name": "POST /v2/quotes", "kind": "SERVER", "start_time": "2026-10-14T09:41:07.300Z", "end_time": "2026-10-14T09:41:11.500Z", "duration_ms": 4200, "status": "OK", "attributes": {"http.method": "POST", "http.route": "/v2/quotes", "http.status_code": 200, "customer.tier": "standard"}}
13
+ {"trace_id": "4b9e1c07a2d35f68e0c1b7a94d2f6031", "span_id": "a1f0c3d2e4b5000c", "parent_span_id": "a1f0c3d2e4b50003", "service": "notify-svc", "name": "emit quote.created", "kind": "PRODUCER", "start_time": "2026-10-14T09:41:11.446Z", "end_time": "2026-10-14T09:41:11.910Z", "duration_ms": 464, "status": "OK", "attributes": {"messaging.destination": "quote.created", "messaging.system": "kafka"}}
@@ -0,0 +1,16 @@
1
+ # INC-0412 - arrival boards frozen - my notes
2
+
3
+ Sat 10 Oct, on call for transit-data. All times below are local (CDT).
4
+
5
+ - 10:41pm night supervisor at Central Station called. Arrival boards showing the same times "for a couple of hours at least". Asked them to open an incident so it pages properly.
6
+ - 10:46pm acked the page.
7
+ - 10:58pm on VPN. feed-builder says it has been serving last-known positions since about 7pm. Vehicle count looks normal (~210) but the positions aren't moving, which is why nothing looked wrong. Freshness panel in Grafana says OK?? The FeedStale alert never fired.
8
+ - ~11:10pm? restarted avl-ingest on the ingest box (`systemctl restart avl-ingest`). Didn't write the time down, could have been 11:05 or as late as 11:15. The ingest box doesn't ship logs to the central log so I can't check.
9
+ - 11:14pm supervisor says the Central Station boards are moving again.
10
+ - 11:30pm rolled feed-builder back to 3.13.2 anyway. 3.14.0 went out just before all this started and I didn't want to risk it overnight.
11
+ - 12:05am posted all-clear in #ops-transit and resolved the incident.
12
+
13
+ Open questions for the review:
14
+ - why didn't FeedStale fire? (it checks feed age, but we were re-publishing old positions with a fresh timestamp?)
15
+ - was it 3.14.0 or did avl-ingest just hang? not sure yet.
16
+ - ask Track Ops if the Riverside tram boards were affected too.
@@ -0,0 +1,14 @@
1
+ # central log export, filter: service in (feed-builder, deploy, pager) tz=UTC
2
+ 2026-10-10T23:30:00Z feed-builder INFO published realtime feed vehicles=209 age_s=4
3
+ 2026-10-10T23:45:00Z feed-builder INFO published realtime feed vehicles=211 age_s=3
4
+ 2026-10-10T23:58:12Z deploy INFO feed-builder v3.14.0 rolled out (3/3 pods) by ci-bot
5
+ 2026-10-11T00:00:00Z feed-builder INFO published realtime feed vehicles=210 age_s=5
6
+ 2026-10-11T00:03:40Z feed-builder WARN avl source returned 0 vehicle positions; serving last-known positions (vehicles=210)
7
+ 2026-10-11T03:44:20Z pager INFO incident INC-0412 opened manually by night-supervisor; paging oncall-transit-data
8
+ 2026-10-11T03:46:02Z pager INFO INC-0412 acknowledged by oncall-transit-data
9
+ 2026-10-11T04:12:07Z feed-builder INFO avl source recovered vehicles=212; published realtime feed age_s=6
10
+ 2026-10-11T04:15:00Z feed-builder INFO published realtime feed vehicles=212 age_s=4
11
+ 2026-10-11T04:31:45Z deploy INFO feed-builder v3.13.2 rolled out (3/3 pods) by oncall-transit-data
12
+ 2026-10-11T04:45:00Z feed-builder INFO published realtime feed vehicles=208 age_s=3
13
+ 2026-10-11T05:00:00Z feed-builder INFO published realtime feed vehicles=205 age_s=4
14
+ 2026-10-11T05:05:40Z pager INFO INC-0412 resolved by oncall-transit-data
@@ -0,0 +1,151 @@
1
+ {
2
+ "export": "orchestrator run export v2",
3
+ "pipeline": "nightly-sky-ingest",
4
+ "run_id": "run-4471",
5
+ "observatory": "Example Ridge Observatory",
6
+ "timezone": "UTC",
7
+ "run_status": "succeeded",
8
+ "run_reported_finished_at": "2026-10-17T01:25:00Z",
9
+ "steps": [
10
+ {
11
+ "id": "s01",
12
+ "name": "nightly-sky-ingest",
13
+ "parent_id": null,
14
+ "started_at": "2026-10-17T01:00:00Z",
15
+ "finished_at": "2026-10-17T01:25:00Z",
16
+ "status": "succeeded",
17
+ "host": "orchestrator"
18
+ },
19
+ {
20
+ "id": "s02",
21
+ "name": "checkout_config",
22
+ "parent_id": "s01",
23
+ "started_at": "2026-10-17T01:00:00Z",
24
+ "finished_at": "2026-10-17T01:00:20Z",
25
+ "status": "succeeded",
26
+ "host": "runner-2"
27
+ },
28
+ {
29
+ "id": "s03",
30
+ "name": "fetch_frames",
31
+ "parent_id": "s01",
32
+ "started_at": "2026-10-17T01:00:20Z",
33
+ "finished_at": "2026-10-17T01:10:20Z",
34
+ "status": "succeeded",
35
+ "host": "runner-2"
36
+ },
37
+ {
38
+ "id": "s04",
39
+ "name": "fetch_north",
40
+ "parent_id": "s03",
41
+ "started_at": "2026-10-17T01:00:22Z",
42
+ "finished_at": "2026-10-17T01:05:00Z",
43
+ "status": "succeeded",
44
+ "host": "runner-3",
45
+ "site": "north-dome"
46
+ },
47
+ {
48
+ "id": "s05",
49
+ "name": "fetch_south",
50
+ "parent_id": "s03",
51
+ "started_at": "2026-10-17T01:00:22Z",
52
+ "finished_at": "2026-10-17T01:10:10Z",
53
+ "status": "succeeded",
54
+ "host": "runner-4",
55
+ "site": "south-dome"
56
+ },
57
+ {
58
+ "id": "s06",
59
+ "name": "fetch_east",
60
+ "parent_id": "s03",
61
+ "started_at": "2026-10-17T01:00:22Z",
62
+ "finished_at": "2026-10-17T01:04:00Z",
63
+ "status": "succeeded",
64
+ "host": "runner-5",
65
+ "site": "east-dome"
66
+ },
67
+ {
68
+ "id": "s07",
69
+ "name": "verify_checksums",
70
+ "parent_id": "s03",
71
+ "started_at": "2026-10-17T01:10:10Z",
72
+ "finished_at": "2026-10-17T01:10:18Z",
73
+ "status": "succeeded",
74
+ "host": "runner-2"
75
+ },
76
+ {
77
+ "id": "s08",
78
+ "name": "calibrate",
79
+ "parent_id": "s01",
80
+ "started_at": "2026-10-17T01:10:20Z",
81
+ "finished_at": "2026-10-17T01:18:20Z",
82
+ "status": "succeeded",
83
+ "host": "gpu-1"
84
+ },
85
+ {
86
+ "id": "s09",
87
+ "name": "load_darks",
88
+ "parent_id": "s08",
89
+ "started_at": "2026-10-17T01:10:20Z",
90
+ "finished_at": "2026-10-17T01:10:50Z",
91
+ "status": "succeeded",
92
+ "host": "gpu-1"
93
+ },
94
+ {
95
+ "id": "s10",
96
+ "name": "load_flats",
97
+ "parent_id": "s08",
98
+ "started_at": "2026-10-17T01:10:50Z",
99
+ "finished_at": "2026-10-17T01:11:20Z",
100
+ "status": "succeeded",
101
+ "host": "gpu-1"
102
+ },
103
+ {
104
+ "id": "s11",
105
+ "name": "stack_and_publish",
106
+ "parent_id": "s01",
107
+ "started_at": "2026-10-17T01:18:20Z",
108
+ "finished_at": "2026-10-17T01:24:52Z",
109
+ "status": "succeeded",
110
+ "host": "runner-2"
111
+ },
112
+ {
113
+ "id": "s12",
114
+ "name": "stack_mosaic",
115
+ "parent_id": "s11",
116
+ "started_at": "2026-10-17T01:18:20Z",
117
+ "finished_at": "2026-10-17T01:23:00Z",
118
+ "status": "succeeded",
119
+ "host": "gpu-1"
120
+ },
121
+ {
122
+ "id": "s13",
123
+ "name": "publish_catalog",
124
+ "parent_id": "s11",
125
+ "started_at": "2026-10-17T01:23:00Z",
126
+ "finished_at": "2026-10-17T01:24:50Z",
127
+ "status": "succeeded",
128
+ "host": "runner-2",
129
+ "target": "catalog-db"
130
+ },
131
+ {
132
+ "id": "s14",
133
+ "name": "upload_thumbnails",
134
+ "parent_id": "s11",
135
+ "started_at": "2026-10-17T01:23:05Z",
136
+ "finished_at": "2026-10-17T01:28:40Z",
137
+ "status": "succeeded",
138
+ "host": "runner-6",
139
+ "detached": true
140
+ },
141
+ {
142
+ "id": "s15",
143
+ "name": "send_summary",
144
+ "parent_id": "s01",
145
+ "started_at": "2026-10-17T01:24:52Z",
146
+ "finished_at": "2026-10-17T01:25:00Z",
147
+ "status": "succeeded",
148
+ "host": "runner-2"
149
+ }
150
+ ]
151
+ }
@@ -0,0 +1,16 @@
1
+ Chat export: #lesson-sync-oncall
2
+ Exported by: support-mei
3
+ Times shown in Asia/Kolkata (UTC+05:30)
4
+
5
+ [15/10/2026, 03:08] deploy-bot: lesson-sync v4.12.0 rolling out to prod (started by @dev-ravi)
6
+ [15/10/2026, 03:10] deploy-bot: lesson-sync v4.12.0 healthy on 6/6 pods
7
+ [15/10/2026, 03:31] support-mei: schools in the north region say lesson downloads are stuck at 0% in the app. opening ticket 8812
8
+ [15/10/2026, 03:36] oncall-ana: looking. seeing "manifest checksum mismatch" in lesson-sync since about 03:22
9
+ [15/10/2026, 03:44] oncall-ana: only on the v4.12.0 pods as far as I can tell
10
+ [15/10/2026, 03:55] oncall-ana: rolling back to v4.11.3
11
+ [15/10/2026, 03:58] dev-ravi: did anyone purge the CDN? I *think* a purge ran around 03:50 but I can't find who or when, don't quote me
12
+ [15/10/2026, 04:01] deploy-bot: lesson-sync v4.11.3 healthy on 6/6 pods
13
+ [15/10/2026, 04:06] oncall-ana: errors stopped at 04:03. watching
14
+ [15/10/2026, 04:20] oncall-ana: support says the school confirmed downloads work again. going to bed, will look at why 4.12 broke checksums in the morning
15
+ [15/10/2026, 11:42] dev-ravi: bumping max_retries 3 -> 5 on the lesson-sync config, been meaning to do that for weeks anyway
16
+ [15/10/2026, 12:10] support-mei: closing 8812
@@ -0,0 +1,24 @@
1
+ # lesson-sync production log, all pods merged
2
+ # filter: level>=INFO, health checks and per-request lines excluded
3
+ # timestamps UTC
4
+ 2026-10-14T21:30:00Z INFO metrics download_success_rate_5m=99.7% pods=6 version=4.11.3
5
+ 2026-10-14T21:38:02Z INFO deploy started version=4.12.0 pods=6 by=dev-ravi
6
+ 2026-10-14T21:40:12Z INFO deploy complete version=4.12.0 healthy=6/6
7
+ 2026-10-14T21:45:00Z INFO metrics download_success_rate_5m=99.5% pods=6 version=4.12.0
8
+ 2026-10-14T21:52:30Z ERROR manifest checksum mismatch pack=grade7-science-u3 pod=lesson-sync-4120-a
9
+ 2026-10-14T21:53:11Z ERROR manifest checksum mismatch pack=grade5-maths-u1 pod=lesson-sync-4120-c
10
+ 2026-10-14T21:55:47Z ERROR manifest checksum mismatch pack=grade7-science-u3 pod=lesson-sync-4120-b
11
+ 2026-10-14T22:00:00Z INFO metrics download_success_rate_5m=41.2% pods=6 version=4.12.0
12
+ 2026-10-14T22:03:19Z ERROR manifest checksum mismatch pack=grade6-history-u2 pod=lesson-sync-4120-e
13
+ 2026-10-14T22:09:52Z ERROR manifest checksum mismatch pack=grade5-maths-u1 pod=lesson-sync-4120-a
14
+ 2026-10-14T22:15:00Z INFO metrics download_success_rate_5m=38.9% pods=6 version=4.12.0
15
+ 2026-10-14T22:17:28Z ERROR manifest checksum mismatch pack=grade8-english-u4 pod=lesson-sync-4120-d
16
+ 2026-10-14T22:24:05Z ERROR manifest checksum mismatch pack=grade7-science-u3 pod=lesson-sync-4120-f
17
+ 2026-10-14T22:25:40Z INFO rollback started version=4.11.3 by=oncall-ana
18
+ 2026-10-14T22:29:13Z ERROR manifest checksum mismatch pack=grade6-history-u2 pod=lesson-sync-4120-b
19
+ 2026-10-14T22:31:04Z INFO rollback complete version=4.11.3 healthy=6/6
20
+ 2026-10-14T22:33:41Z ERROR manifest checksum mismatch pack=grade5-maths-u1 pod=lesson-sync-4113-c
21
+ 2026-10-14T22:40:00Z INFO metrics download_success_rate_5m=99.6% pods=6 version=4.11.3
22
+ 2026-10-15T06:12:00Z INFO config reload requested max_retries=5 (was 3) by=dev-ravi
23
+ 2026-10-15T06:12:03Z INFO config reload complete healthy=6/6
24
+ 2026-10-15T06:15:00Z INFO metrics download_success_rate_5m=99.7% pods=6 version=4.11.3
@@ -0,0 +1,6 @@
1
+ timestamp_utc,actor,action,field,old_value,new_value,comment
2
+ 2026-10-14T22:05:00Z,support-mei,created,status,,Open,"Several schools in the north region: lesson downloads stuck at 0% in the app"
3
+ 2026-10-14T22:05:00Z,support-mei,set,priority,,P2,
4
+ 2026-10-14T22:12:00Z,oncall-ana,set,priority,P2,P1,"Errors in lesson-sync, investigating"
5
+ 2026-10-14T22:48:00Z,support-mei,comment,,,,"School contact confirms downloads are completing again"
6
+ 2026-10-15T06:40:00Z,support-mei,set,status,Open,Resolved,"Fixed by the retry config change (max_retries 3 -> 5) deployed this morning"
@@ -0,0 +1,203 @@
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-KdTstAM29gVIP104X7Tju5/PTeQKbRDMMG1TjJdsOyI='; 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" markerUnits="userSpaceOnUse" markerWidth="10" markerHeight="10" 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" markerUnits="userSpaceOnUse" markerWidth="10" markerHeight="10" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" class="arrowhead"/></marker><marker id="tri" viewBox="0 0 12 12" refX="11" refY="6" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto-start-reverse"><path d="M1,1 L11,6 L1,11 z" class="mk-open"/></marker><marker id="open" viewBox="0 0 12 12" refX="11" refY="6" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto-start-reverse"><path d="M1,1 L11,6 L1,11" class="mk-line"/></marker><marker id="dia-solid" viewBox="0 0 16 10" refX="15" refY="5" markerUnits="userSpaceOnUse" markerWidth="18" markerHeight="11" orient="auto-start-reverse"><path d="M1,5 L8,1 L15,5 L8,9 z" class="arrowhead"/></marker><marker id="dia-open" viewBox="0 0 16 10" refX="15" refY="5" markerUnits="userSpaceOnUse" markerWidth="18" markerHeight="11" orient="auto-start-reverse"><path d="M1,5 L8,1 L15,5 L8,9 z" class="mk-open"/></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
+ .findings{margin:10px 0 0;padding:8px 14px;border-left:3px solid var(--accent);background:var(--panel);border-radius:4px;max-width:1000px}
53
+ .findings ul{margin:4px 0 0;padding-left:18px}.findings li{margin:2px 0}
54
+ table.answers{border-collapse:collapse;margin:10px 0 0;max-width:1100px;font-size:13px}
55
+ table.answers th,table.answers td{border:1px solid var(--line);padding:5px 9px;text-align:left;vertical-align:top}
56
+ table.answers th{background:var(--group2);font-weight:600}table.answers td.ev{font-size:12px;color:var(--muted)}
57
+ .issues{margin:8px 24px 0;padding:8px 12px;border:1px solid var(--warn);border-radius:6px;color:var(--warn);font-size:13px}
58
+ /* diagram */
59
+ .canvas{fill:var(--canvas)}
60
+ .group rect{fill:var(--group);stroke:var(--line);stroke-width:1}.group.d2 rect{fill:var(--group2)}.group.d3 rect{fill:var(--group)}
61
+ .glabel{font-size:12px;font-weight:600;fill:var(--muted);letter-spacing:.02em}
62
+ .group.st-removed rect{stroke:var(--bad);stroke-dasharray:6 4;fill:none}
63
+ .node{cursor:pointer;outline:none}.node .shape{stroke-width:1.5;fill:var(--panel)}.node .shape.solid{fill:var(--ink);stroke:var(--ink)}
64
+ .node .shape-line{fill:none;stroke-width:1.2}.node .glyph{fill:var(--muted)}
65
+ .node .label{font-size:13px;font-weight:600;fill:var(--ink)}.node .note{font-size:11px;fill:var(--muted)}
66
+ .node .badge{font-size:10px;fill:var(--ok)}.node .badge.warn{fill:var(--warn)}
67
+ .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)}
68
+ .c-blue .shape,.c-blue .shape-line{stroke:#3b82f6}.c-violet .shape{stroke:#8b5cf6}.c-green .shape,.c-green .shape-line{stroke:#16a34a}
69
+ .c-teal .shape,.c-teal .shape-line{stroke:#0d9488}.c-amber .shape,.c-amber .shape-line{stroke:#d97706}.c-slate .shape{stroke:#64748b}
70
+ .c-rose .shape{stroke:#e11d48}.c-indigo .shape{stroke:#4f46e5}.k-external .shape{stroke-dasharray:5 3}
71
+ .node.assumed .shape{stroke:var(--warn)!important;stroke-dasharray:6 4}
72
+ .node.st-added .shape{stroke:var(--ok)!important;stroke-width:2.5}.node.st-changed .shape{stroke:var(--accent)!important;stroke-width:2.5}
73
+ .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}
74
+ .edge .line{fill:none;stroke:var(--edge);stroke-width:1.5}.edge .hit{fill:none;stroke:transparent;stroke-width:12;cursor:pointer}
75
+ .edge.s-dashed .line{stroke-dasharray:7 5}.edge.s-dotted .line{stroke-dasharray:2 4}.edge.emph .line{stroke-width:2.6}
76
+ .edge.assumed .line{stroke:var(--warn);stroke-dasharray:6 4}.edge.st-added .line{stroke:var(--ok);stroke-width:2.2}
77
+ .edge.st-removed .line{stroke:var(--bad);stroke-dasharray:6 4;opacity:.7}
78
+ .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)}
79
+ .lifeline{stroke:var(--line);stroke-width:1.2;stroke-dasharray:4 4}.seqno{font-size:10px;fill:var(--muted)}
80
+ .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}
81
+ svg.focus .node,svg.focus .edge,svg.focus .group{opacity:.18;transition:opacity .15s}
82
+ 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}
83
+ svg .node.sel .shape{stroke:var(--hl)!important;stroke-width:3}svg .node:focus-visible .shape{stroke:var(--accent)!important;stroke-width:3}
84
+ @media (prefers-reduced-motion:no-preference){svg .edge.on.flow .line{stroke-dasharray:8 6;animation:flow 1s linear infinite}}
85
+ @keyframes flow{to{stroke-dashoffset:-28}}
86
+ .lane rect{fill:var(--group);stroke:var(--line)}.lane-line{stroke:var(--line);stroke-width:2}
87
+ .axis line{stroke:var(--muted);stroke-width:1}.axis text{font-size:11px;fill:var(--muted)}.axis .grid{stroke:var(--line);stroke-dasharray:2 4}
88
+ .brk{stroke:var(--muted);stroke-width:1.5;fill:none}.brk-label{font-size:10px;fill:var(--muted)}
89
+ .node.event .shape{stroke-width:2;fill:var(--panel)}.node.event .bar{fill:var(--group2);stroke-width:1.5}
90
+ .node.event .leader{stroke:var(--line);stroke-width:1}.node.event.approx .shape{stroke-dasharray:3 2}.node .label.tl{font-size:12px}.node .note.tl{font-size:10px}
91
+ .node.span .rowbg{fill:transparent}.node.span:hover .rowbg,.node.span.on .rowbg{fill:var(--group)}
92
+ .node.span .bar{stroke:none;opacity:.88}.node.span.crit .bar{stroke:var(--hl);stroke-width:2.5;opacity:1}
93
+ .node.span.err .bar{stroke:var(--bad);stroke-width:2.5}.node .label.wf{font-size:12px;font-weight:500}.node .note.wf{font-size:11px}
94
+ .node .label.italic{font-style:italic}.mk-open{fill:var(--canvas);stroke:var(--edge);stroke-width:1.2}.mk-line{fill:none;stroke:var(--edge);stroke-width:1.4}
95
+ .edge.tree .line{stroke:var(--line);stroke-width:1.5}
96
+ aside table.facts{border-collapse:collapse;font-size:13px;margin:6px 0}aside table.facts th{text-align:left;color:var(--muted);font-weight:400;padding:2px 10px 2px 0}
97
+ @media print{.bar,aside,.caption{display:none!important}.wrap{height:auto}#stage{border:0}#stage svg{position:static;height:auto}}
98
+ </style>
99
+ <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, "facts": 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, "facts": 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, "facts": 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, "facts": 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, "facts": 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, "facts": 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, "facts": 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, "facts": 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", "words": {"out": "Goes to", "in": "Comes from", "reach_out": "Everything it reaches", "reach_in": "Everything that reaches it"}, "auto_steps": []}</script>
100
+ <script>
101
+ (function(){
102
+ var DATA=JSON.parse(document.getElementById('diagram-data').textContent);var WD=DATA.words;
103
+ var svg=document.getElementById('map'),stage=document.getElementById('stage'),panel=document.getElementById('panel');
104
+ var root=document.documentElement;
105
+ var pref=localStorage.getItem('idg-theme')||root.getAttribute('data-theme-default');
106
+ if(pref==='auto'||!pref){pref=window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}
107
+ root.setAttribute('data-theme',pref);
108
+ var W=+svg.getAttribute('width'),H=+svg.getAttribute('height');svg.removeAttribute('width');svg.removeAttribute('height');
109
+ var vb={x:0,y:0,w:W,h:H};
110
+ function apply(){svg.setAttribute('viewBox',vb.x+' '+vb.y+' '+vb.w+' '+vb.h)}
111
+ 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()}
112
+ function zoom(f,cx,cy){var r=stage.getBoundingClientRect();if(cx==null){cx=r.width/2;cy=r.height/2}
113
+ 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;
114
+ vb.x=px-(px-vb.x)*k;vb.y=py-(py-vb.y)*k;vb.w=nw;vb.h=vb.h*k;apply()}
115
+ 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});
116
+ var drag=null;
117
+ 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)});
118
+ 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()});
119
+ stage.addEventListener('pointerup',function(){drag=null;stage.classList.remove('drag')});
120
+ // Lookups are Maps, never plain objects: ids come from the spec and the URL, and a key such as
121
+ // __proto__ must stay a key.
122
+ var nodes=new Map(),edges=new Map(),outE=new Map(),inE=new Map();
123
+ DATA.nodes.forEach(function(n){nodes.set(n.id,n);outE.set(n.id,[]);inE.set(n.id,[])});
124
+ 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)});
125
+ function el(kind,id){return document.getElementById(kind+'-'+id)}
126
+ function clear(){svg.classList.remove('focus');svg.querySelectorAll('.on,.sel,.flow').forEach(function(x){x.classList.remove('on','sel','flow')})}
127
+ function light(ns,es,flow){clear();svg.classList.add('focus');ns.forEach(function(i){var x=el('n',i);x&&x.classList.add('on')});
128
+ es.forEach(function(i){var x=el('e',i);if(x){x.classList.add('on');flow&&x.classList.add('flow')}})}
129
+ 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)}})}
130
+ return {n:Array.from(seen),e:es}}
131
+ 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;
132
+ 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]}));
133
+ nb.forEach(function(p){if(!prev.has(p[0])){prev.set(p[0],[u,p[1]]);q.push(p[0])}})}
134
+ 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()}}
135
+ function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
136
+ function evHtml(list){if(!list||!list.length)return '<p class="muted">No evidence cited.</p>';
137
+ 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:''):''));
138
+ 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>'}
139
+ 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>':'');
140
+ return '<li>'+t+v+(e.snippet?'<pre>'+esc(e.snippet)+'</pre>':'')+'</li>'}).join('')+'</ul>'}
141
+ var selected=null;
142
+ 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>';
143
+ 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>';
144
+ if(x.note)h+='<p>'+esc(x.note)+'</p>';if(x.detail)h+='<p>'+esc(x.detail)+'</p>';
145
+ 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>';
146
+ if(x.facts&&x.facts.length)h+='<table class="facts">'+x.facts.map(function(f){return '<tr><th>'+esc(f[0])+'</th><td>'+esc(f[1])+'</td></tr>'}).join('')+'</table>';
147
+ h+='<div class="acts"><button data-act="out">'+esc(WD.reach_out)+'</button><button data-act="in">'+esc(WD.reach_in)+'</button><button data-act="near">Neighbours</button></div>';
148
+ h+='<p class="muted" style="font-size:12px">Shift-click another part to see the path between them.</p>';
149
+ 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('');
150
+ 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('');
151
+ if(o)h+='<b>'+esc(WD.out)+'</b><ul>'+o+'</ul>';if(n)h+='<b>'+esc(WD.in)+'</b><ul>'+n+'</ul>';
152
+ h+='<b>Evidence</b>'+evHtml(x.evidence);
153
+ 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');
154
+ }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>';
155
+ if(x.detail)h+='<p>'+esc(x.detail)+'</p>';h+='<b>Evidence</b>'+evHtml(x.evidence);light([x.from,x.to],[id],true)}
156
+ panel.innerHTML=h;panel.classList.add('open');selected=kind==='n'?id:null;
157
+ panel.querySelector('.close').onclick=function(){panel.classList.remove('open');clear();selected=null};
158
+ panel.querySelectorAll('.jump').forEach(function(a){a.onclick=function(){show('n',a.getAttribute('data-n'))}});
159
+ panel.querySelectorAll('[data-act]').forEach(function(b){b.onclick=function(){var a=b.getAttribute('data-act');
160
+ if(a==='near'){show('n',id);return}var r=reach(id,a==='out'?1:-1);light(r.n,r.e,true);el('n',id).classList.add('sel')}})}
161
+ svg.addEventListener('click',function(e){var g=e.target.closest('.node');if(g){var id=g.getAttribute('data-id');
162
+ 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}
163
+ var ed=e.target.closest('.edge');if(ed){show('e',ed.getAttribute('data-id'));return}});
164
+ 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'))}});
165
+ document.addEventListener('keydown',function(e){if(e.key==='Escape'){panel.classList.remove('open');clear();selected=null}
166
+ if(e.target.tagName==='INPUT')return;if(e.key==='ArrowRight'&&tour)step(1);if(e.key==='ArrowLeft'&&tour)step(-1)});
167
+ var q=document.getElementById('find');q.addEventListener('input',function(){var v=q.value.trim().toLowerCase();if(!v){clear();return}
168
+ 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,[])});
169
+ document.getElementById('zin').onclick=function(){zoom(1/1.25)};document.getElementById('zout').onclick=function(){zoom(1.25)};document.getElementById('zfit').onclick=fit;
170
+ 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){}};
171
+ 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)}
172
+ 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');
173
+ c.querySelectorAll('.on,.sel,.flow').forEach(function(x){x.classList.remove('on','sel','flow')});
174
+ 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(';');
175
+ 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];
176
+ c.insertBefore(st,c.firstChild);return new XMLSerializer().serializeToString(c)}
177
+ var base=(DATA.slug||'diagram');window.interactiveDiagram={svg:standalone,mermaid:function(){return DATA.mermaid}};
178
+ document.getElementById('xsvg').onclick=function(){download(base+'.svg','image/svg+xml',standalone())};
179
+ 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)})};
180
+ img.src='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(standalone())};
181
+ document.getElementById('xmmd').onclick=function(){var t=DATA.mermaid;if(navigator.clipboard)navigator.clipboard.writeText(t).catch(function(){});download(base+'.mmd','text/plain',t)};
182
+ var tour=null,cap=document.getElementById('caption');
183
+ var steps=DATA.story&&DATA.story.length?DATA.story:(DATA.auto_steps&&DATA.auto_steps.length)?DATA.auto_steps:(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]}}):[]);
184
+ var tb=document.getElementById('tour');if(!steps.length){tb.style.display='none'}
185
+ function step(d){tour.i=Math.max(0,Math.min(steps.length-1,tour.i+d));var s=steps[tour.i];
186
+ 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);
187
+ 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')}
188
+ 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)};
189
+ document.getElementById('prev').onclick=function(){if(tour)step(-1)};document.getElementById('next').onclick=function(){if(!tour){tb.onclick()}else step(1)};
190
+ if(!steps.length){document.getElementById('prev').style.display='none';document.getElementById('next').style.display='none'}
191
+ function size(){var wr=document.querySelector('.wrap');var avail=Math.max(window.innerHeight-150,360);var r=stage.getBoundingClientRect();
192
+ 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'}
193
+ size();fit();window.addEventListener('resize',function(){size();fit()});
194
+ 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))});
195
+ if(kv.get('theme'))root.setAttribute('data-theme',kv.get('theme'));
196
+ 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'));
197
+ 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)}
198
+ 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)}
199
+ if(kv.get('step')&&steps.length){tour={i:-1};tb.textContent='Stop';step(+kv.get('step'))}}
200
+ hash();window.addEventListener('hashchange',hash);
201
+ })();
202
+ </script>
203
+ </body></html>