Files
externals/.loom/published/externals.md
T
jeffryandClaude Opus 5 544f895920 confidentiality does not travel with the copy
loom saw it while arranging access to a private cluster repository: then you would
have a file from a private repo in your repository, and that does not sound
kosher. It is not, and it is a hazard this convention creates by construction.

Adopting is copying, so a document from a repository somebody may not read ends up
in one they may, and the publisher loses control at the moment of adoption because
visibility is governed by the consumer's repository rather than theirs. Nothing in
a tree marks a copy as having come from somewhere private — a credential lets you
read and does not let you redistribute, and this convention had no way to say so.

Two ways out. Reference-only: record the lock, fetch on demand, keep no copy,
giving up offline reading which is most of what a copy is for. Or ask them to
publish, which is usually right because the thing you needed was almost certainly
not the confidential part — a repository that must stay private can have a public
sibling that publishes, and the split follows a line that already exists, since
the operational tree is what is sensitive and the pages telling somebody what to
decide are not.

Recorded as a rule rather than a case because the failure is silent and one-way:
once copied into a public tree it is published, and no later fix retrieves it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 15:09:47 -04:00

149 lines
6.5 KiB
Markdown

# 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 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*
**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/<name>.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.