Back to the network

Tooling

0-libraries

Fourteen packages shared by copy rather than as dependencies, with a wrapper that tracks every vendored copy.

Copies, not dependencies

Some code kept getting copied from one of my repos to the next: a bootstrapper, a connector, the core of a small chat bot. Copies drift. When I compared the copies of that bot, one of its files differed by 103 lines, and a safety guard was in some copies and missing from others. Nothing tied a copy to where it came from.

0-libraries is where those pieces live now, as fourteen packages. A package is a directory the library declares in one file, packages.toml, and its version is the last commit that touched that directory. A project that uses them, a consumer, carries a single folder, 0-lib/, holding the packages it asked for. Each package is either vendored, a copy inside 0-lib/, or linked, a path into the library's own checkout. One rule picks between them: link where the library is on disk, vendor where it is not. A vendored consumer runs on a machine that has never seen the library.

The wrapper knows which copy it holds

The one package every consumer carries is the wrapper, its access point to the rest. When the wrapper installs a package, it records a hash of the copy and the commit it came from, in a file committed with the consumer. That record answers the question loose copies never could: is this copy only behind the library, or did someone change it by hand? A stale copy can be replaced. An edited one is refused unless I pass --force, because replacing it would throw someone's work away. Each consumer chooses whether a stale copy is only reported, offered once, or synced on its own; an edited copy is never synced on its own.

The record did not always travel. It used to live in a file each machine kept for itself, so a fresh clone had none, and the wrapper could not tell stale from edited. Its advice in that case was --force, the one thing its guard exists to prevent. Since decision 0008 the record is committed with the consumer, and a clone knows what it holds.

A swap that survives a crash

How the wrapper replaces a vendored copyFirst the wrapper checks whether the copy is current, stale or edited by hand, against the hash taken at install and committed with the consumer. Then it stages a fresh copy inside the 0-lib container, beside the copy in use. It renames the old copy aside instead of deleting it, moves the fresh copy into the package's place and carries the machine's runtime folder across. Only then does it delete the aside. If the move fails, the aside is moved back, drawn as a dashed line, and after a crash the container's own README says how to recover by where the swap stopped.A sync, inside the consumer's 0-lib/1 · Checkcurrent, stale or edited by hand? The hash takenat install, committed with the consumer, decides2 · Stagea fresh copy is written inside 0-lib/,beside the copy in use3 · Set asidethe old copy is renamed, not deleted:.wrapper-stage-*-aside4 · Move inthe fresh copy takes the package's place;the machine's .runtime/ is carried across5 · Doneonly now, and onlythen, delete the asideOn failurerestore on failure:the aside moves back.After a crash, thecontainer's READMEsays how to recover
How the wrapper replaces a vendored copy. The old copy is set aside, never deleted first; a failed move puts it back.

Replacing a folder is not one atomic step, so the wrapper never deletes first. It stages the new copy inside the container, renames the old copy aside, moves the new one in, and deletes the aside only when the swap is complete. If the move fails, the aside goes back. A crash leaves the aside on disk, and the container's own README says how to recover from each place the swap can stop. A test forces the move to fail and checks that the old copy is still intact.

The wrapper updates itself the same way, so a sync can replace the very code that is running it. Setting the old copy aside is what makes that survivable.

The library never writes into a consumer

The first version pushed: hooks in the library wrote updated files into consumers on their own. Writing into consumer repositories unattended needed a defence against a bad push, signed commits, and that defence was never switched on, because no commit was signed. Push was retired on 2026-08-20, nine days after the first commit. Now a consumer pulls, and only when it asks.

A pull does not sync either. "Stale" has no direction: it says a copy differs from the library's checkout, not that the checkout is newer. A hook that synced on every pull could move a current consumer backwards to a checkout that lags. So the library keeps a report instead, a script that checks every consumer on a machine and fails unless all their copies are current, and the decision to sync stays with the consumer.

Even the container's name was measured. Across 16 of my repos, the name config was already in use in five, four of them the projects' own folders, so a flat layout would sooner or later have read a project's own settings as the library's. Everything the library owns sits inside one folder with a fixed name.

Decisions, written down

Every choice above has a numbered decision record: 28 so far, each with its context, and most with the options it turned down and why. A record is not rewritten: a later record supersedes it, or a dated amendment is added to it. A few worth reading:

Tests stay home

The library has 766 test functions: 237 for the wrapper, the setup and the reports, 469 for the Python packages and 60 for the Python parts of the shell packages, beside shell suites of their own. None of them travel. A vendored copy is checked by its hash, so a suite shipped inside it would prove nothing the hash does not, and would be dead weight in every consumer.

On the shelf

Stack

  • Python
  • POSIX shell
  • cmd.exe
  • uv
  • pywebview

Connected to