Files
jeffryandClaude Opus 5 101b0280c4 venues moved to bedrock; keep only the sentence about externals
bedrock publishes venues.md now, so the four-line section here had become the
second saying of a rule owned elsewhere.

Kept the one sentence that is genuinely this document's: put a choice where
reconciliation will look for it, because a .usages.md is what somebody opens when
a document moves.

The old heading — "venues, for things you cannot fetch" — was wrong and had been
since it was written. A venue is not defined by being unfetchable, it is defined
by your having decided something. Kafka's documentation is extremely fetchable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 10:55:32 -04:00

7.9 KiB

Externals

How a repository records what it depends on, and finds out when it changes.

Pull what you use

You fetch a copy of somebody's document and keep it at .loom/externals/<host>/<path>.md.

The path says where it came from, so nothing has to record an origin. This was false and it was load-bearing. A stored path is short enough to read and therefore too short to resolve: it drops the host's routing, the branch, and — worst — the published/ segment, which is the whole contract.

The path is for a person. The origin is recorded in the lock, resolved: host, route, branch, and full path.

Record the resolved URL and not the short form. A host may redirect a short form to whatever the default branch is at the time you ask — so a lock holding one is locked to a moving target, and a rename of the branch reports as a change in the document.

Pull what you need to understand, not everything it depends on. A document you fetch may refer to others; follow one when you hit something you do not know. Pre-resolving that is how you get a node_modules.

Confidentiality does not travel with the copy

Adopting is copying. So a document from a repository somebody may not read ends up in a repository they mayand the publisher loses control of it at the moment of adoption, because the copy's visibility is governed by your repository and not by theirs.

Do not adopt from a source less readable than the repository you are adopting into. If you may read it and your readers may not, copying it publishes it.

Two ways out, and the second is better when it is available.

Reference-onlyrecord the lock and fetch on demand, keep no copy. You give up reading it offline, which is most of what a copy is for, and you keep the dependency recorded and checkable.

Ask them to publishthe thing you needed was almost certainly not the confidential part. A repository that must stay private can still have a public sibling that publishes, and the split is usually along a line that already exists: the operational tree is what is sensitive; the pages telling somebody what to decide are not.

Two facets beside it

  • .usages.mdwhat we use, and which of our artifacts depend on it
  • .gaps.mdwhat we expected here and did not find

A usage that does not name what it justifies is half a usage. "We use the tailnet approach" is weak. "We use the tailnet approach — see manifests/ingress.yaml" is the whole value, because it answers the only question reconciliation asks.

What a lock holds

One record per adopted document, in .loom/externals/.locks:

  • where it was fetched fromresolved, as above
  • the publisher's ETagverbatim
  • the visibility the source could be read at, when it was fetched

It is committed, because the thing it locks is committed, and a lock that travels separately from what it locks is the drift this is meant to prevent.

A document with no lock is not broken; it is unlocked. Report it and fetch again. Do not adopt whatever the remote currently serves as the lock — that asserts your copy is the one being served, which is the thing you were going to check.

Why visibility is in the lock and not left to the moment

Access is checked once and the copy is durable. A token gets you the read; nothing revisits it afterwards. So the adoption's legitimacy rests on the relative visibility of two repositories — and that is changeable by a checkbox, a year later, by somebody who never saw the adoption.

Record public or not-public. Never private. An anonymous request can tell you a repository is not public. It cannot tell two repositories private to different people apartwhich is exactly the case where private into private genuinely widens access. Record the word you can verify.

Checking it costs one request per run, not one per document, because the only thing that must be current is your own visibility. Re-check a source only when the alarm would fireyou are public, it was recorded not-publicand if it has since gone public, update the lock and say nothing.

A stored visibility decays in both directions, which is why the alarm re-checks rather than trusting the record.

Freshness is a conditional request

Locked on the publisher's ETag, verbatim — never a hash you compute. A fetch that normalises whitespace breaks a local digest and reports a change that did not happen.

304 nothing changed
200 changed — the new copy is a candidate, not a replacement
410 gone — follow whatever the response points at
404 unresolved. Report both readings; do not pick one

404 is two different answers wearing one status. The document was withdrawn, or you no longer have access — and over HTTP they are indistinguishable, because a host that distinguished them would leak the existence of things you may not see.

Say both. Do not guess. They want different actions — re-pull elsewhere, versus ask somebody for access — and a tool that picks one will be wrong half the time silently.

Over ssh they are distinguishable — permission denied against repository not found — so a client that has both transports should say which it used.

Reconciliation runs the other way

You cannot fold your facets back into their document. So the question is not what do we rewrite here:

Given what changed in theirs, what do we change in ours?

The facets usually survive unchanged. What moves is the manifests, the config, the code that a usage named — which is why a usage names them.

And gaps reconcile too, which is the half nobody builds for: a new version may have filled one, and nothing will tell you.

A facet describes the local pair. A gap is true of the copy you hold, not of the document upstream — so a publisher fixing their end does not close it. It closes when you fetch the new copy and replace the pair, and until then it is still true of what is in your tree.

What survives a closed gap is not the gap. It is what the gap justified. If you recorded a local workaround, ask whether it is retired or merely no longer provisionalthe second is the common case and it is invisible in the code, which is why it is an entry in your own log. Somebody inheriting your workaround will go looking for the gap that justified it, and find nothing.

A closed gap is not a decline. A decline is what you considered and did not do; a closed gap is what you needed and got.

The new copy replaces the old pair wholesale. There is no merging a document you do not own.

A choice about something you adopted does not go in a venue file

Put a choice where reconciliation will look for it. When the thing you decided about publishes a document you hold a copy of, the choice belongs in the .usages.md beside that copybecause that is the file somebody opens when the document moves.

Venues are for what is left: things that constrain you without publishing anything you could adopt. See venues.

Gaps that belong to nobody

.loom/gaps/a need with no owner. The test: could you say whose job this is? If yes, it is a claim against them and belongs in their .gaps.md.

Moving a file from gaps/ into an external's .gaps.md is how a gap gets homed. The position is the claim.

A gap records the local workaround beside the need. "We are using Docker Hub for now." Forgetting it was provisional is the disease; the provisional answer is fine — and when the gap is finally homed, every workaround that should be retired is findable.