Spec and code, merge by merge

Treat plan.md and the day specs as a vector, and the codebase as a second vector in the same space. Across every merge to main, this page tracks how far the spec rotated away from the plan as written, how far apart spec and code sit, and which side did the moving.

This repository is an engineering experiment. Its metrics are experimental diagnostic signals, not optimization targets or standard measures of quality.

Where we are

Refreshed after every merge from the repository itself: the day in progress, its tracks and criteria, what comes next, and the road ahead.

Loading the latest data…

What these figures cannot see

The selected merge

What the merge chosen above did to the spec and the code, over code names (the backticked identifiers the specs use).

Spec–code name gap, merge by merge

The spec–code name gap is the angle between the whole spec and the code, over the code names the specs put in backticks, in degrees, not a percentage. The dashed line is what it would read with nothing to find, and the shading around it is the 95% band of 1,000 seeded draws: the gap to the same code counts shuffled across the names. A merge with no spec name in the code has no gap angle and is left off the line. The strip under the axis shows what kind of PR each merge was; below that, a tick where each calendar date begins, so a busy date takes more width. A day marked * ran out of numeric order.

Name gap: spec vs code Name gap by chance, with its 95% band Spec change Code track Close the day Other (docs, CI) D*Run out of numeric order

What each merge did to the gap

The step each merge took on the gap above, in merge order, one dot per merge coloured by kind of pull request. Above zero, that merge moved the spec away from the code; below zero, closer. Hover a dot for the merge; the net beside the panel is the sum of its steps.

The sawtooth, up close

The share of code names in the specs for days already reached that exist in the code. It dips when a day starts, because the spec names things not built yet, and climbs back as that day's tracks land.

Service boundaries

The gap reads names; this reads the tree. For the services under services in spec-drift.config.json, each with the packages it takes over: the share of their main source lines that sits in its own directory under services/ rather than where it started. It reaches 100% when the last service has moved.

Where the work went

Every line changed in a merged pull request, sorted by what the file is: the spec, the context the agents work from, the production code, and the tests with the scripts and CI that check them. Sorted by path, not by purpose: the rules are under “How the vectors are built”. Breaks on purpose never merge and the auditors only report, so verification does more than its lines show.

Tests per line of code, so far

Test lines changed so far divided by production lines changed so far, at each merge. The line starts once production passes 1,000 lines, so tests written before any code do not read as a spike; above the dashed line, more test than code has been written.

Spec and code, each against its own total

Lines changed so far in the specs (plan.md and specs/) and in everything else except Markdown, each as a share of its own total: code lines outnumber spec lines several times over, so on one scale the spec line would hug the axis. When the spec line rises ahead of the code line, the plan is being written before it is built.

Pull requests per day

Each day's merged pull requests by kind, in run order, counted from the day-NN/ branches; tooling and docs PRs belong to no day, so they are only in the bar above, which counts every merge. A tall bar is a day that took more tracks, or more spec changes, than it was written for: hover for the estimate.

Verification the lines do not show

A break on purpose never merges and a failed CI run leaves nothing in git, so neither is in “Where the work went”. Both are read from GitHub instead. First, each code PR's description: does it record a break on purpose in the one line the rules fix (broken: <what> → <what it reported>), say why there is none (broken: none → <why>), or say nothing? If the line was asked for only from a later PR, a PR before it described its breaks in free text and is not measured; the panel names that PR. Any silent PR is listed below.

Pushes CI failed

Every commit CI ran on for a pull request that later merged, counted once however many workflows ran on it, and failed if any of them failed (a cancelled run is not a failure). Pushes to main are not counted: what failed there had already merged. Each pull request CI failed on at least once is listed below, with the workflows that failed.

Where the corrections landed

Lines edited in each spec file (rows) by each merge (columns). The stepped line marks the day being worked on. Cells below it are edits to days not reached yet: what was learned today, rewriting the path ahead.

Does a second reader agree with the defect tags?

At each close, scripts/relabel.py strips the found:, cause: and made: tags from the day's Defect lines, and the defect-rater agent, which wrote none of the day's work, tags the text again. This is how many of the day's tags it gave the same way, as the spec's Notes quote relabel.py compare. Where they agree the tags measure the defects; where they do not, the tags may only measure what the session that wrote them believed. Days whose Notes give no counts (Day 1 does not mention the comparison; Day 3 tells it in prose) are not counted.

Infrastructure, as CI last checked it

The infra workflow, infra_workflow in spec-drift.config.json, checks the infrastructure's criteria on the merges that touch it, and scripts/infra-summary.py writes its summary. This is its last run on main: the criteria, what it would create, and an estimate of what it would cost an hour to run. Nothing here reads a cloud account.

The deployment, as it was checked live

Once the system is deployed, the project's live check checks it against the domain and commits deployment.json. Each check shows its result and when it ran.

Tokens spent

Output tokens Claude Code wrote each day on this repository, stacked by model, subagents included. Counted from the session transcripts on the maintainer's machine by scripts/token-usage.py, which only records totals; sessions run elsewhere are not in it. The table splits the same replies by the day whose branch they ran on. Under each bar, and in the table, is the context per reply: how much of the conversation each reply re-read. It falls when sessions are kept short, and it drives most of the cost.

