venues, and a guide staleness check that could not see an arrival

Adopts bedrock/venues.md and reconciles externals.md, which dropped its four-line
venue section in favour of the one sentence that is actually about externals.

The staleness check compared only documents present in both the stamp and the
locks, so a section gaining a page never flagged its guide — and the bedrock
guide enumerates its pages by hand. It went silently incomplete the moment venues
was adopted. Now reports changed, added and gone separately, in words that fit
each case.

Found the same way as the sidebar: by adopting something real for the first time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 10:56:54 -04:00
co-authored by Claude Opus 5
parent 2d766d0a72
commit c4e2a2e67d
8 changed files with 245 additions and 16 deletions
+2 -1
View File
@@ -9,6 +9,7 @@ git.hypertheory-labs.dev/loom/bedrock/publication.md https://git.hypertheory-lab
git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/recording-decisions.md "d190eeae7929b9a99ecf09211d94e7e5b32635bd" public git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/recording-decisions.md "d190eeae7929b9a99ecf09211d94e7e5b32635bd" public
git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md "a46446a34ccb8bfc533d3cce19f4c88548c4fa04" public git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md "a46446a34ccb8bfc533d3cce19f4c88548c4fa04" public
git.hypertheory-labs.dev/loom/bedrock/starting.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md "b9eefba0f4668a496ccfc6a1377277f0721456d2" public git.hypertheory-labs.dev/loom/bedrock/starting.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md "b9eefba0f4668a496ccfc6a1377277f0721456d2" public
git.hypertheory-labs.dev/loom/bedrock/venues.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/venues.md "35e940195e2ba9a30b4b4dc81a57f9383c709f64" public
git.hypertheory-labs.dev/loom/cart/cart.md https://git.hypertheory-labs.dev/loom/cart/raw/branch/main/.loom/published/cart.md "15331f1a9cc81bf61a44830cfbb7c274f4c2b119" public git.hypertheory-labs.dev/loom/cart/cart.md https://git.hypertheory-labs.dev/loom/cart/raw/branch/main/.loom/published/cart.md "15331f1a9cc81bf61a44830cfbb7c274f4c2b119" public
git.hypertheory-labs.dev/loom/externals/externals.md https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md "50673ccffc14d57150ac0a9027b0712d9dcf940d" git.hypertheory-labs.dev/loom/externals/externals.md https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md "772c67f460f0ff3b9563aee9b38a64c0f3423dbd"
git.hypertheory-labs.dev/loom/loom-cli/guarantees.md https://git.hypertheory-labs.dev/loom/loom-cli/raw/branch/main/.loom/published/guarantees.md "e8c697ea61df47838f38af7353e29d9ae3f7070c" git.hypertheory-labs.dev/loom/loom-cli/guarantees.md https://git.hypertheory-labs.dev/loom/loom-cli/raw/branch/main/.loom/published/guarantees.md "e8c697ea61df47838f38af7353e29d9ae3f7070c"
@@ -0,0 +1,81 @@
# Venues
**`.loom/venues/<name>.md` — a choice you made about something that has no
document of its own to hang it on.**
*Kafka. React. HTTP. The organisation you work for.* **Things that constrain what
you build without ever being invoked, and without publishing anything at you that
you could adopt.**
---
## Venue and external are two questions, not two kinds of thing
**`external` is *who published it*. `venue` is *whether you had to decide
anything about it*.** *They are orthogonal. A thing may be both, either, or
neither.*
| | published at us | we recorded a choice | files |
|---|---|---|---|
| Kafka, React, HTTP | yes, by the world | yes — *protobuf for schema* | venue only |
| a service another team owns | yes, by them | not yet — the use is in the code | external only |
| a cluster your own org publishes | yes | yes | both |
| a library you use exactly as documented | yes | no | neither |
**Both files are governed by the same rule, applied to two different ambients:**
> **An external entry exists when the reader cannot be assumed to know the
> thing.** *A venue file exists when you made a choice worth recording.*
**Nobody needs telling what Kafka is, so there is no external entry** — *but "we
use protobuf for schema" is a choice, so there is a venue file.* **Nobody outside
can know what your internal API is for, so there is an external entry** — *and
until using it requires an exception or a preference, there is nothing to
accommodate and no venue file, because the actual use is in the code.*
*This is why sorting things into two bins does not work and people keep trying.
The bins were never the problem. The question was.*
## A venue file is an account of your own decisions
**Which is what makes it local by construction.** *An external is a copy of
somebody's document and could in principle be centralised.* **A venue file has no
upstream** — *there is nobody to reconcile with, because the only party who
decided anything was you.*
**So it does not go stale the way a copy does.** *It goes stale when you change
your mind and do not say so*, which no fetch will ever detect.
> **A venue file that carries no delta should not exist.** *If you use the thing
> exactly as documented, write nothing.* **Recording the adoption is ceremony.**
## If there is a document, hang the choice on the document
**Put a choice where reconciliation will look for it.** *When the thing you
decided about publishes something you have adopted, the choice belongs in the
`.usages.md` beside that copy and not in a venue file* — **because that is the
file somebody opens when the document moves**, *and a venue file is not.*
*The venue file is for what is left: the constraints of a thing that publishes
nothing you can hold.*
## A venue is where an authority claim belongs
**Because a venue fact carries a reason and can therefore be shown wrong.**
*The alternative is worse and common: somebody with authority enters a
conversation as a voice, and "we do it this way" arrives with no reason attached
and nothing that could contradict it.* **Written as a venue constraint, the same
instruction becomes a claim about the world**, *and a claim about the world is
something a later reader can discover was false.*
## Checking it is still true
**Nobody will tell you.** *There is no publisher to ask, no `ETag`, and no
conditional request that will ever come back `200`.*
**So a venue file needs what a published document needs: a date, and the reason
underneath the choice.** *The reason is the whole of it***"we use protobuf for
schema" is unfalsifiable a year later; "we use protobuf because the consumers are
in three languages and JSON schema drift was costing us a release a quarter" can
be checked against a world that may have changed.**
@@ -147,13 +147,16 @@ do; a closed gap is what you needed and got.*
**The new copy replaces the old pair wholesale.** *There is no merging a document **The new copy replaces the old pair wholesale.** *There is no merging a document
you do not own.* you do not own.*
## Venues, for things you cannot fetch ## A choice about something you adopted does not go in a venue file
**`.loom/venues/<name>.md`** — *Kafka, React, HTTP.* **Choices we made about **Put a choice where reconciliation will look for it.** *When the thing you
something with no document to hang them on.** 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.**
*If there is a document, hang it there instead: **put a choice where *Venues are for what is left: things that constrain you without publishing
reconciliation will look for it.*** 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 ## Gaps that belong to nobody
+22 -3
View File
@@ -154,13 +154,25 @@ function guidePage(repo, records, existing) {
let data = { title: repo, loom: {} } let data = { title: repo, loom: {} }
let body = GUIDE_TEMPLATE(repo) let body = GUIDE_TEMPLATE(repo)
let stale = [] let stale = []
let changed = []
let added = []
let gone = []
if (existing) { if (existing) {
const split = splitFrontmatter(existing) const split = splitFrontmatter(existing)
data = { ...split.data } data = { ...split.data }
body = split.body body = split.body
const was = new Map((data.loom?.writtenAgainst ?? []).map((s) => [s.path, s.etag])) const was = new Map((data.loom?.writtenAgainst ?? []).map((s) => [s.path, s.etag]))
stale = records.filter((r) => was.has(r.path) && was.get(r.path) !== r.etag).map((r) => r.name) // A section gaining or losing a document changes the guide too, and this
// did not notice until bedrock grew a fifth page under a guide that
// enumerates its pages by hand. Comparing only what is present in both is a
// comparison that cannot see an arrival.
changed = records.filter((r) => was.has(r.path) && was.get(r.path) !== r.etag).map((r) => r.name)
added = records.filter((r) => !was.has(r.path)).map((r) => r.name)
gone = [...was.keys()]
.filter((p) => !records.some((r) => r.path === p))
.map((p) => p.split('/').pop().replace(/\.md$/, ''))
stale = [...changed, ...added, ...gone]
} }
data.title ??= repo data.title ??= repo
@@ -175,9 +187,16 @@ function guidePage(repo, records, existing) {
// only ever know that they differ. The first time it fired in real use, the // only ever know that they differ. The first time it fired in real use, the
// guide was written against a version newer than the copy here, and a // guide was written against a version newer than the copy here, and a
// message claiming "an older X" was confidently backwards. // message claiming "an older X" was confidently backwards.
const which = stale.map((s) => `<code>${s}</code>`).join(', ') const code = (xs) => xs.map((x) => `<code>${x}</code>`).join(', ')
const parts = []
if (changed.length)
parts.push(
`${code(changed)} differs from the version this page was written against — so one of the two is behind`,
)
if (added.length) parts.push(`${code(added)} was adopted after this page was written`)
if (gone.length) parts.push(`${code(gone)} is no longer adopted`)
data.banner = { data.banner = {
content: `Written against a different ${which} than the copy in this repository — so one of the two is behind. Run <code>loomctl external check</code>, or re-read this page, then <code>npm run ack</code>.`, content: `${parts.join('; ')}. Run <code>loomctl external check</code>, or re-read this page, then <code>npm run ack</code>.`,
} }
} else { } else {
delete data.banner delete data.banner
+16
View File
@@ -12,6 +12,8 @@ loom:
etag: '"a46446a34ccb8bfc533d3cce19f4c88548c4fa04"' etag: '"a46446a34ccb8bfc533d3cce19f4c88548c4fa04"'
- path: git.hypertheory-labs.dev/loom/bedrock/starting.md - path: git.hypertheory-labs.dev/loom/bedrock/starting.md
etag: '"b9eefba0f4668a496ccfc6a1377277f0721456d2"' etag: '"b9eefba0f4668a496ccfc6a1377277f0721456d2"'
- path: git.hypertheory-labs.dev/loom/bedrock/venues.md
etag: '"35e940195e2ba9a30b4b4dc81a57f9383c709f64"'
--- ---
Read [starting](/bedrock/starting/) first — it is two questions and a directory, Read [starting](/bedrock/starting/) first — it is two questions and a directory,
@@ -29,6 +31,20 @@ repository starts depending on you:
by whoever wrote the facet. by whoever wrote the facet.
- [loom-directory](/bedrock/loom-directory/) — type by location. A stream is a - [loom-directory](/bedrock/loom-directory/) — type by location. A stream is a
file; a collection of things with identity is a directory. file; a collection of things with identity is a directory.
- [venues](/bedrock/venues/) — a choice you made about something that publishes
nothing you could adopt. Kafka, React, the organisation you work for.
## Venue and external are two questions, not two bins
Worth reading [venues](/bedrock/venues/) alongside
[externals](/externals/externals/), because sorting things into two kinds is the
mistake almost everybody makes first, and we made it for a day.
**`external` is who published it. `venue` is whether you had to decide anything
about it.** They are orthogonal, so a thing can be both, either, or neither —
and the same rule governs both files: record only the delta. Nobody needs telling
what Kafka is, so there is no external entry; *we use protobuf for schema* is a
choice, so there is a venue file.
## The one that gets skipped ## The one that gets skipped
+98
View File
@@ -0,0 +1,98 @@
---
title: Venues
editUrl: false
loom:
generated: true
path: git.hypertheory-labs.dev/loom/bedrock/venues.md
source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/venues.md
etag: '"35e940195e2ba9a30b4b4dc81a57f9383c709f64"'
visibility: public
---
**`.loom/venues/<name>.md` — a choice you made about something that has no
document of its own to hang it on.**
*Kafka. React. HTTP. The organisation you work for.* **Things that constrain what
you build without ever being invoked, and without publishing anything at you that
you could adopt.**
---
## Venue and external are two questions, not two kinds of thing
**`external` is *who published it*. `venue` is *whether you had to decide
anything about it*.** *They are orthogonal. A thing may be both, either, or
neither.*
| | published at us | we recorded a choice | files |
|---|---|---|---|
| Kafka, React, HTTP | yes, by the world | yes — *protobuf for schema* | venue only |
| a service another team owns | yes, by them | not yet — the use is in the code | external only |
| a cluster your own org publishes | yes | yes | both |
| a library you use exactly as documented | yes | no | neither |
**Both files are governed by the same rule, applied to two different ambients:**
> **An external entry exists when the reader cannot be assumed to know the
> thing.** *A venue file exists when you made a choice worth recording.*
**Nobody needs telling what Kafka is, so there is no external entry** — *but "we
use protobuf for schema" is a choice, so there is a venue file.* **Nobody outside
can know what your internal API is for, so there is an external entry** — *and
until using it requires an exception or a preference, there is nothing to
accommodate and no venue file, because the actual use is in the code.*
*This is why sorting things into two bins does not work and people keep trying.
The bins were never the problem. The question was.*
## A venue file is an account of your own decisions
**Which is what makes it local by construction.** *An external is a copy of
somebody's document and could in principle be centralised.* **A venue file has no
upstream** — *there is nobody to reconcile with, because the only party who
decided anything was you.*
**So it does not go stale the way a copy does.** *It goes stale when you change
your mind and do not say so*, which no fetch will ever detect.
> **A venue file that carries no delta should not exist.** *If you use the thing
> exactly as documented, write nothing.* **Recording the adoption is ceremony.**
## If there is a document, hang the choice on the document
**Put a choice where reconciliation will look for it.** *When the thing you
decided about publishes something you have adopted, the choice belongs in the
`.usages.md` beside that copy and not in a venue file* — **because that is the
file somebody opens when the document moves**, *and a venue file is not.*
*The venue file is for what is left: the constraints of a thing that publishes
nothing you can hold.*
## A venue is where an authority claim belongs
**Because a venue fact carries a reason and can therefore be shown wrong.**
*The alternative is worse and common: somebody with authority enters a
conversation as a voice, and "we do it this way" arrives with no reason attached
and nothing that could contradict it.* **Written as a venue constraint, the same
instruction becomes a claim about the world**, *and a claim about the world is
something a later reader can discover was false.*
## Checking it is still true
**Nobody will tell you.** *There is no publisher to ask, no `ETag`, and no
conditional request that will ever come back `200`.*
**So a venue file needs what a published document needs: a date, and the reason
underneath the choice.** *The reason is the whole of it***"we use protobuf for
schema" is unfalsifiable a year later; "we use protobuf because the consumers are
in three languages and JSON schema drift was costing us a release a quarter" can
be checked against a world that may have changed.**
<div class="loom-provenance">
This page is a copy of a document published by `loom/bedrock`, rendered here.
The source is [https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/venues.md](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/venues.md) and is what the copy is checked against.
</div>
+9 -6
View File
@@ -5,7 +5,7 @@ loom:
generated: true generated: true
path: git.hypertheory-labs.dev/loom/externals/externals.md path: git.hypertheory-labs.dev/loom/externals/externals.md
source: https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md source: https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md
etag: '"50673ccffc14d57150ac0a9027b0712d9dcf940d"' etag: '"772c67f460f0ff3b9563aee9b38a64c0f3423dbd"'
visibility: unknown visibility: unknown
--- ---
**How a repository records what it depends on, and finds out when it changes.** **How a repository records what it depends on, and finds out when it changes.**
@@ -155,13 +155,16 @@ do; a closed gap is what you needed and got.*
**The new copy replaces the old pair wholesale.** *There is no merging a document **The new copy replaces the old pair wholesale.** *There is no merging a document
you do not own.* you do not own.*
## Venues, for things you cannot fetch ## A choice about something you adopted does not go in a venue file
**`.loom/venues/<name>.md`** — *Kafka, React, HTTP.* **Choices we made about **Put a choice where reconciliation will look for it.** *When the thing you
something with no document to hang them on.** 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.**
*If there is a document, hang it there instead: **put a choice where *Venues are for what is left: things that constrain you without publishing
reconciliation will look for it.*** 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 ## Gaps that belong to nobody
+9 -1
View File
@@ -3,7 +3,7 @@ title: externals
loom: loom:
writtenAgainst: writtenAgainst:
- path: git.hypertheory-labs.dev/loom/externals/externals.md - path: git.hypertheory-labs.dev/loom/externals/externals.md
etag: '"50673ccffc14d57150ac0a9027b0712d9dcf940d"' etag: '"772c67f460f0ff3b9563aee9b38a64c0f3423dbd"'
--- ---
A copy of somebody's document, a lock recording where it came from, and — beside A copy of somebody's document, a lock recording where it came from, and — beside
@@ -38,6 +38,14 @@ request cannot tell two repositories private to *different people* apart — and
that is precisely the case where private-into-private genuinely widens access. that is precisely the case where private-into-private genuinely widens access.
Record the word you can verify. Record the word you can verify.
## What does not go in a `.usages.md`
A choice about something you have *not* adopted — Kafka's wire format, the
organisation's policy — is a [venue](/bedrock/venues/), not a usage. This
document used to define venues in four lines and no longer does, which is the
right way round: put a choice where reconciliation will look for it, and a
`.usages.md` is what somebody opens when a document moves.
## Why this site can only hold public documents ## Why this site can only hold public documents
*Do not adopt from a source less readable than the repository you are adopting *Do not adopt from a source less readable than the repository you are adopting