# 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//.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 may* — **and 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-only** — *record 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 publish** — *the 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.md`** — *what we use, and **which of our artifacts depend on it*** - **`.gaps.md`** — *what 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 from** — *resolved, as above* - **the publisher's `ETag`** — *verbatim* - **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** apart* — **which 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 fire** — *you are public, it was recorded not-public* — *and 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 provisional* — **the 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 copy* — **because 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](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/venues.md).** ## 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.