# 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`.*** ## 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* **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. ## 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.* ## Venues, for things you cannot fetch **`.loom/venues/.md`** — *Kafka, React, HTTP.* **Choices we made about something with no document to hang them on.** *If there is a document, hang it there instead: **put a choice where reconciliation will look for it.*** ## 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.