@caddis/cli 0.4.73 → 0.4.74

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.159",
3
+ "version": "1.3.160",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }
@@ -117,6 +117,7 @@
117
117
  | Skill | Path | When to Use |
118
118
  |-------|------|-------------|
119
119
  | Draw Io | `draw-io/` | draw.io diagram creation, editing, and review. Use for .drawio XML editing, PNG conversion, layout adjustment, and AWS icon usage. |
120
+ | Flow Chart From Counts | `flow-chart-from-counts/` | Turn a from-to-count edge list into a zoomable Mermaid flow chart page with counts, a legend and a render check. |
120
121
  | Mermaid Diagrams | `mermaid-diagrams/` | Software diagrams in Mermaid: class, sequence, flowchart, ERD, C4, state, git graph, gantt. Use for domain models, API flows, processes and database schemas. |
121
122
  | Particle Art | `particle-art/` | >- |
122
123
 
@@ -236,7 +236,10 @@ and 7.3. While the queue is frozen (section 5 step 1), do steps 3.1 to 3.3 only.
236
236
  Do only the merge or the `go`:
237
237
  - Target mode: `GH_TOKEN=$(gh auth token -u <account>) gh pr merge <n> --<merge_method> --match-head-commit <watched sha>`.
238
238
  The merge SHA is `mergeCommit.oid` from `gh pr view <n> --json state,mergeCommit`.
239
- - Interim mode: send `go <PR> <watched sha>`; the PR's session merges with the same command. Then
239
+ - Interim mode: send `go <PR> <watched sha>`, and on the next line: `I wrote the ack file
240
+ ~/.claude/merge-acks/<checkout folder name>/pr-<n>.json. Do not write, change or delete it.` The PR's
241
+ session merges with the same command. If its merge hook refuses because the ack file is wrong or
242
+ missing, write the ack file again (step 3.13) and send `go` once more. Then
240
243
  poll with a Bash command in `run_in_background`: `gh pr view <n> --json state,mergeCommit` once a
241
244
  minute, for at most 15 min, until `state` is `MERGED`. Take `mergeCommit.oid` as the merge SHA; the
242
245
  session's `merged` reply only tells you to look. No merge after 15 min: ask the PR's session once.
