Putting The House In Order

manor house

Photo by Mike Smith on Unsplash

The fortnight that’s just ended started with one of the smallest possible problems: commandspace, “screenlock”, return, and the wrong version of ScreenLock launched. An older build. The settings layout was a previous iteration’s. The pill toggle wasn’t there. I’d been certain I’d installed the new build into /Applications, but here was Spotlight handing me something that contradicted that, looking otherwise indistinguishable from the right answer.

I said “huh” out loud, restarted the app from /Applications directly, finished the test I was doing, and tried to forget about it. Then a few hours later I caught myself doing the same wrong thing with HyperCaps. And after that, with the Notes Editor.

That’s the moment my week stopped being about whatever I’d planned to do and started being about housekeeping. Two weeks later I’ve fixed every known bug across the suite, shipped two new products and a top-level Apps section on the website, rebuilt Release Manager into a thin Swift dispatcher around a shared Make include, delivered on every user feature request that was queued, and reclaimed roughly nine and a half gigabytes of accumulated cruft from disk along the way. I want to write down what the audit-and-cleanup half of that effort looked like, because it’s the part where the most was learned per hour spent.

The audit I should have built earlier

I run about seventeen tiny macOS utilities. They sit in the menu bar and they do their thing and I largely forget they exist — which is, by design, exactly how they’re supposed to work. ClipMan watches the clipboard. ActiveSpace shows my current space. QuitProtect intercepts accidental quits. RainbowApple draws a stripey Apple logo over the Apple in the menu bar — because life is short. They’re invisible infrastructure.

The problem with invisible infrastructure is that you stop checking it. You assume it’s running. You assume it’s running from the right place. You assume the version installed locally is the version you released to GitHub, and that the version you released to GitHub is the version that’s in your repository, and that all of those are in agreement with each other. You assume things are consistent, because why wouldn’t they be? You’re the only person touching them.

The Spotlight thing was the moment I noticed that several of those assumptions weren’t actually being verified by anyone. My pipeline had been producing releases for months, but nothing in the system was checking the releases afterwards. I was solo developer, solo reviewer, solo QA, solo ops. The checks I forgot to write were the checks that didn’t happen.

So I built a system audit. The first version was a Sunday afternoon’s work. I added a button to Release Manager’s toolbar. It opens a table with one row per app and three columns: version, path, source. Each cell is green, red, or amber. The whole table fits on one screen. The audit completes in about four seconds for the whole catalogue.

The three checks each answer a different question, and together they cover the dimensions of deployment consistency I cared about.

Version: is what I have what I shipped? The installed app has a CFBundleShortVersionString stamped in its Info.plist. The Release Manager catalogue has the version of the latest GitHub release. If the two match, green. If they don’t, red — with both numbers shown, so the direction of the mismatch tells me which kind of problem I’m looking at.

Path: is it running from where it should be? This is where ps came in. ps -eo pid,args returns one line per process with the full path to the executable. The audit captures the output, finds processes whose path contains the app’s product name, and checks whether the path begins with /Applications/. If yes, green. If no, the audit categorises where it actually is — AppTranslocation, DerivedData, the source tree’s build output, somewhere unexpected.

Source: is the repository in sync with the release? A git tag --sort=-v:refname finds the highest semantic-version tag, then a commit count between that tag and HEAD tells me whether there’s unreleased work sitting in the repo. Zero commits ahead is green. Anything else is amber: a reminder to release.

The whole audit engine is about 190 lines of Swift, shells out to four commands (ps, git, gh, PlistBuddy), uses no frameworks beyond Foundation, makes one network request (the GitHub API call to refresh the release-version cache). It’s the smallest thing I’ve added to Release Manager in months, and it might be the most useful.

What the first run found

The very first time I ran the audit, three things were wrong.

Release Manager itself showed N/A for the path check, even though I was obviously running it. After a few minutes of thinking I’d coded the path check incorrectly, I realised it was actually working perfectly — my naming was off. The audit was searching for JorvikReleaseManager.app (the build name) in the process paths, but the installed bundle is Jorvik Release Manager.app with spaces (the install name). String.contains() returned false, so the search couldn’t find its own host process. A correct negative from incorrect assumptions. An audit that finds its own bugs has a certain charm; I fixed the lookup to consider both names and moved on.

Notes Editor showed Running from AppTranslocation. I’d been running it for weeks, editing and deploying blog posts, with no indication that anything was wrong. The Dock icon showed the right name. The About window showed the right version. The app itself had no idea it had been silently relocated to a randomised directory deep inside /private/var/folders/ by macOS’s anti-tampering machinery. ps knew. None of the rest of my workflow did.

