CodeAtlas · engineering notes

I built a tool to measure everything I have ever written. It started by telling me I was wrong.

155 projects across 4 drives: games, AI services, data tools, experiments, and a respectable pile of half-finished ideas. I wanted one honest answer to "what have I built, and what is it worth?" The first count said one project held 27.5 million lines. It held about 95,000. Everything below grew out of refusing to accept a number I couldn't check.

The short version
155projects indexed and re-measured every night
9,499,267authored lines — after subtracting the data, generated output and copies that only look like code
1,416automated tests, and the nightly refresh refuses to run if one fails
194lessons recorded, each a wrong answer the tool once gave
12read-only tools any AI agent in any of my projects can call
2.9 sto re-analyse the whole estate when nothing changed (2 min 50 s cold)

01Why I built it

I write a lot of software, much of it with AI coding agents beside me, and it lives in a lot of places. Eventually I couldn't answer simple questions any more. Have I built this before? Which port is free? Which of these three folders is the real one? And the one that started it all: if someone asked what I've built, could I show them anything better than a list of folder names?

So I built an atlas: a scanner that reads every project, a set of analyses on top, and a nightly job that keeps it all current. The plot twist was that every time I pointed it at my own work, it found a number that was wrong, usually because the obvious way to measure something quietly measures something else. The tool became less about counting and more about not lying — including to me.

02Architecture at a glance

Before the war stories, the map. CodeAtlas is a stack of layers, each reading the one below, and one rule holds it together: a layer reuses the answer beneath it instead of working it out again. Several of the lessons further down are what happened when I forgot that.

The scannerReads every project and buckets each top-level folder — source, ingested, generated or duplicate — before counting a line. Unchanged projects are recognised from folder metadata and never opened.
One shared walkOne read of each project feeds all three analyses, so they can't disagree about what exists, and every number says whether it was parsed or pattern-matched.
A cache that knows its rulesEvery stored answer is keyed on the project and on a hash of the rules that produced it. Change a rule and its old answers expire on their own.
Doors for agentsContext packs and a small MCP server hand AI agents what has already been measured: read-only, and over a pipe rather than a port.
The consoleOne local page in front of everything. It sends the name of an action, never a command, and any tool that describes its rows gets a proper table.
The nightly refreshTests first, then everything in dependency order, out to the portfolio and resume. One failing test and the night stops.

Why this shape? The expensive part is reading files, and the dangerous part is two components deciding the same thing two different ways. So: read once, decide once, pass the answer up. The price is that a shared piece has to be right for everyone who uses it, which is harder than being right for one tool — but it only has to be right once.

03How it grew

254 commits since 2026-08-17. The suite grew from 744 tests in the first week to 1,416 today, and every one of them pins a defect that actually shipped. Less a boast than a confession with a test runner.

2026-W34: 1032026-W35: 592026-W36: 52026-W37: 02026-W38: 12026-W39: 02026-W40: 72026-W41: 152026-W42: 212026-W43: 102026-W44: 332026-08-17this week
Commits per week. Green weeks added a new subsystem; the quiet weeks are when I was busy actually using it.
2026-W34: 744 tests2026-W35: 1,044 tests2026-W36: 1,059 tests2026-W37: 1,059 tests2026-W38: 1,237 tests2026-W39: 1,237 tests2026-W40: 1,261 tests2026-W41: 1,294 tests2026-W42: 1,337 tests2026-W43: 1,374 tests2026-W44: 1,416 tests7441,416
Tests in the suite at the end of each week.
  1. 2026-08-17The scannerevery project on every drive, measured, not guessed
  2. 2026-08-17Generated docsfacts and judgement in different files
  3. 2026-08-19A page that checks itselfbuilt is not the same as right
  4. 2026-08-19The consoleevery tool behind one local page
  5. 2026-08-19Grades you can argue withevery score decomposes into its evidence
  6. 2026-08-23Credentials in historydeleting a secret does not remove it
  7. 2026-08-23Context packseverything known about a project, for an agent
  8. 2026-08-25The reference graphwhat breaks if I change this file
  9. 2026-08-25One walk, three answersthe analyses cannot disagree about files
  10. 2026-08-28The career storethe resume becomes data
  11. 2026-08-29Open to AI agentsa read-only MCP server any project can ask
  12. 2026-10-13The claims registerevery figure carries its evidence
  13. 2026-10-25Security watchincidents matched against what is installed
  14. 2026-10-26The vulnerability sweepevery environment, ranked by what runs
  15. 2026-10-27Measured factsthe resume quotes measurements, not memory

