# Pipeline in motion

One CSV, one prompt, no design tool, no video editor. A CRM deal export becomes the numbers for a
Monday pipeline meeting, and a script draws them as a 30-second film.

The report answers three questions, in order: what moved since last week (forward, back, won, lost, new),
which open deals have had no activity for 14 days or more, and where open pipeline stands against target.

## What is in here

| File | What it is |
|---|---|
| `prompt.md` | The prompt. Paste your export where it says `{{CSV}}`, fill in the report date and the target. |
| `prompt-bare.md` | For comparison: the same opening, the same three questions and the same answer shape, without the column and format notes and without the nine rules. |
| `samples/*.csv` | Four invented exports. Every company is invented. A, B and C (48, 86 and 117 rows) are generated, with the mess real exports have: duplicate rows, a duplicate in other case and spacing, deals moved back a stage, blank amounts, blank activity dates, close dates in the past, and in B a Danish Excel export with semicolons, Danish headers, `dd-mm-yyyy` and `125.000,00`. D (40 rows) is written by hand, one or two rows per branch of the nine rules: ties on activity date, blank against dated, the 14-day and 7-day edges, a tie on amount in the top 3, close date on the report date, `1.250.000` and `125.000,00` in a comma file, and coverage of exactly 87.5 %. |
| `key.py`, `key.json` | The answer key, computed with pandas from the four files. Needs Python 3 and pandas. |
| `runs/<arm>/<file>.record.json` | Every reply Claude gave, verbatim, with the model requested, the models the CLI reports using, and the command. `<file>.json` is the answer parsed from that reply; it is missing where there was no answer. |
| `run_cli.py`, `make_prompts.py` | How the runs were made: the prompt filled in per file, then `claude -p --tools '' --model <model>` from an empty folder, the prompt on stdin, nothing else. No tools, so the answer can only come from reading the export. |
| `score.py`, `facts.json` | Scores every run figure by figure against the key. |
| `v2-sha256.txt` | The SHA-256 of the key, the four exports and both prompts as they were when the runs started. |
| `render.mjs`, `scene.html` | The render script and the scene it draws. |
| `test-render.mjs` | Tests for the check inside the render script: the recomputation equals the key on all four files, a planted wrong figure is caught, the same export in another shape gives the same figures, an unreadable amount or date is warned about, and a broken file (no rows, too few columns, junk) is warned about rather than drawn as an empty report. `node test-render.mjs`. |

The scripts' comments show the paths they have in the build repository (`brinvik/lab/...`). Here, run them
from this folder.

## The result

Four arms, four files, one run each: Sonnet 5.5 twice and Haiku 5.5 with the prompt, Sonnet 5.5 with the
bare prompt. One call was lost: the first Sonnet 5.5 run 1 call on file C finished, the harness's
parser crashed on its reply before the record was written, and the record in `runs/sonnet-1/` is the rerun. 19 figures are scored per file. They are not 19 independent tests: `deals` is rows minus
duplicates, and most counts have a kroner twin.

| | A | B | C | D |
|---|---|---|---|---|
| Sonnet 5.5, run 1 | 19 | 19 | no answer | 19 |
| Sonnet 5.5, run 2 | 19 | 19 | 19 | 19 |
| Haiku 5.5 | 19 | 19 | 19 | 19 |
| Sonnet 5.5, bare prompt | 19 | no answer | no answer | 17 |

Where a figure was given, it was right, with one exception: on file D the bare prompt broke the tie in the
top 3 the other way round (Z to A) and counted a blank amount on a closed deal as missing. Both are
choices the bare prompt never had a rule for, not arithmetic slips.

"No answer" means the reply contains no answer at all. In all three, Claude wrote out a Python script to
compute the figures, could not run it (the runs had no tools), and stopped. It happened on the two biggest
files. In a chat app that can run code, that script would probably have run; here it shows the one thing
to check before you trust the numbers: that you got numbers.

So the rules did not make Claude better at arithmetic. They pin down what each figure means, so two
answers can be compared with each other and with a key. And in this set the bare prompt gave no answer
on 2 of its 4 files, the prompt with rules on 1 of 12 runs.

The key and the four files were pushed to our repository at 20:24:44 UTC on 18 October 2026, and the
first run started at 20:25:10 UTC. The repository is private, so that is our record and not proof you
can check. What you can check is `v2-sha256.txt`: the SHA-256 of every input, so you know the files you
have are the ones that were scored.

## Use it on your own export

1. Export your deals with these columns, in this order, whatever your CRM calls them: deal name,
   company, stage, amount, owner, created, last activity, expected close, stage last week. If your CRM
   has no "stage last week", keep last Monday's export and copy the stage column across.
2. Rename your stages to Lead, Qualified, Meeting booked, Proposal sent, Negotiation, Closed won, Closed
   lost. The stage names are written into the prompt, `key.py` and `scene.html`; changing them means
   changing all three.
3. Put your report date, your target and the CSV into `prompt.md` and give it to Claude. Save the JSON
   it answers with as `claude.json`.
4. Render (Node 18 or later, and ffmpeg):

   ```
   npm i playwright
   npx playwright install chromium
   node render.mjs --csv my-export.csv --figures claude.json --target 2500000 --as-of 2026-10-19 --lang en --out monday.mp4 --strict
   ```

   Before rendering it re-computes every figure from the CSV with the same rules and prints any figure
   where Claude's answer differs, and any amount or date it could not read. With `--strict` it exits 1
   when either list is not empty, so a schedule can stop there. The recomputation reads amounts like
   `125000`, `125.000,00`, `125,000.00`, `1.250.000` and `125000 DKK`, and dates as `2026-10-19` or
   `19-10-2026`. Anything else is read as blank and printed as a warning. So is a stage name it does not
   know, a row with fewer than nine columns (left out) and a file with no rows at all.

## What it will not do

- It does not forecast. Coverage is open pipeline over target, nothing weighted, nothing predicted.
- It will not fix a dirty CRM. A deal nobody updated looks stuck because, as far as the export knows, it is.
- It counts a deal as moved only against the "stage last week" column. A deal that moved twice in a week
  shows as one move. A closed deal that was reopened is counted as moved back if it was won, and not at
  all if it was lost: the rules do not cover reopening.
- "New" is "created on or after the report date minus 7 days", so a deal dated after the report date
  also counts as new.
- "A to Z" is plain character order, so capitals sort before lower case and Å comes before Æ and Ø.
- A blank amount counts as zero in every sum. The report shows how many open deals have one, so the room
  knows how much of the number is missing.
- The four files are invented and small. A real export with thousands of rows has not been tested here.

Built by brinvik.com with Claude. Free to use, change and share.