I’d installed Notes Editor by downloading the zip from GitHub in a browser, extracting it, and dragging it to /Applications. The browser had attached com.apple.quarantine to the download. macOS’s App Translocation feature had silently relocated the app on first launch. Every subsequent launch had been finding the translocated copy. The version in /Applications/Jorvik Notes Editor.app was, in some technical sense, not the version I’d been running.

This is the kind of finding that makes you question everything you thought you knew about your own machine. The app is in /Applications. You launched it from /Applications. But it’s not running from /Applications. macOS performed a silent redirect, and the only way to know is to ask the process table.

The third finding was a small but real bug in my source check. I’d originally implemented the “latest tag” lookup with git describe --tags --abbrev=0, which sorts by commit ancestry rather than by semantic version. On a couple of repos where I’d cherry-picked commits between tags, the nearest tag by graph distance was an older release than the highest tag by version number. The check was answering a slightly different question than the one I was asking. I rewrote it to use git tag --sort=-v:refname | head -n 1 and the answers became right.

Three checks, three problems, on the very first run. The audit paid for itself before I’d finished writing this paragraph.

And then the disk

If the apps were in disagreement with the catalogue, what else was? I ran a one-line mdfind query against my whole machine:

mdfind 'kMDItemCFBundleIdentifier == "cc.jorviksoftware.*"'

The output was longer than I’d expected. Forty-something .app bundles whose identifiers started with cc.jorviksoftware., scattered across:

So the situation Spotlight had been facing wasn’t “two ScreenLock bundles.” It was “five copies of every Jorvik app, two-thirds of them old, all of them indexed, and a heuristic algorithm trying to pick one.” The wonder isn’t that it occasionally picked the wrong one. The wonder is that it ever picked the right one.

The Spotlight cache trap

I dropped a .metadata_never_index marker into the source tree. macOS supports this convention for any directory you want Spotlight to ignore. Empty file, unambiguous semantics, takes about a second to create:

touch "/Users/jonathanhollin/Desktop/Jorvik Software/.metadata_never_index"

Then I deleted the iCloud clone, the ~/backups/ tree, and the four stray build directories. Total reclaimed: about 9.6 gigabytes. Not enormous in modern terms — a single Xcode install is bigger — but the amount wasn’t the point. The point was that none of those gigabytes were doing any work. They were silently confusing search results, populating duplicate matches, and (in the iCloud case) forcing me to reason about a parallel-universe copy of every file I edited.

I tested Spotlight again. The wrong copy of MouseCatcher was still being returned.

This was the bit I learned the hard way. .metadata_never_index stops future indexing. It does not retroactively flush the cache. macOS still knew about MouseCatcher.app because it had indexed the file sometime last week, before the marker existed; the marker tells the indexer “don’t visit this path again,” but the entry already in the index just sits there. Cached entries normally age out as the indexer revisits the volume — but for a directory under a .metadata_never_index marker, the indexer doesn’t revisit. So the cached entries can persist indefinitely.

This produced a fairly perfect bit of self-trolling. I’d marked the directory. I’d deleted the dead trees. I’d stripped 9.6 GB of files I shouldn’t have had. The world was tidier than it had ever been. And Spotlight was lying about it.

The fix is, as Apple-fix things tend to be, hidden in plain sight: System Settings → Spotlight → Privacy. Drag a folder into that pane. macOS treats the addition as “immediately drop everything you have for paths under here.” This is materially different behaviour from the marker file — Privacy is an active flush, the marker is a passive don’t-index instruction. You can use both together: drag the source tree into Privacy, watch the cache get flushed within seconds, leave it there or remove it again afterwards (the marker keeps it from being re-indexed regardless). I went with leave-it-in-Privacy — belt and braces, plus a permanent indication in System Settings that this path is intentionally excluded.

After all of that, mdfind 'kMDItemCFBundleIdentifier == "cc.jorviksoftware.*"' returned exactly one result per app: the canonical install in /Applications/. Spotlight launches the right app every time, because there is no wrong app on the system any more.

The other strands

While the audit was teaching me things, the rest of the fortnight kept happening alongside it. I want to mention the work briefly because each of these strands fed into the same arc: making the estate easier to live in.