04Counting honestly

A project folder holds four different kinds of thing, and a line counter can't tell them apart:

Sourcecode somebody wrote — the only thing that counts
Ingestedwhat an app takes in and keeps: uploads, caches, databases, logs
Generatedwhat an app emits: reports, exports, documentation bundles
Duplicatea second copy of code counted elsewhere: backups, worktrees

Every top-level folder goes into one bucket before anything is excluded, and the subtraction is always printed beside the total. Across my estate, 62% of the lines on disk turn out to be real source; the rest is my apps' luggage. When the scanner can't settle a question it raises a flag rather than guessing, and the answer is written down once, with a reason, so nobody has to ask again.

The design choice underneath is attribute before you exclude. You can argue with a subtraction you can see. You can't argue with one that just happened.

05Fast, without going stale

Scanning is limited by the disk, not the processor. Antivirus inspects every file open, so a one-thread scan crawls along at about 45 files a second. Sixteen parallel readers fix that. The bigger win is not reading at all: each project is fingerprinted from directory metadata alone, and only projects that moved get opened. A no-change refresh went from 147 seconds to 31.7.

The sneaky part is the cache. A stored answer goes stale in two ways: the project changed, or my rule for reading it changed. No timestamp can see the second one, so every analysis stores a hash of its own detector definitions, and changing a rule invalidates every answer that rule ever gave. Nobody has to remember to pass --full — which is just as well, because nobody would.

06One walk, three answers

Three analyses — what depends on what, where complexity and churn pile up, and which code nothing calls — used to read each project separately. Between them they read 61,641 files and still couldn't agree on how many files existed. Now one walk feeds all three. I filed it as a speed fix; it was a correctness fix first. They agree on every project, and a full cold run takes under three minutes instead of about ten.

Languages go through one registry. Python is parsed properly; JavaScript and TypeScript are matched with patterns, and that word — pattern — travels with every number that came from them. A reader who doesn't know which one they're looking at can't judge any of it.

07Secrets in the history

Deleting a credential from a file doesn't remove it from the repository. It's one history command away, in every copy, more or less forever. So CodeAtlas reads commit history too. The value never leaves the scanning process: it's redacted at the source, and only a file, a line and a kind are kept. A scan that couldn't run reports "unknown", never "clean", because a reassuring wrong answer is the most expensive kind.

08The security sweep

Every Python environment on my machines is read for what is actually installed, not what its manifest says ought to be there. That's matched against published advisories, the list of vulnerabilities being exploited in the wild, and severity scores, then boiled down to one worklist with whatever is running at the top. Each fix is tested beside the old environment — same tests, same inputs, outputs compared — before anything changes, and recorded in a runbook. The first pass recorded 57 verified fixes and moved six environments to a newer Python so their fixes could land at all.

09Open to AI agents

An AI agent working in one of my projects knows nothing about the other 155 unless something tells it — a brilliant new colleague with no onboarding. CodeAtlas does two things about that. A context pack gathers everything already measured about a project, in milliseconds and measuring nothing new, and ends with a ranked "what to do about it". And a small MCP server puts 12 read-only tools in every agent's tool list: search what exists, check which ports are taken, read a project's evidence. It talks over a pipe, not a port, so nothing on the network can reach it.

Every section of a pack says one of three things: found, none, or not measured, and why. Collapsing the last two is how a tool lies to an agent — and the agent will believe it.

10The console

