A Small Firm of Specialists Part 6 of 7

Part Six

Only Bootstrap Imports Infra

A 2,007-line file, four layers, and a test that fails if the architectural rule ever becomes vacuous.

Only Bootstrap Imports Infra
Four decks. Water flows one way. One hatch.

By mid-June the firm worked. Nine seats, hard authority boundaries, a tribunal that judged its own output, 621 passing tests. And one file, goose/session/loop.py, was 2,007 lines long, with a run() method of 460 lines that touched everything.

The tree was organised by runtime role — session/, agents/, ipc/, execution/ — which is the organisation that feels natural and explains nothing. From my own notes at the time: "The layer boundary is a docstring + a comment, not a filesystem edge."

I knew which code was domain logic and which was infrastructure. I knew it the way you know where things are in a messy room: by memory, not by structure.

Pipes and water

The doctrine started as a kitchen observation. From my notes, mid-June: "Obvious move is to separate the behaviour/piping/wiring from implementation details, push them on the sides. My kitchen pipes do not need water to exists, but water does need the pipes .." The formal write-up kept the metaphor and added the economics:

Water needs pipes; pipes don't need water. The pipe defines the shape of a connection; the water satisfies it.

Without boundaries, a change can touch anything — so the cost of being sure is proportional to the whole graph… This is how a one-line fix ends up costing 200–300k tokens.

That last line is the whole motivation, and it's specific to building with AI agents. When a model implements a change, the expensive part is not writing the diff. It's establishing what the diff can break. In an unlayered codebase that's the entire import graph, and you pay for it in context every single time.

And the trap I'd already fallen into:

Modularity alone doesn't fix this. Splitting a 2000-line file into ten 200-line files doesn't shrink the blast radius if the coupling is unchanged… Only a boundary you're allowed not to cross shrinks the blast radius.

Ten small files that all import each other is one big file with extra steps. The thing that saves you is not smallness. It's the existence of an edge you are permitted to stop at — a place where you can say "everything past here is irrelevant to this change" and be right.

Four layers, in dependency order: core_domain (types, rules, protocols; depends on nothing) → application (the pump: wake loop, dispatches, projections) → infra (the dangerous things: LLM clients, DuckDB, Unix sockets, the agents) → bootstrap (the composition root; the only place that imports all three).

One sentence carries it: only bootstrap imports infra.

The fourth layer wasn't in the original design; it came out of a design conversation I had with GLM-5.2, a model running on my own hardware, logged in a working file. My response when it landed: "I think this settles it and good place to start pushing it from. rest will follow."

The tell

Here's the heuristic that made the whole refactor tractable, and it's the part I'd take to any codebase:

if a file wants to be in both core_domain AND application, that's a tell — a layering leak the physical structure surfaces. The logical structure couldn't surface it (the file was just "in session/"); the physical structure forces the choice.

A file that braids layers has no single home — the structure makes that braiding visible and uncomfortable.

The typed-interfaces pass had already given every concept a proper home logically. That pass found nothing, because a file can implement three layers' worth of concerns and still sit quietly in a directory named after a runtime role. It's only when you must physically git mv it — must pick one directory — that the braiding becomes a decision you can't defer.

The instruction I gave the implementing agent, quoted back into two commit messages: "if things want to sit in 2+ layers, note it down, revisit later." Not "fix it." Note it. Some of those notes turned out to be the most interesting output of the entire refactor.

Three of my own corrections to the layering inventory are worth recording, because they're where the metaphor gets tested:

execution is core_domain, not infra — the policy logic (stops, trails, phases, position state) IS the domain narrative; only the data provider (DuckDB historical / Binance) is infra.

The word "execution" sounds like infrastructure. It's the most domain thing in the system: when to move a stop is the firm's doctrine, not a mechanism. Same with the Chartist — the concept is domain, the implementation is infra. And two functions turned out to be neither move: "file mechanism → infra; concept (session summary projection) → core_domain." Not a relocation, a split.

Plus a rule I kept needing: "shapes without consumers are documentation." A beautifully typed view object that nothing reads is not architecture. It got deferred every time it came up, correctly.

The test that enforces the rule