Release Manager itself got rebuilt — the part of the codebase that was the audit’s sibling rather than its subject. The 1,600-line Swift pipeline that built, signed, notarised, and packaged every Jorvik app became a 770-line dispatcher around a shared Make include. The build/sign/notarise/staple/package work moved out into shell, where every tool involved natively lives, and the Swift app kept the pieces that genuinely benefit from being Swift. That migration alone was ten days’ work, but it’s its own post.

A handful of long-standing user feature requests landed during the same window. The Web Editor’s file browser learned to refresh on focus, so files added in another tool stop requiring an editor restart. The Daily News reader gained a per-feed health pill (green / amber / red) and a real OPML import-time dedup, both of which were necessary to manage a 300-plus-feed collection without losing your mind. Release Manager’s Prod-status indicator stopped lying when the source had been pushed to GitHub but not actually deployed to Cloudflare Pages. The Web Editor’s commit dialog stopped having a single-line text field with no visible cursor (a small bug that I had been silently working around for six weeks while telling myself I’d fix it next time).

Two new product pages went up on jorviksoftware.cc — one for Daily News, one for Reverie — and a new top-level “Apps” section was added to the navigation, with the homepage and every static page updated, the build script updated to propagate the change to every regenerated note page, the sitemap updated, the Cloudflare deploy refreshed. About sixty files touched in one commit, and not a single visual regression on the homepage.

I rewrote a clutch of stale KB documents, deleted three pieces of memory that were no longer accurate, and added two new conventions (one for keyboard-shortcut style on the website, one for catalogue/Makefile drift in the build pipeline). The KB grew. The misinformation in it shrank.

Each of those strands looked like its own thing while it was happening. Looking back from this end of the fortnight, they were all the same thing: every place where the estate was a bit out of square got squared up.

The principle

The audit, the cleanup, the RM rebuild, the feature requests, the KB hygiene — if there’s a single thread tying them together, it’s this: trust, but verify, and make verification cheap enough that you actually do it.

I trust my pipeline. I built it. I know what it does. But trust without verification is faith, and faith is a poor foundation for engineering. The audit exists so that I can trust the pipeline and confirm that my trust is justified, routinely, without effort. Four seconds and one button is cheap enough.

The same principle applied to the disk. I trusted that /Applications was canonical. mdfind verified that it wasn’t, because the iCloud clone, the backups directory, and the build dirs were all populating the same answer in different shapes. I trusted that Spotlight reflected reality. The cache verified that it didn’t, until I forced it to. Each of these was an assumption I’d held for months without ever testing — and each of them turned out to be wrong in some interesting way.

The same principle applied to the Release Manager rebuild. I trusted that putting the build pipeline inside a Swift host app was the right shape, because Swift is what I write Mac apps in. The audit-style framing of “but does the structure match the work?” revealed that the answer was no — the build pipeline was shell-shaped work, and putting it in a Swift host had been adding one bug per new project type for over a year. Verification told me what habit had been hiding.

I think this is the lesson of the fortnight, more than any of the individual cleanups. The estate runs better when the assumptions underneath it are routinely checked. Most of the time the checks come back green and you proceed. Occasionally one comes back red, and you find a thing that’s been quietly wrong for weeks — possibly months — and that you’d never have found through normal use because it was nowhere your normal use looked.

A small confession

The pleasure of finishing this fortnight isn’t the disk space, although the disk space is nice. It isn’t even the RM line count, although that’s also nice. The pleasure is the correlation between the system as I understand it and the system as it actually is.

After the cleanup, when I ask “where is ScreenLock,” there is one answer. Before, there were five. Five is too many. One is the right number. After the audit, when I ask “is the version I’m running the version I shipped,” the table shows me green for every app and I believe the green. After the RM rebuild, when I ask “why did this build behave this way,” the answer lives in 430 readable lines of release.mk rather than 1,600 lines of Swift across thirteen interlocking pipeline stages.

Anyone who’s spent time in a kitchen knows the feeling. Half the joy of a clean countertop isn’t the cleanliness; it’s that you can put a thing down without first having to move three other things out of the way. The countertop is in correspondence with itself.

A clean filesystem is a clean countertop. A clean catalogue is a clean drawer. A clean release pipeline is a clean cooker hood. None of it is glamorous, but if you’ve ever sat down to start work and realised that the workspace itself is in your way, you’ll know the feeling I mean. Today, mine isn’t. Tomorrow I get to start something new on a workspace that won’t fight me when I do.

Two weeks of housekeeping, paid for by the small dignity of asking my own tools to be honest with me about themselves. I’d do it again. I will do it again, periodically, because that’s the deal. Estates don’t stay tidy on their own.

But not tomorrow. Tomorrow, I think, I’m going to build something.