All of it sits behind one local web page. The page never sends a command, only the name of an action, so however cleverly someone asks, it can't be turned into a remote shell. Any tool that declares what its rows are gets a sortable, keyboard-friendly table for free, with every column explaining what it measured. And an empty table has to say whether nothing was found or nothing was measured: two very different kinds of empty.

11The nightly refresh

At night everything is re-measured in dependency order, and the very first step is the whole test suite. If a test fails, nothing downstream runs, so a broken rule can never reach the documents or the portfolio. That makes the suite the most expensive step of the night, which is exactly where I want the time to go.

self-test616 scredential history110 sdecision scorecard92 sdependency usage87 spack value83 sdepth analysis45 sremote state42 sstatic analysis25 ssearch index18 sextract briefs14 s
The slowest steps of the last full refresh, in seconds — about 20 minutes in all.

12A resume that checks itself

The same measurements now build my portfolio and resume. Every figure in my career records is registered as a claim, with its evidence and who may see it, so a number about my day job never wanders onto a public page by accident. Figures about my own work aren't typed at all: the text says {measured:projects}, and the build fills in today's measurement. My resume used to say "140+ projects, 1,200 tests, six machines". It now says what is true, and it will keep saying it without me having to remember.

13Twenty lessons

Each of these is a confident wrong answer the tool once gave me. Most of CodeAtlas is scar tissue, and these are the scars. Under each: what I do now, and the moral — the part worth taking back to your own code.

0127 million lines, give or take 27 million

One project measured 27,570,349 lines. Its real source was about 95,000; the rest was generated HTML and notebook outputs doing a convincing impression of code.

Now: nothing is excluded before it is attributed, and every exclusion carries a written reason.

a surprising total is a question to answer, not a number to trim until it looks right.

02Notebooks are mostly pictures

Twenty-five notebooks counted 1,075,236 lines and held 7,746 lines of code — a 139× inflation from images stored inline as text.

Now: only the source of code cells is counted.

find out what a file format actually stores before you measure it.

03One lowercase, 303 missing files

Lowercasing the exclusion list turned "skip the virtualenv's Scripts folder" into "skip every scripts folder". It hid 303 code files across 27 projects, and one project reported containing no Python at all.

Now: a short, explicitly case-sensitive list, with the reason written beside it.

normalising text is a decision about meaning; sometimes the capital letter is the meaning.

04The fix that looked like a fix

Recovering include/ folders the same way pulled in 694,189 lines of a vendored GPU library, and one project's authored lines jumped from 841,545 to 1,535,734. It looked exactly like the fix working.

Now: recovering files is only a fix if they are the project's own — check authored lines before and after, not the file count.

the result you were hoping for deserves the most suspicion, not the least.

05The cap that ate a directory

A 40,000-file safety cap spent its whole budget inside one 50,127-file data folder and never opened the app's code. The project reported 6 Python files against 53.

Now: the walk takes turns across folders, and a truncated walk names what it never reached.

a limit should share its budget fairly and say out loud what it skipped.

06294 GB of change that wasn't

A freshness check with its own idea of what to skip reported 294 GB of change across 53 projects. Reusing the scanner's own fingerprint said 7 projects had moved.

Now: reuse the scan's answer. A second implementation drifts immediately, not eventually.

when a new number disagrees with an established one, suspect the new one first.

07Saved web pages, masquerading as JavaScript

A Python web service measured 97% JavaScript, because it had saved thousands of web pages as data. The same mistake came back twice more, in two different analyses, before I fixed it for all of them at once.

Now: every analysis asks the scan which folders are data; a test fails on a second copy of that logic anywhere.

the second time a bug appears, fix the pattern rather than the place.

08FAIL is not fail

A priority band fired for 0 of 168 projects and read like good news. It was comparing against "FAIL" while grading writes "fail". After the fix: 164.

Now: a branch that never fires across the whole estate is a bug until proven otherwise.

silence from a check is only good news once you've seen it speak.

09Two right answers, one leaked key

One tool advised putting unversioned projects under version control. Another marked their credentials resolved, because with no repository nothing had leaked. Both true — and followed in order, they commit a live key. Four of nine findings were in exactly that state.