Who spent them

The same output tokens by agent: the main sessions, and each subagent by its type, from the .meta.json Claude Code writes beside a subagent's transcript. The auditors and the defect-rater check work they did not write; the implementer writes the track code from a brief. Hover a bar for the context it read.

Evidence behind the ticks

For each finished day: of its ticked criteria, how many cite the PR they landed in, name a test, and record a break seen red in a broken: line. Each named test is looked up on main. CI runs every test that is there, so a criterion goes quiet only when its test leaves the tree or can be skipped (a test run on a system property runs when a workflow passes the property as true). A test that moved is followed under its new name, of the same stem or listed by the PR that moved it. A name counts only if it was in the tree when the day's last PR merged. Grey is retired: a later day's spec names the test and that day's PR removed it; orange is gone with no spec saying so. Days marked pre-format were written before the evidence format: nothing asked their ticks to record any of this, so a zero there is not measured, not missing. If the broken: line was asked for only from a later PR, “Seen red” is not measured for a day none of whose PRs reaches it.

Merge by merge

The distinct tests the finished days name, looked up at every merge from the day each was ticked. Green is there and run by CI; orange can be skipped or has left the tree with no spec saying so; grey is retired. A green step is a day closing; a dip, or orange rising, is a check that has gone quiet.

Removal checks

The greps a finished day's criteria or Verify say must print nothing, run over the tree at every merge from the day's end, from the root or a directory under verify_bases in spec-drift.config.json, wherever their paths are. Paths follow the directories that moved since (its moved), because a grep of a path that is gone prints nothing too. CI runs none of them. A check is orange when it finds lines on main, listed under it; grey when it reads no files; green otherwise. Since is the merge where that state began.

Hand-offs to days not yet run

What finished days leave for later: a Notes item that names a day not yet run, or a code comment that does ("Day 12 adds this"). Nothing else carries them forward. A Notes item with an ID (like H12.1) counts as picked up only when the receiving spec cites the ID; one without, when it names the day the item came from; a comment, when the spec names its file. Days in run order; a spec written in full should leave none open. Last: ID'd items no later day picks up, and tracks a PR announced that no branch ever named.

What the vectors say

How the vectors are built

Snapshots. The initial commit plus every first-parent merge on main (). Code PRs, spec-change PRs and close-the-day PRs are told apart by branch name and title.

The spec–code name gap needs a space both sides share. It is built from the code-like names the specs put in backticks. The spec vector counts how often each name appears in the specs. The code vector counts how often it appears in the code, with Markdown, data/, screenshots and comment lines left out: a comment naming a thing is not the thing. Both are log-scaled and compared by angle. Each merge’s gap uses only the names the specs had used by that merge, so a name a later spec adds does not move an earlier point. Counts are never negative, so even unrelated vectors sit well under 90°: the same code counts shuffled across the names would read today (the dashed line and its band). With no spec name in the code there is no direction to compare, so no angle, rather than 90°. The other direction is counted apart: names the specs once used and have dropped, that the code still has. The gap is worth reading only as its distance below that line; the share of reached names found in the code (“The sawtooth, up close”) is the plainer measure.

Where the work went counts git diff --numstat between one merge and the one before it, every file, so nothing merged is counted twice and nothing is left out. A moved file counts only the lines that changed in the move, under the path it moved to. Binary files, package-lock.json, screenshots/ and docs/dashboard/token-usage.json are written by tools, not people, and are not counted. The first rule that matches a file’s path decides its box:

  1. Specs: plan.md and specs/, the template and README there included.
  2. Context: any other *.md (CLAUDE.md, READMEs, the PR template), .claude/ (the agents and settings), and docs/ apart from docs/dashboard/.
  3. Tests: a path through a test/ or tests/ folder (src/test/, the harness and its support/ included), or a file named *Test.java, *Tests.java, *IT.java, test_*.py, *_test.py, conftest.py, *.test.js or *.spec.ts and their kin.
  4. Scripts, CI, dashboard (drawn with the tests, as verification): the rest of scripts/, .github/ and docs/dashboard/.
  5. Production code: everything else: source, migrations, Dockerfiles, compose, .env.example, observability/, the frontend and data/ pipelines.

The rules sort by path, and a path does not always say what a change was for. A spec-change PR that fixes a test counts as tests; a test helper under src/test/ counts as tests even when it is plumbing; a Markdown file inside a service counts as context; the dashboard’s own code counts as verification. The ratio of tests to code uses the Tests box alone, without the scripts.

Refresh. python scripts/spec-drift.py --build in the repository writes this page with its data. It is rerun after every merge, before the next step starts.

Limits. Names in a spec that should disappear (like a table leaving the module that owned it) still count as shared vocabulary. Comments are recognised by line (//, /*, *, --, #, <!--), not parsed, so a marker inside a string can hide the code after it on that line. The gap measures shared language, not correctness; correctness lives in the acceptance criteria and the tests.

Every merge as a table
Conclusion

What the repository says so far

Nothing in this panel is typed into the page. The read is the latest one the lab notebook records at the end of a phase, the table is the notebook's own measurements, and the rest is counted from git on every refresh.

What is being measured