This is the part I'd actually recommend stealing. Layering rules written in a document decay in about a fortnight. So the rule is a test — tests/test_layering_guardrail.py, three of them:

1. No infra imports in application. Walks the application files, regex-matches from goose.infra, and fails unless every imported symbol appears in a per-file whitelist. The pattern set was collapsed from five patterns to exactly one during the move — "Tighter, not looser." And each whitelist entry is annotated with the phase that will retire it: cartographer_daemon.py: {Broker} → "Retires when phase 4a (the MarketData port) lands." A documented residual with an expiry date attached, not an exception.

2. The whitelist can't go stale. An AST pass that fails when a whitelist entry is no longer imported. Without this, the exemption list quietly becomes permanent amnesty — entries accumulate, nobody removes them, and five years later nobody knows which are real. This test makes the whitelist shrink on its own.

3. The composition roots still import infra. This one is my favourite and I've never seen it elsewhere:

if either's infra imports disappear, the layering rule becomes vacuous for that process.

Test 1 passes trivially if nothing imports infra anywhere — including if you accidentally break the wiring. So test 3 asserts the opposite direction: bootstrap must still import the dangerous things, because bootstrap is where the danger is supposed to live. It's a guard against the rule becoming true by accident. Every constraint test should have one and almost none do.

One thing I deliberately did not do: add the same check for core_domain. Six or more real residuals would have flagged it red on day one, and a permanently-red test is a disabled test. It's listed as pending with the residuals enumerated and each one assigned to a future phase.

Two problems where every move makes it worse

The refactor closed with two items written down and left alone. Both are interesting precisely because no file move fixes them.

The hub. SessionHub holds the firm's choreography — alerts, pulse, watch, pace notes, the co-channel. That is application-shaped state. But it's constructed and owned by the broker, which is infra. Move it to application/ and you create an infra→application dependency, "the wrong direction, worse than the current". Move it to infra/ and it's mechanically clean but "conceptually wrong." The actual fix isn't a move at all:

separate the hub from the broker. The broker holds world state (clock, data, positions); the hub holds firm state (roles, handoffs, alerts). They should be sibling objects composed in bootstrap, not hub-owned-by-broker.

Two different kinds of state got fused into one object early on because it was convenient, and the layering only made that visible much later.

The logging leak. Three domain files import a print helper from a root-level grab-bag module. That's "a domain-shouldn't-log leak — domain emits events; logging is a subscriber concern." And again, every available move is worse: put the helper in infra/logging and you've turned the residual into core_domain→infra, which is visibly worse. Put it in core_domain and you've put a logging mechanism inside the domain layer. The only real fix is converting those calls into event emissions — a behavioural refactor, not a structural one.

I find these more instructive than the parts that worked. A layering exercise that produces only clean moves is a layering exercise that didn't go deep enough. The residue is where the real design debt lives.

Who actually typed it

Not me, mostly, and not the model you'd guess. Mid-refactor, my access to the frontier model I'd been pair-designing with was cut — a policy change, nothing to do with me. From the notes, June 15: "I was on the third task, running the backtest to validate the work and then boom, no access .. gone .. what is happening." The response was already in the architecture: if the brief is the contract, the implementer is swappable. The remaining phases were implemented by GLM-5.2 — an open-weight model running in opencode on my own machines — with my approvals quoted verbatim into the commit messages: "yes :)", "yup, let's go with 3e-iii-b", "I trust your judgement."

The layering doctrine got its live test the same week it was written: the pipes held when the water changed.

Where it stops

The last commit closes stage 2 of a four-stage plan. 830 tests passing, zero regressions, 112 seconds, guardrail green, working tree clean. The status file says "Stage 3 (repositories) is next." Stage 3 was never briefed. The final recommendation in the last completion note — "Live functional smoke recommended before stage 3" — was never run.

The 17 commits containing all of this have never been pushed. They sit on a local branch called swing-setup-detection, named after a pivot I started in May and never finished.

The series

  1. I Built a Prop Desk Out of Language Models
  2. Same Model, Opposite Personalities
  3. The Car Crash and the Seven Minutes
  4. We Put the Agents on Trial
  5. I Replaced the Trading Chart With a Picture of a Duck
  6. Only Bootstrap Imports Infra (you are here)
  7. The Brief Is the Contract