Now: advice from different tools is tested in pairs, and a warning never contains a runnable copy of the thing it warns about.

parts that are each correct can still combine into something wrong; test the combination.

10Dead code, cautiously

Any name that appears in a string anywhere is never reported as unused: that excludes 17,513 names while still reporting 14,487. A false "delete this" costs a working feature; a miss costs nothing.

Now: confidence comes from the kind of thing, and every row opens the definition before anyone deletes it.

when one kind of mistake costs far more than the other, lean the tool away from it on purpose.

11238 is not a headline

Credentials found only in history started at 238. Minus 20 in committed virtualenvs, 67 in a cloned third-party repository and 16 duplicates across copies: 135 distinct. The virtualenv pattern first missed a folder called venv2, and the duplicate counter reported "0 collapsed" while collapsing 16.

Now: every subtraction is printed, and one that under-reports itself is treated like one that hides.

people trust a total they can argue with, so show the working.

12+6,600% growth, courtesy of commit one

The trends view counted a scan from the first day, when only two projects were configured, and reported explosive growth. Another tool had already solved this.

Now: import the threshold instead of copying it — and read my own lessons file before writing a new analysis.

someone has often solved your problem already, and that someone is frequently past you.

13Narrow the run, lose the estate

Analysing one project rewrote the results file with one project in it, quietly discarding 145. Nothing errored. The same bug lived four days longer in a second tool, because the test only read the first one's code.

Now: a narrowed run merges, a full run replaces, and a probe checks every tool of that shape, not one instance.

for any write that takes a filter, ask what happens to the records it wasn't asked about.

14Hoarding is not thoroughness

One results file reached 48 MB, saved on every refresh. 84% of it was 112,290 symbols nobody could act on.

Now: keep what can be acted on and count what was dropped. 9.1 MB.

storing everything is easy; storing what someone can use is the actual job.

15279.6% typed

The TypeScript counter counted annotations rather than annotated functions, so one function with three typed parameters counted three times.

Now: a measurement that does not apply is "not applicable", never zero — and never 280%.

decide exactly what one unit is before you start counting.

16Three tools, three realities

Three analyses carried three skip lists and found 17,923, 21,856 and 21,862 files in the same estate. When two say different things, at least one is wrong and nothing says which.

Now: one shared reader. 146 of 146 projects agree.

two counts that ought to agree are the cheapest bug detector you will ever own.

17The resume aged; the estate did not wait

My resume said 140+ projects, 1,200 tests and six machines. The measurements said 155, 1,397 and seven. And the first version of the fix counted 9 agent tools where there were 12 — caught by checking it against a second, independent count.

Now: figures about my work are quoted from measurements, and two counts of the same thing must agree.

any figure typed from memory has an expiry date; quote the source instead.

18A stamp is a claim

Marking one new write-up as reviewed silently re-dated every write-up, including 19 that nobody had reviewed against their changed code.

Now: only the write-ups I name are stamped.

a bulk "checked" is a separate claim about every item it touches.

19The site was fine; the checker wasn't

The nightly link check reported a live page as dead. The host redirects a missing trailing slash with a status code my checker's library refused to follow. Every browser follows it.

Now: follow redirects the way a browser does — and suspect the instrument before the thing it measures.

when a check fails, check the checker too.

20Nine silent nights

A test compared a saved data file with what the code would now produce. One number moved, and the nightly refresh stopped nine nights in a row — at the very step that could have updated the file.

Now: a test that guards the refresh checks what the code does, never the state of the data.

a gate must never depend on the thing only the gated step can fix.

14What I'd do differently

Hindsight is cheap, so here it is at a discount: the lessons above, read back as advice to an earlier me.

15What I do now

A measurement is a claim. A claim nothing checks is a guess wearing a number.
Unknown is not zero. "Not measured, and why" is an answer; a confident 0% is a lie.
Reuse the first answer. A second implementation of the same rule drifts the day it is written.
Show the subtraction. Anything filtered out is counted and named, never just gone.
Prove the guard can fail. A check that has never fired is indistinguishable from one that cannot.