2026-10-25

architecture · git

Sixty repos is a fence

The disk is one folder and about seventy git repos. Physics in one. The PDF reader in another. Hours off to the side, private. A registry next to that with about five hundred packages. I did not split it so I could say microservices at dinner. I split it so an afternoon in one place could not quietly become a rewrite of another.

A monorepo would have made the hop free. One solution, one history, one blame. I have lived in that shape. The hop is free, so people take it. A rendering fix grows a simulation change because both projects are a keystroke away and the debugger is already attached. The commit says "fix the frame." The diff moves a clock. Six weeks later nobody knows which half was the fix.

The fence is the inconvenience. I want the inconvenience.

A package is the unit I am willing to share

A package is the unit I am willing to let another project depend on. It has a version. It has a feed. It was built from a commit that already landed. If the reader only compiles by reaching into ../../../novolis-physics, physics stopped being a library. It became a roommate. Roommates change on the same day. Libraries change when you publish.

Roommates are nice when I am the only one home. I can edit both sides of a bug and see the test go green before I have decided what the public shape is. That green is a local fairy tale. The consumer in CI does not have my dirty physics tree. The consumer has whatever 2026.1.* resolved to this morning. If I need the fairy tale to ship, I have not shipped. I have a coupled pair of working copies and a story about how they belong together.

What I commit is a package reference, or a LibraryReference that turns into the sibling project only when that project is actually on disk and stays a package when it is not. The expansion is governance's job. The csproj does not grow a conditional to remember the trick. I do not check in Condition="'$(NovolisPhysicsSrc)' != ''". That line is a confession that the fence was in the way and I moved it.

The afternoon switch

The big platform solution can rewrite the rest for the afternoon I am building. Novolis.Platform.slnx knows the map from package id to project file. If the project already depends on that package, and the project file exists in the checkout, the build uses the project. If I have not cloned that repo, it stays a package. The map does not invent dependencies. It does not reach into a lab sample and compile it because the folder was there.

Per-repo CI does not open the meta solution and declare victory. CI restores packages from nuget.org and GitHub Packages. If it only builds because every checkout is present, I have not finished. Submodules in the lab are allowed as source I might want on disk. They are not allowed as a committed ProjectReference. CI for a lab checks out the lab and restores. It does not need the whole forest cloned to prove the lab still compiles against the feed.

There is a force flag, NovolisUseProjectReferences. I use it when I mean to. I turn it off when I am about to claim a consumer works. "It built" is not one fact. It built against siblings, or it built against the feed. I want to know which. The afternoon switch is for the API. The feed is for the promise.

The tax

The tax is that I am often one publish behind myself. I change physics. The simulation repo still restores yesterday's package. Restore fails, or worse, restore succeeds and the new test I have not written yet would have failed. The fix is a commit on main so the merge build ships the package, then the consumer moves. Two pulls. A lag. A version number that does not match the branch I am staring at.

I already wrote about the folder of nugets. That cheat is tempting exactly because of this lag. Pack to a directory, point nuget.config at it, both repos go green, nobody has to wait for GitHub Actions. Seventy repos plus a private feed is one repo with worse git. The history is split, the binary is shared through a folder nobody can diff, and the version is whatever I stamped. I banned it because I liked it too much.

Build-line floats are the other way to dodge the wait. Pin 2026.1.10.* and you are racing the publish of that one line. Pin 2026.1.* and you take the platform line, including the risk that a junk version sorts first. I would rather wait for a real publish than win the race against my own CI.

What the fence is made of

It is not a speech about bounded contexts. It is a few mechanical facts.

Same repository: project references are normal. Tests reference src. That is one history, and it should be.

Another repository: a package, or a LibraryReference that governance expands. The committed file does not contain ../novolis-physics/...csproj.

Third-party: nuget.org, pinned. I do not float someone else's stack on the same line I use for mine.

The verifier fails the merge when a csproj props the door open. The analyzer fails the compile when the reference is the kind that pulls a window into a library. Those are different fences. This post is the repo fence. If it still compiles, it is not a rule is the compile fence. I need both. A project reference across repos compiles. That is why the script exists.

Why I still want it

A physics afternoon should not be able to become a UI afternoon just because the project system made the hop free. If I want a window on a body, I open a lab or an app, and I take the physics package. The window lives where windows live. I wrote that half in the mess has an address. The fence is what keeps the address true. Without it, every repo is a little bit of library and a little bit of host, and the folder structure is decoration.

About five hundred packages do not fit in my head, and they do not fit in a prompt. The fence is how I stop pretending they should. I can work in one repo, publish, and let the next repo notice. The delay is the point. It is the time where I find out the public shape was wrong before every consumer has compiled the mistake.

All posts