@@ -0,0 +1,193 @@
1
+ ---
2
+ name: flow-chart-from-counts
3
+ description: Turn a from-to-count edge list into a zoomable Mermaid flow chart page with counts, a legend and a render check.
4
+ ---
5
+
6
+ # flow-chart-from-counts
7
+
8
+ Turns counted paths (N cases went from step A to step B) into a flow chart page that a
9
+ reader can trust: counts on every arrow, a legend for the box types, a title with the date and
10
+ time zone, a note that names what is not drawn, and a viewer with zoom, pan and full screen.
11
+
12
+ Paths below are relative to the folder that holds this file.
13
+
14
+ ## When to use it
15
+
16
+ - "Draw how cases move through the process", "show the journey with counts", "flow chart of
17
+ calls / orders / tickets / sessions from this table".
18
+ - You have, or can query, rows of `from, to, count`, or an event table with one row per step.
19
+ - A Sankey or a funnel was asked for, but the paths branch and rejoin: a flow chart shows that
20
+ better.
21
+
22
+ Use something else for:
23
+
24
+ - An interactive page from a JSON model with lanes, playback and an inspector: `flow-studio`.
25
+ - Mermaid syntax for other diagram kinds (sequence, ERD, class): `mermaid-diagrams`.
26
+ - A diagram of a SQL object's structure: `db-diagram`.
27
+
28
+ ## What is in the folder
29
+
30
+ | file | use |
31
+ |---|---|
32
+ | `scripts/edges_to_mermaid.py` | CSV edge list to first-draft Mermaid source (standard library only) |
33
+ | `assets/viewer-template.html` | page with the zoom/pan/full-screen viewer, theme tokens and one example chart |
34
+ | `scripts/check-render.mjs` | headless render check: fails on no SVG, error text, too few nodes, page errors; saves screenshots |
35
+ | `references/edge-list-sql.md` | SQL pattern: event table to edge list and box counts, and the checks to run on it |
36
+
37
+ ## Process
38
+
39
+ 1. **Get the edge list.** From an event table, use `references/edge-list-sql.md`. Run its checks
40
+ (START rows add up to the cases; path occurrences in equal path occurrences out per step).
41
+ Count arrows as distinct cases per pair. Get the box counts (distinct cases per step) at the
42
+ same time.
43
+ 2. **Name the node types** with the reader, or from the process owner's own glossary: for
44
+ example automatic step, in-scope step, optional step, hand-off, end. Write a `nodes.csv`
45
+ (`name,type,count`).
46
+ 3. **Make the first draft:**
47
+ ```
48
+ python scripts/edges_to_mermaid.py edges.csv --nodes nodes.csv --min 25 \
49
+ --in-scope scope --optional choice --out chart.mmd --strict
50
+ ```
51
+ `--out` writes UTF-8 (do not use `>` in Windows PowerShell 5.1: it can write UTF-16). The note
52
+ on hidden paths goes to stderr. Keep it: it goes under the chart. `--strict` exits 1 on a
53
+ `warning:` line, which means an arrow is larger than its box: fix the query before you draw.
54
+ Duplicate `from,to` rows are added together. Node types become classes named `t_<type>`.
55
+ 4. **Put it in the page.** Copy `assets/viewer-template.html`. Paste the source into the
56
+ `<script type="text/plain">` block. Set `data-title` (what, which day) and `data-sub` (the UTC
57
+ window, the unit of the counts, the minimum path size). Replace the legend and the note.
58
+ For more charts, add one `.fv` block and one source script per chart.
59
+ 5. **Render and check:**
60
+ ```
61
+ node <skill folder>/scripts/check-render.mjs page.html --out shots [--channel msedge]
62
+ ```
63
+ Run it from a folder whose `node_modules` has `playwright`. It exits 1 when a check fails.
64
+ 6. **Look at the screenshots** (`chart-N.png` is the chart at Fit, `chart-1-full.png` is full
65
+ screen) and run the tidy checklist below. If a rule fails, change the layout with the layout
66
+ fixes, and go back to step 5.
67
+ 7. **Show** the page and the screenshots only when every checklist rule passes. Say which labels
68
+ are your reading (see Wording). If a rule cannot be met, say which rule and why.
69
+
70
+ ## Tidy checklist (the chart must pass it before you show it)
71
+
72
+ A clean, tidy chart is an acceptance rule, not a nice-to-have. A reader who has to trace a
73
+ line across the page stops trusting the numbers on it.
74
+
75
+ 1. **No crossing lines.** No line passes through a box or a label. Use the tree rule with
76
+ dashed stubs, and split shared targets (layout fixes 1 and 2).
77
+ 2. **No overlaps.** No label or box sits on another. Edge labels are short: `(1,234)` and at
78
+ most a few words. Long text goes in the note under the chart, not on the chart.
79
+ 3. **Rows and side flows.** Boxes of the same kind line up in one row where the flow allows
80
+ (layout fix 5). A side flow sits in its own dashed box at the bottom, pushed down with an
81
+ invisible link (layout fix 4).
82
+ 4. **One look per type.** One box shape per type, a small fixed palette (5 to 7 classes), a
83
+ legend that matches the chart exactly (stubs included), and the title with the date and the
84
+ time zone on the chart.
85
+ 5. **Readable at Fit and at full screen** on a 1280 x 900 screen. Nothing is clipped.
86
+ 6. **Checked by eye.** You opened both screenshots and checked rules 1 to 5 on them. The render
87
+ check's warnings (box overlaps, labels on boxes) help, but they cannot see a crossing line.
88
+
89
+ ## Count rules
90
+
91
+ - **Arrows and boxes both count distinct cases.** An arrow says how many distinct cases took
92
+ that path (`COUNT(DISTINCT case_id)` per from-to pair). A box says how many distinct cases
93
+ reached it. Do not draw path occurrences (`COUNT(*)`): a case that loops counts twice, and a
94
+ reader who sees a box of 8,538 followed by an arrow of 8,541 stops trusting the chart. Label
95
+ every number as cases.
96
+ - **Self-check: no arrow into or out of a box exceeds that box's count.** If one does, the
97
+ query counts occurrences. The converter warns on stderr when this happens.
98
+ - **Say in the note that arrows into a box can sum to a little more than the box**, because a
99
+ case can reach it by two paths.
100
+ - **Put counts in brackets, with thousands separators:** `(1,234)`. In Mermaid an edge label that
101
+ holds brackets must be quoted: `A -->|"(1,234)"| B`. Unquoted, `(` starts a node shape and the
102
+ chart fails to parse.
103
+ - **Hide small paths with a stated minimum.** Choose a minimum (for example 25), drop the paths
104
+ under it, and write a note that names them and their total: "Not drawn because fewer than 25
105
+ cases took them: X to Y (12), ... (41 in total)." A chart that drops paths without a note looks
106
+ complete and is not.
107
+ - **Merge small boxes into one named box, and say so.** When you merge, the count is a sum of
108
+ rounded or filtered parts: write `(about 600)`, and say in the note what the box holds.
109
+ - **Put the date and the time zone on the chart.** Write the title as HTML inside the zoomable
110
+ stage (the template does this from `data-title` and `data-sub`), so it stays with the chart in
111
+ full screen and in screenshots. Do not use a frontmatter `title:` as well: it would show a second title. Give the
112
+ local day and the UTC window: `Mon 5 Oct 2026 (4 Oct 23:00 to 5 Oct 23:00 UTC)`.
113
+ - **Name the unit.** "Counts in brackets are calls / orders / cases." A count with no unit gets
114
+ read as the wrong one.
115
+
116
+ ## Node types and the legend
117
+
118
+ - One `classDef` per node type, and one `class id1,id2 type;` line per type. The converter writes
119
+ these from `nodes.csv`.
120
+ - **In-scope boxes get a thick border** (`stroke-width:3px`), so the reader finds the subject of
121
+ the chart first.
122
+ - **Optional or owner-choice boxes get a dashed border** (`stroke-dasharray:6 4`): a step that is
123
+ a decision still open, or one that runs only for some cases.
124
+ - End boxes share one quiet grey class. Stubs share one class: white fill, thin dashed grey
125
+ border (`stroke-dasharray:3 3`), so they read as pointers, not steps.
126
+ - Put a legend above the chart with one swatch per type, with the same fill, border width and
127
+ dash as the chart. Use light fills with dark text. Keep the chart canvas light in dark mode
128
+ too (the template's `--canvas` token does this), or the dark text loses contrast.
129
+
130
+ ## Layout fixes that work
131
+
132
+ Mermaid's layout (dagre) puts each node on a rank and orders each rank to cut crossings. It does
133
+ not know what the reader looks for. These fixes move it.
134
+
135
+ 1. **Draw the chart as a tree.** Give every box exactly one parent edge: the biggest path into
136
+ it. For every other path into that box, add a small dashed stub under its own source,
137
+ labelled with the target and the count: `B --> toB_C(["to C (412)"])`. Shared targets and
138
+ back-edges are what make the layout cross lines; a tree with stubs has none. Add a legend
139
+ entry for stubs ("dashed: the path continues at the box named"). The converter does this by
140
+ default (`--no-tree` turns it off).
141
+ 2. **Give each source its own end box.** One shared "Ends" box pulls a long line from every step
142
+ across the chart. Write `A --> endA(["Ends"])` and `B --> endB(["Ends"])` instead. The
143
+ converter does this. Apply the same fix to any shared target that sits far from its sources:
144
+ a copy of the box next to each source is easier to read than one box with crossing lines.
145
+ 3. **Do not draw a loop-back as an edge.** A line back up the chart crosses everything. End the
146
+ path in a stub: `A --> backA(["back to Triage (190)"])`. The converter does this for every
147
+ edge that points back to a step on the path.
148
+ 4. **Put a side flow in its own subgraph and push it down.** For example the steps after a
149
+ hand-off. Draw them in `subgraph AFTER[...] direction LR ... end` and add an invisible link
150
+ from a low node of the main flow: `X2 ~~~ AFTER`. The subgraph then sits under the main
151
+ flow, not beside it.
152
+ 5. **Put same-kind boxes in one row.** Group them in a subgraph, and lengthen the edges into it
153
+ (`---->`, one dash per extra rank) so they land on the same rank.
154
+ 6. **Replace a long jump with a short stub or a dotted link.** When an edge must reach a box
155
+ far away, end it in a small stub node near the source, or draw it dotted (`-.->`) with a
156
+ label such as `"few"`, so the eye can tell it is secondary.
157
+ 7. **Order the edges from a node in the order you want the boxes, left to right**, and declare
158
+ the nodes in the same order. Dagre takes this as its first guess. It can still swap them to
159
+ cut crossings: check the screenshot.
160
+ 8. **Quote every label that holds brackets, quotes or `<br/>`:** `["Text<br/>(12)"]`,
161
+ `|"(12)"|`. Write a quote inside a label as `#quot;`.
162
+
163
+ Change one thing at a time and re-render. Two fixes at once hide which one helped.
164
+
165
+ ## Wording
166
+
167
+ - **Use the process owner's words for box labels** when a glossary or a list of step names
168
+ exists. A reader checks the chart against those names.
169
+ - **Write neutral endings.** "Case ends" or "Call ends" means the data shows no further step.
170
+ Do not write a guess such as "hangs up" or "gives up": the data does not say who ended it.
171
+ Say what "ends" means in the note.
172
+ - **Mark your own reading.** When a label or a grouping is your interpretation, not a name from
173
+ the source, say so in the note ("'Team queues' is our grouping of four queues").
174
+
175
+ ## Verify
176
+
177
+ `scripts/check-render.mjs` fails (exit 1) when a chart has no SVG, shows an error message, has
178
+ fewer nodes than `--min-nodes`, or the page throws. Make it fail once on purpose (break one
179
+ line of the source) to see that it can. It also counts box overlaps and arrow labels on boxes
180
+ as warnings. Those are hints: open the screenshots and run the tidy checklist by eye, because
181
+ the check cannot see a crossing line.
182
+
183
+ Also check the numbers, not only the drawing: the START count equals the cases in the window,
184
+ and no arrow into or out of a box is larger than the box (the converter warns when one is).
185
+
186
+ ## Publishing
187
+
188
+ - As an Artifact: follow the artifact-design rules. Mermaid loads from
189
+ `https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js` (an exact version on the
190
+ allowed CDN). The template already has theme tokens for light and dark, an explicit body
191
+ background, and a 16px side gutter for phone width.
192
+ - In an app: render with the app's own Mermaid package and keep the same source, title and note.
193
+ Keep the count rules; they are what makes the chart trustworthy.
@@ -0,0 +1,190 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Case Flow Chart</title>
7
+ <style>
8
+ /* Flow chart page from the flow-chart-from-counts skill.
9
+ One .fv block per chart. Each block names a <script type="text/plain"> that holds the Mermaid source.
10
+ --canvas stays light in dark mode on purpose: the coloured boxes use dark text and need a light ground. */
11
+ :root{
12
+ --bg:#faf8f5; --surface:#ffffff; --ink-1:#1c1917; --ink-2:#57534e; --ink-3:#78716c; --line:#e7e2da; --canvas:#fbfaf8;
13
+ --sans:"Segoe UI",system-ui,-apple-system,Arial,sans-serif; --mono:Consolas,ui-monospace,"Courier New",monospace;
14
+ }
15
+ @media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
16
+ --bg:#23252f; --surface:#2c2f3b; --ink-1:#f3f1ec; --ink-2:#c9c6c0; --ink-3:#a19d97; --line:#3f4250; --canvas:#f4f2ee; color-scheme:dark}}
17
+ :root[data-theme="dark"]{
18
+ --bg:#23252f; --surface:#2c2f3b; --ink-1:#f3f1ec; --ink-2:#c9c6c0; --ink-3:#a19d97; --line:#3f4250; --canvas:#f4f2ee; color-scheme:dark}
19
+ *{box-sizing:border-box}
20
+ body{margin:0;background:var(--bg);color:var(--ink-1);font-family:var(--sans);font-size:14px;line-height:1.55}
21
+ .wrap{max-width:1000px;margin:0 auto;padding:24px 16px 56px;display:grid;gap:20px}
22
+ h1{font-size:22px;margin:0}
23
+ p{margin:0;max-width:74ch}
24
+ .small{font-size:13px;color:var(--ink-2)}
25
+ .legendrow{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--ink-2)}
26
+ .sw{display:inline-block;width:12px;height:12px;border-radius:3px;margin-right:5px;vertical-align:-2px;border:1px solid var(--ink-3)}
27
+ .sw.thick{border-width:3px}
28
+ .sw.dash{border-style:dashed;border-width:2px}
29
+
30
+ /* Viewer */
31
+ .fv{border:1px solid var(--line);border-radius:8px;background:var(--canvas);overflow:hidden;display:flex;flex-direction:column}
32
+ .fv-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--line);background:var(--surface)}
33
+ .fv-hint{font-size:12px;color:var(--ink-3)}
34
+ .fv-btns{display:flex;gap:6px;align-items:center}
35
+ .fv-btns button{font:inherit;font-size:13px;min-width:34px;padding:5px 10px;border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--ink-1);cursor:pointer}
36
+ .fv-btns button:hover{border-color:var(--ink-3)}
37
+ .fv-btns button:focus-visible{outline:2px solid #2563eb;outline-offset:2px}
38
+ .fv-pct{font-family:var(--mono);font-size:12px;color:var(--ink-2);min-width:44px;text-align:center}
39
+ .fv-view{position:relative;height:70vh;min-height:420px;overflow:hidden;cursor:grab;touch-action:pan-y;background:var(--canvas)}
40
+ .fv-view.drag{cursor:grabbing}
41
+ .fv-stage{position:absolute;left:0;top:0;transform-origin:0 0}
42
+ .fv-stage > p{color:#57534e;padding:12px}
43
+ /* The title is HTML inside the zoomable stage, so it zooms with the chart and stays in full screen and screenshots. */
44
+ .fv-title{font-size:22px;line-height:1.35;color:#1c1917;padding:8px 0 14px;text-align:center}
45
+ .fv-title b{font-weight:700}
46
+ .fv-title span{display:block;font-size:15px;color:#57534e}
47
+ .fv-stage svg{max-width:none!important;display:block}
48
+ .fv.expanded{position:fixed;inset:0;z-index:50;border-radius:0}
49
+ .fv.expanded .fv-view{height:auto;flex:1}
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="wrap">
54
+ <h1>How support cases moved on Mon 5 Oct 2026</h1>
55
+ <p class="small">Example data. Replace the title, the legend, the chart source and the note.</p>
56
+
57
+ <div class="legendrow" aria-label="Legend">
58
+ <span><span class="sw" style="background:#e7e5e4"></span>Automatic step</span>
59
+ <span><span class="sw thick" style="background:#bbf7d0;border-color:#15803d"></span>In scope (thick border)</span>
60
+ <span><span class="sw dash" style="background:#fde68a;border-color:#a16207"></span>Optional, owner choice (dashed)</span>
61
+ <span><span class="sw" style="background:#f6e7d6"></span>Hand-off to a team</span>
62
+ <span><span class="sw" style="background:#ffffff;border-color:#57534e;border-width:2px"></span>Start</span>
63
+ <span><span class="sw" style="background:#eceae6"></span>Case ends</span>
64
+ <span><span class="sw dash" style="background:#ffffff;border-color:#a8a29e;border-width:1px"></span>Stub: the path continues at the box named</span>
65
+ </div>
66
+
67
+ <!-- One chart. data-src = id of the script that holds the Mermaid source.
68
+ data-title and data-sub are plain text (set with textContent). data-label is the accessible name. -->
69
+ <div class="fv" data-src="chart1"
70
+ data-title="Support cases: Mon 5 Oct 2026"
71
+ data-sub="(4 Oct 23:00 to 5 Oct 23:00 UTC). Counts in brackets are cases. Paths under 20 cases are not drawn."
72
+ data-label="Flow of a support case on 5 October, from intake to self-service, team queues and case ends">
73
+ <div class="fv-bar">
74
+ <span class="fv-hint">Drag to move. Use + and − or Ctrl + scroll to zoom.</span>
75
+ <span class="fv-btns">
76
+ <button type="button" data-z="out" aria-label="Zoom out">−</button>
77
+ <span class="fv-pct">100%</span>
78
+ <button type="button" data-z="in" aria-label="Zoom in">+</button>
79
+ <button type="button" data-z="fit">Fit</button>
80
+ <button type="button" data-z="full">Full screen</button>
81
+ </span>
82
+ </div>
83
+ <div class="fv-view"><div class="fv-stage"><p class="small">Drawing the flow chart…</p></div></div>
84
+ </div>
85
+ <script type="text/plain" id="chart1">
86
+ flowchart TD
87
+ S(["Cases opened<br/>(2,400)"]) --> I["INTAKE<br/>Web form"]
88
+ I -->|"(180)"| X1(["Case ends"])
89
+ I -->|"(2,220)"| T["TRIAGE<br/>Rules engine<br/>(2,220)"]
90
+ T -->|"(1,300)"| A["SELF-SERVICE<br/>Account help<br/>(1,300)"]
91
+ T -->|"(about 600)"| Q["Team queues"]
92
+ T -->|"(320)"| C["CHECK, owner choice<br/>Fraud check"]
93
+ C -->|"(300)"| Q2["Fraud team queue"]
94
+ A -->|"(1,100)"| X2(["Case ends after self-service"])
95
+ A --> B(["back to Triage (190)"])
96
+ subgraph AFTER["After a team picks up a case"]
97
+ direction LR
98
+ AG["Agent"] -->|"(700)"| X3(["Case ends"])
99
+ end
100
+ X2 ~~~ AFTER
101
+ classDef auto fill:#e7e5e4,stroke:#78716c,color:#1c1917;
102
+ classDef scope fill:#bbf7d0,stroke:#15803d,stroke-width:3px,color:#1c1917;
103
+ classDef choice fill:#fde68a,stroke:#a16207,stroke-width:2px,stroke-dasharray:6 4,color:#1c1917;
104
+ classDef queue fill:#f6e7d6,stroke:#b45309,color:#1c1917;
105
+ classDef endn fill:#eceae6,stroke:#78716c,color:#1c1917;
106
+ classDef startn fill:#ffffff,stroke:#57534e,stroke-width:2px,color:#1c1917;
107
+ classDef stub fill:#ffffff,stroke:#a8a29e,stroke-dasharray:3 3,color:#57534e;
108
+ class I,T auto;
109
+ class A scope;
110
+ class C choice;
111
+ class Q,Q2,AG queue;
112
+ class X1,X2,X3 endn;
113
+ class S startn;
114
+ class B stub;
115
+ style AFTER fill:#fbfaf8,stroke:#a8a29e,stroke-dasharray:4 4,color:#57534e;
116
+ </script>
117
+ <p class="small">Every number counts distinct cases: an arrow counts the cases that took that path, a box counts the cases that reached it. Arrows into a box can sum to a little more than the box, because a case can reach it by two paths. "Case ends" means the data shows no further step; it does not say why. "Team queues" merges four small queues (about 600). Not drawn: 3 paths with fewer than 20 cases each (41 in total).</p>
118
+ </div>
119
+
120
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js" integrity="sha512-6a80OTZVmEJhqYJUmYd5z8yHUCDlYnj6q9XwB/gKOEyNQV/Q8u+XeSG59a2ZKFEHGTYzgfOQKYEBtrZV7vBr+Q==" crossorigin="anonymous"></script>
121
+ <script>
122
+ (function(){
123
+ if(!window.mermaid){
124
+ document.querySelectorAll('.fv-stage').forEach(function(s){s.innerHTML='<p class="small">The chart library did not load, so the flow chart cannot be drawn.</p>';});
125
+ return;
126
+ }
127
+ try{
128
+ mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:'base',
129
+ themeVariables:{fontFamily:'Segoe UI, system-ui, sans-serif',fontSize:'15px',lineColor:'#57534e',primaryTextColor:'#1c1917',edgeLabelBackground:'#ffffff',background:'#fbfaf8'},
130
+ flowchart:{htmlLabels:true,curve:'basis',nodeSpacing:40,rankSpacing:55,padding:10}});
131
+ }catch(e){console.error('mermaid.initialize failed',e);}
132
+ var n=0;
133
+ function viewer(fv){
134
+ var view=fv.querySelector('.fv-view'),stage=fv.querySelector('.fv-stage'),pct=fv.querySelector('.fv-pct'),full=fv.querySelector('button[data-z="full"]');
135
+ var k=1,x=0,y=0,w=0,h=0,fitK=1;
136
+ function msg(t){stage.innerHTML='<p class="small"></p>';stage.firstChild.textContent=t;}
137
+ // Page scroll stays free on a phone until the chart is zoomed in or in full screen; then a drag pans the chart.
138
+ function touch(){view.style.touchAction=(fv.classList.contains('expanded')||k>fitK*1.01)?'none':'pan-y';}
139
+ function apply(){touch();stage.style.transform='translate('+x+'px,'+y+'px) scale('+k+')';pct.textContent=Math.round(k*100)+'%';}
140
+ function fit(){var vw=view.clientWidth,vh=view.clientHeight;if(!w)return;k=Math.min(vw/w,vh/h)*0.96;fitK=k;x=(vw-w*k)/2;y=(vh-h*k)/2;apply();}
141
+ function zoomAt(f,cx,cy){var nk=Math.max(Math.min(0.15,fitK),Math.min(4,k*f));x=cx-(cx-x)*nk/k;y=cy-(cy-y)*nk/k;k=nk;apply();}
142
+ function title(){
143
+ var d=document.createElement('div');d.className='fv-title';
144
+ var b=document.createElement('b');b.textContent=fv.dataset.title||'';d.appendChild(b);
145
+ if(fv.dataset.sub){var s=document.createElement('span');s.textContent=fv.dataset.sub;d.appendChild(s);}
146
+ return d;
147
+ }
148
+ var src=document.getElementById(fv.dataset.src);
149
+ var done=Promise.resolve();
150
+ if(!src){msg('The flow chart source is missing.');}
151
+ else try{
152
+ done=mermaid.render('flowsvg'+(n++),src.textContent).then(function(r){
153
+ stage.innerHTML=r.svg;stage.insertBefore(title(),stage.firstChild);
154
+ var svg=stage.querySelector('svg');
155
+ var vb=svg.viewBox.baseVal;w=vb&&vb.width?vb.width:svg.getBBox().width;h=vb&&vb.height?vb.height:svg.getBBox().height;
156
+ svg.setAttribute('width',w);svg.setAttribute('height',h);svg.style.maxWidth='none';
157
+ w=Math.max(w,stage.scrollWidth);h=stage.scrollHeight;
158
+ svg.setAttribute('role','img');svg.setAttribute('aria-label',fv.dataset.label||'Flow chart');
159
+ fit();
160
+ }).catch(function(e){console.error(e);msg('The flow chart could not be drawn: '+e.message);});
161
+ }catch(e){console.error(e);msg('The flow chart could not be drawn: '+e.message);}
162
+ function toggle(){var on=!fv.classList.contains('expanded');fv.classList.toggle('expanded',on);full.textContent=on?'Close':'Full screen';
163
+ document.body.style.overflow=on?'hidden':'';
164
+ // Real full screen when the browser allows it; the fixed-position .expanded class is the fallback.
165
+ try{if(on&&fv.requestFullscreen)fv.requestFullscreen().catch(function(){});else if(!on&&document.fullscreenElement)document.exitFullscreen().catch(function(){});}catch(e){}
166
+ setTimeout(fit,60);}
167
+ fv.addEventListener('click',function(ev){var b=ev.target.closest('button[data-z]');if(!b)return;var c=[view.clientWidth/2,view.clientHeight/2];
168
+ if(b.dataset.z==='in')zoomAt(1.25,c[0],c[1]);else if(b.dataset.z==='out')zoomAt(0.8,c[0],c[1]);else if(b.dataset.z==='fit')fit();else toggle();});
169
+ document.addEventListener('fullscreenchange',function(){if(!document.fullscreenElement&&fv.classList.contains('expanded')){fv.classList.remove('expanded');full.textContent='Full screen';document.body.style.overflow='';setTimeout(fit,60);}});
170
+ document.addEventListener('keydown',function(e){if(e.key==='Escape'&&fv.classList.contains('expanded'))toggle();});
171
+ // Plain scroll keeps scrolling the page; Ctrl + scroll (or any scroll in full screen) zooms.
172
+ view.addEventListener('wheel',function(e){if(!(e.ctrlKey||e.metaKey||fv.classList.contains('expanded')))return;e.preventDefault();var r=view.getBoundingClientRect();zoomAt(e.deltaY<0?1.12:0.89,e.clientX-r.left,e.clientY-r.top);},{passive:false});
173
+ var pts={};
174
+ view.addEventListener('pointerdown',function(e){view.setPointerCapture(e.pointerId);pts[e.pointerId]=[e.clientX,e.clientY];view.classList.add('drag');});
175
+ view.addEventListener('pointermove',function(e){if(!pts[e.pointerId])return;var p=pts[e.pointerId],ids=Object.keys(pts);
176
+ if(ids.length===2){var o=pts[ids[0]==e.pointerId?ids[1]:ids[0]];var d0=Math.hypot(p[0]-o[0],p[1]-o[1]);var d1=Math.hypot(e.clientX-o[0],e.clientY-o[1]);var r=view.getBoundingClientRect();if(d0>0)zoomAt(d1/d0,(e.clientX+o[0])/2-r.left,(e.clientY+o[1])/2-r.top);}
177
+ else{x+=e.clientX-p[0];y+=e.clientY-p[1];apply();}
178
+ pts[e.pointerId]=[e.clientX,e.clientY];});
179
+ function up(e){delete pts[e.pointerId];if(!Object.keys(pts).length)view.classList.remove('drag');}
180
+ view.addEventListener('pointerup',up);view.addEventListener('pointercancel',up);
181
+ window.addEventListener('resize',fit);
182
+ return done;
183
+ }
184
+ // Render one chart after another, so each chart finishes before the next one starts.
185
+ var all=document.querySelectorAll('.fv[data-src]'),i=0;
186
+ (function next(){if(i<all.length){Promise.resolve(viewer(all[i++])).then(next,next);}})();
187
+ })();
188
+ </script>
189
+ </body>
190
+ </html>
@@ -0,0 +1,105 @@
1
+ # Edge list from an event table (SQL pattern)
2
+
3
+ Use this when the steps of each case are rows in an event table. The output is the edge list that
4
+ `scripts/edges_to_mermaid.py` reads: `from,to,count`, plus a box-count list `name,count`.
5
+
6
+ The SQL is ANSI with window functions. It runs on most SQL engines with small changes
7
+ (parameter style, date functions).
8
+
9
+ ## Input shape
10
+
11
+ One row per step a case took:
12
+
13
+ | column | meaning |
14
+ |-------------|-----------------------------------------------------------------------------|
15
+ | `case_id` | the unit you count: a call, an order, a ticket, a session |
16
+ | `step` | the step name, in the words of the team that owns the process |
17
+ | `step_time` | when the step started, in UTC |
18
+ | `step_seq` | a tie-breaker when two steps share one timestamp (an event id or a counter) |
19
+
20
+ ## Query
21
+
22
+ ```sql
23
+ -- 1. Choose the cases by their START time in the window, then take ALL their steps.
24
+ -- If you filter steps by time instead, a case that crosses the window edge loses its
25
+ -- first or last steps, and the chart shows false starts and false ends.
26
+ WITH cases AS (
27
+ SELECT case_id
28
+ FROM events
29
+ GROUP BY case_id
30
+ HAVING MIN(step_time) >= :window_start_utc
31
+ AND MIN(step_time) < :window_end_utc
32
+ ),
33
+ ordered AS (
34
+ SELECT e.case_id, e.step, e.step_time, e.step_seq,
35
+ LAG(e.step) OVER (PARTITION BY e.case_id ORDER BY e.step_time, e.step_seq) AS prev_step
36
+ FROM events e
37
+ JOIN cases c ON c.case_id = e.case_id
38
+ ),
39
+ -- 2. Collapse consecutive repeats: A, A, B becomes A, B. A repeat is not a path.
40
+ collapsed AS (
41
+ SELECT case_id, step, step_time, step_seq
42
+ FROM ordered
43
+ WHERE prev_step IS NULL OR prev_step <> step
44
+ ),
45
+ -- 3. The next step of each step. No next step means the case ends: 'END'.
46
+ pairs AS (
47
+ SELECT case_id,
48
+ step AS from_step,
49
+ COALESCE(LEAD(step) OVER (PARTITION BY case_id ORDER BY step_time, step_seq), 'END') AS to_step,
50
+ ROW_NUMBER() OVER (PARTITION BY case_id ORDER BY step_time, step_seq) AS rn
51
+ FROM collapsed
52
+ )
53
+ -- 4. Count DISTINCT cases per pair: the same unit as the boxes. Add one START row per case.
54
+ -- path_count (times taken) is kept only to compare; do not draw it.
55
+ SELECT from_step AS "from", to_step AS "to",
56
+ COUNT(DISTINCT case_id) AS "count",
57
+ COUNT(*) AS path_count
58
+ FROM pairs
59
+ GROUP BY from_step, to_step
60
+ HAVING COUNT(DISTINCT case_id) >= :min_cases
61
+ UNION ALL
62
+ SELECT 'START', from_step, COUNT(*), COUNT(*)
63
+ FROM pairs
64
+ WHERE rn = 1
65
+ GROUP BY from_step
66
+ ORDER BY 3 DESC;
67
+ ```
68
+
69
+ Box counts (distinct cases per step), from the same `collapsed` set:
70
+
71
+ ```sql
72
+ SELECT step AS name, COUNT(DISTINCT case_id) AS count
73
+ FROM collapsed
74
+ GROUP BY step;
75
+ ```
76
+
77
+ ## What the numbers mean
78
+
79
+ - **Draw distinct cases on the arrows, the same unit as the boxes.** `count` is
80
+ `COUNT(DISTINCT case_id)` per pair. `path_count` (`COUNT(*)`) counts each time a path was taken,
81
+ so a case that loops (A, B, A, B) counts twice. A business reader then sees a box of 8,538
82
+ followed by an arrow of 8,541 and stops trusting the chart. On one measured day `COUNT(*)`
83
+ overstated an arrow by 4%. Keep `path_count` only to compare.
84
+ - **Self-check: no arrow into or out of a box exceeds the box's distinct count.** If one does,
85
+ the query counts occurrences, not cases. The converter warns on this when it has box counts.
86
+ - **Arrows into a box can sum to a little more than the box** when a case reaches it by two
87
+ paths. That is correct; say so in the note under the chart.
88
+ - **`HAVING` hides small paths.** Apply the threshold to either count, but label every number on
89
+ the chart as cases. Run the query once without it, or sum the hidden rows, so the
90
+ note under the chart can name what is not drawn and how many cases that is. A chart that hides
91
+ paths without saying so looks complete and is not.
92
+ - **The tie-breaker matters.** Without `step_seq`, two steps at one timestamp come out in any
93
+ order, and `LAG`/`LEAD` give different paths on each run. Check: run the query twice and compare.
94
+ - **Time zone.** Filter in UTC. When the reader thinks in local days, convert the local day to a
95
+ UTC window first, and put both on the chart title, for example
96
+ `Mon 5 Oct 2026 (4 Oct 23:00 to 5 Oct 23:00 UTC)`.
97
+
98
+ ## Check the edge list before you draw it
99
+
100
+ - The `START` rows add up to the number of cases in the window.
101
+ - Before the `HAVING` filter, for each step the paths in equal the paths out (with `END`
102
+ counted as out). A gap means a filter dropped rows, or a step name differs in spelling
103
+ between rows.
104
+ - Run the converter: its note lists every hidden path. Read that list. A large hidden total means
105
+ the minimum is too high, or many small steps need to be merged into one named box.