jobmatch-microservices

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 migration 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: Days 38–40 before Day 17, and Day 17 after Days 18–19.

Name gap: spec vs code Name gap by chance, with its 95% band Spec change Code track Close the day Other (docs, CI) D38*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 four services in plan.md (identity, jobs, applications, matching, each with the monolith packages it takes over): the share of their main source lines that sits in its own directory under services/ rather than in the monolith. It reaches 100% when the last service leaves.

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. Days 1–4 wrote the contract tests before any code moved, so the line starts once production passes 1,000 lines; 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? The line was asked for from #310; a PR before it described its breaks in free text, read by phrasing until then, and is not measured. 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.

Infrastructure, as CI last checked it

infra-ci plans the Terraform without an AWS account, applies the bus and score table to LocalStack, and checks the criteria of Days 32 and 36 on every merge that touches them. This is its last run on main: the criteria, what the plan would create, and an estimate of what it would cost an hour to run. Nothing here reads the AWS account; the deployment itself is Day 35's.

The deployment, as it was checked live

Day 35 deploys once. scripts/live-checks.py then checks the running system against the domain (C35.16 and C35.17) and commits deployment.json. Each check shows its result and when it ran; the account is destroyed after.

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 migration 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.

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, like GatewayHarnessIT, 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 (#55, from Day 9): nothing asked their ticks to record any of this, so a zero there is not measured, not missing. The broken: line was asked for from #310: “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 backend/ or the root, wherever their paths are. Paths follow the directories that moved since (backend/jobs/ to job-service, backend/ to identity-service), 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 17 adds job-service"). Nothing else carries them forward. A Notes item with an ID (H28.1, from Day 28 on) 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 (Day 25's E1c, #298).

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 (SavedJobCounts, saved_jobs, JobRepository…). 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 saved_jobs leaving jobs/) 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