loom-cli's builder measured all three things we had published and none could be fetched by somebody who is not us: settled 404, .loom fetchable but not where a reader lands, and homelab-cluster 404 while this page named it as the canonical answer. Three for three. A public page naming a private thing as its canonical answer is worse than no example — it costs the reader a request and their confidence in the rest of the page. The page now says the example exists, is private, and why, and does not link it. The diagnosis is theirs and worth keeping: publication was being treated as a change of directory rather than the act publication.md describes. Moving a file into published/ is not publishing; the file becoming fetchable is. And reachable, the command we nearly cut for being a third kind of thing, found all three on the day it would have shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
174 KiB
Event log
Decisions, appended, newest last, never revised. An entry states what was decided, what it is believed to advance, and the belief that could turn out false. Later entries supersede earlier ones by saying so; nothing above is edited.
Entries 1–78 were converted together from cart almanac on 2026-09-07, and are
tagged with it. Reading them in order shows the round changing its own mind —
notably at entry 69, where the ontology was bracketed.
1. The catalogue/commitment split — waits on loom
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
"an affordance has exactly one staging" is a leaf-level rule. A venue
publishes a catalogue of alternative stagings; an application commits to one.
§1 is unamended and holds where it was written.
Advances the registry being describable without §1 having to be reopened, which is what the handoff asked for and what I twice failed to do by reading the rule at the wrong altitude.
Because I reported the rule as broken — Traefik and Tailscale are both current, and choosing between them is the point — and loom's diagnosis was that I was one level up from where the rule applies. Angular's trigger change detection has several stagings because Angular offers a menu; an application has one because it chose. The rule was never about alternatives; it was about commitment, and a document describing a venue is not describing an application. The cost, stated so it is not rediscovered as a defect: affordance now names two populations — what a venue offers and what an application offers — and only the second is what §1 defines.
2. Freshness is a property of a bead, and the special case was never scope
Decided (2026-09-05, claude-substrate — discovered, cart: almanac): a
bead outside a version-controlled tree needs how to re-check it and when it
last was checked, and this is the axis on which the general case differs from
the repository case. Not modelled yet; recorded so that adding it later is
recognised as this decision rather than as a new idea.
Advances knowing which of §1's silences are deliberate and which are artifacts of only ever having described code.
Because I believed the special case was scope — one repository versus
many — and it is not. In a code repo the description and the referent are in
the same tree, so a bead can only go stale by way of a commit: there is a diff,
an author, and a moment. A bead pointing at a cluster goes stale while nobody
acts. gitea-certificate expires 2026-10-25 and no one will have done anything.
The evidence is that loom's cluster README already carries the mechanism, written
by hand and without the vocabulary: "Verified against the live cluster on
2026-09-03", plus a # Verification section of commands that re-establish the
claim. A dated claim and a way to re-earn it. §1 says what is true and
nothing about when it was last known to be.
And the price of leaving it out is measured, not hypothetical. That cluster
ran over a month with no usable backup while every status field reported healthy:
destinationPath and endpointURL both named the bucket, so barman wrote and
read at the same wrong path and ContinuousArchiving stayed True. A bead
that resolved to the wrong referent while claiming to resolve. The handoff
calls a bead's reference resolving-and-not-drifting the one mechanically
checkable claim in the system, and then places resolution outside this
application. That may have deferred the load-bearing half, and the evidence was
in loom's cluster before either of us went looking.
3. Role names do not collide on their own
Decided (2026-09-05, claude-substrate — discovered, cart: almanac):
cross-staging comparison is authored at the affordance, not derived from
role names. No role vocabulary is imposed and no matching is built.
Advances the registry's central payoff — how many ways do we do this? — resting on a mechanism that exists, rather than on a collision that does not happen.
Because I had planned to defer this until two real substrates existed and then look at whether their role names had collided without being told to. The test was already available and it has run. Two stagings of expose a service, written by the same author, in the same repository, one day apart: TAILSCALE.md names the roles as questions in the negative — needs a DNS record, needs a cert-manager Certificate, needs a router port-forward — and README.md names the same concerns as positive inventory sections — DNS, Certificates, Networking and TLS. Both are correct, and they diverge because a comparison view names what a staging avoids while an inventory names what exists.
If one author cannot collide with himself across two files in one repo, two teams will not collide across an enterprise. I still decline to impose a vocabulary — imposed early it is routed around, and matched loosely it produces findings nobody can wave off in a sentence, which §4 says takes the real signals with it when they get muted.
What this buys, which is why it is a decision and not a defeat: the affordance-level document is where somebody names the roles once, and the stagings become answerable to them. The registry's unit of work is not collecting stagings; it is authoring the roles they answer to.
The carve-out, which is where the payoff actually lives. Role names do not collide when authors name them. They collide for free when a venue imposes them. Five applications in loom's classroom repo each cast authenticates a user, and each casts it with a Keycloak client — five different beads, one role, and nobody agreed on a word for it. The venue supplied the vocabulary by owning the thing.
So two questions that look alike are not the same difficulty:
- "If we moved from Keycloak to Authentik, what would need addressing?" is answerable and nearly free — the role is venue-imposed, and the venue can enumerate its own clients without any application publishing anything.
- "How many ways do we verify credit cards in this enterprise?" stays hard, because nothing owns verify a credit card and no registry can be listed.
Where a venue owns the thing, the graph is enumerable from the venue's side rather than assembled from the applications' — which makes a venue's own inventory the cheapest useful projection available, and the first one worth building.
Falsifier, so this can go stale: two stagings written independently, by different authors, with no venue imposing the vocabulary, whose role names match closely enough that a reader accepts the pairing without being told. One such pair is not proof; three would mean I generalised from a single author's habits, which is exactly what this entry accuses the enterprise case of.
4. The registry must have a place for not knowing — loom's belief, stated by loom
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
the substrate is not spec-driven and not a wiki. Every mechanism is judged by
whether it has a place for not knowing, not only a place for the answer.
Advances the one design pressure most likely to pull this project somewhere useless, being named before it acts rather than after.
Because loom put it plainly: "There is a lot of influencer talk about creating wikis for your LLM — all of those things are based on 'the human knows, just have stuff the LLM can find.' We humans don't know." A wiki assumes someone possessed the answer and only distribution was missing. That assumption is false here and the whole project is built on its being false — the handoff itself says two weeks were spent unable to tell which ideas were load-bearing.
What makes this an entry rather than an attitude is that four mechanisms already implement it and were each built for a different reason, which is the evidence it was structural before it was noticed:
known.ts's four states. Looked-at-and-empty is a positive finding, and it is the state a wiki cannot represent — a wiki has present and absent, and absent means nothing.- The write-ahead log, holding findings that are explicitly not yet decisions.
- Declines, recording what was considered and refused.
- The cart, which exists so that "I need to know more from you before I do this, and I suspect you don't know either" has somewhere to go.
The falsifier is the design rule: a feature that can only represent the answer is wrong here, however useful it looks. If that rule ever has to be suspended to ship something, this entry is what it is being suspended against.
5. The system is derived, not stored
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
the third relation — several applications jointly providing what none provides
alone — is a projection over the externals graph, not a stored entity. It is
called system. Nothing models it, and no file holds one.
Advances not building a container, and not having to name one.
Because I put the choice as entity or dissolves, and both were wrong. loom's answer was neither: "the aggregation thing really could be a projection of you and I looking at a graph of repos with externals, and then saying something about it." There is nothing to name because there is nothing stored — the naming problem I declined to solve was one I should not have had.
system over aggregation for a reason about readers rather than correctness:
aggregation looks precise, so a DDD reader assumes they already know it and
smuggles bounded context in. A term that looks precise gets assumed; a vague
one gets defined.
The half that is not derived, recorded so it is not discovered later as a defect: the graph is computed and "saying something about it" is authored. So a system view is a projection in §5's sense — audience-shaped, and it goes stale — and it is the first one we will actually build.
Systems may be named, and they nest — Hypertheory Training containing Hypertheory Classroom Management. Naming one is fine and sometimes necessary. Listing its constituents is not:
The name is authored. The membership is projected. A system file never holds a roster.
A roster is a layout by another name, and §4 already says anything emitted is treated as a contract — but worse than stale, a roster lies, because membership is created by acts of judgement that never touch the roster. loom's own case: notes joins the graph the moment someone says "that belongs in notes", and nothing in any codebase changed. A hand-kept list would not have moved, and would have been believed.
There is also no privileged system view to go stale from — different beholders and different altitudes give different, equally correct systems. Membership is a projection with a parameter, not a fact.
What it must answer, from loom, and note that both are about meaning rather than breakage: "will a change in this app change meaning in the Tauri app?" and "if we get rid of the Tailscale stuff, what does that impact?" A build graph answers what breaks and can never answer what stops making sense — which is the non-obvious reason these are not called dependencies.
And what an external carries, from loom's own example of onboarding into the Tauri app: "students are authenticated using Keycloak; for this app there is only the notion of a current course; the VMs are destroyed at the end of a session." None of that is an interface. An external publishes what someone needs in order for your app to make sense to them, and an API schema would not have carried one of those three facts.
6. Venue and external are not two kinds of thing. They are two questions about one thing.
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
external is who published it; venue is whether we had to decide
anything about it. The two are orthogonal, and a thing may be both, either, or
neither. Neither file is created unless it carries a delta.
Advances the whole round's stickiest confusion collapsing into a rule that can be applied without judgement, instead of a taxonomy that needed one every time.
Because we spent a day sorting things into two bins and the bins kept
leaking. cart looked like both. React looked like a venue by one test and an
external by another. The bins were never the problem — the question was.
| published at us | we recorded a choice | files | |
|---|---|---|---|
| Kafka | yes, by the world | yes — protobuf for schema | venue only |
| React, HTTP | yes, by the world | yes | venue only |
| Hypertheory.API | yes, by us | not yet — the use is in the code | external only |
| the homelab, the DO cluster | yes, by loom's own repo | yes | both |
cart, annotating |
yes, by loom | yes — roles cast in cart.notes.md |
both |
And both files are governed by the same rule loom stated on day one — record only the delta from ambient knowledge — applied to two different ambients:
An external entry exists when the reader cannot be assumed to know the thing. A venue file exists when we 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 Hypertheory.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 retires my earlier framing that "unowned venue files stay local because
there is nothing to centralise." That was true and it was the wrong reason. The
reason is that a venue file is an account of our own decisions, which is local by
construction — so cart.notes.md, which I filed as annotations, has been a venue
file the entire time.
7. The registry is consulted at decision points, not at session start
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
the registry's primary use is during work, not before it. It answers
questions asked by the developer at a decision point. It does not detect,
interrupt, or advise unasked.
Advances building the thing that catches the expensive failure, without building an enforcer that gets muted for its trouble.
Because I had it as an onboarding surface: read once, get oriented, proceed. The failure loom actually paid for was not an onboarding failure. An hour was lost rebuilding the homelab cluster, making decisions that were correct for the work in view, because the reason for the rebuild — staging for the classroom system — arrived after "done".
And loom had that context the whole time. It was not missing; it was not retrieved at the moment it would have changed a decision. So the symmetry loom proposes is not a courtesy toward a forgetful agent — it is accurate in both directions. I never had the context; loom had it and did not surface it; the outcome was identical. The failure is retrieval at a moment, not memory over time, and a surface read at session start would not have fired, because the decisions that needed it were made an hour in.
On who initiates, which loom corrected after this entry was first staged and before it was applied. The original said the registry produces grounds to open a cart — an agent noticing overlap and raising it. That is an enforcer, and it is refused. loom: "I have no interest in putting you in the role of enforcer or cop."
Three postures, and only the middle one is rejected:
- Asked — the developer poses the question at the moment it matters; the registry is where the answer comes from. This is the design.
- Enforcing — the agent detects and interrupts. Refused.
cart.mdalready forbids it in another context: "a dialog that invents an obligation creates shadow policy — something everyone obeys, nobody agreed to, and which cannot be found or argued with." An unasked finding is exactly that. - Legible — the fact is simply visible where the developer is already looking, creating no obligation. Permitted, and it is what remains of the problem, since loom's own account is that in the homelab repo the question never occurred: "I saw them as a sequence of problems to be solved."
The distinction is grammatical and that is not a joke: in an enforcing finding the agent is the subject of the sentence, and in a legible one the artifact is. The test is whether it produces an obligation.
What survives from the first draft: the cart is still where such a conversation goes, and the moment is still mid-work. What was wrong was the arrow.
8. Being wrong out loud is the mechanism, not the failure mode
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
externals and venue files are claims, not specifications. No validation, no
required fields, and no enforcement of correctness. A wrong entry that provokes a
correction is a success of the system, not a defect in it.
Advances the registry staying cheap to write, which is the only condition under which anything gets written in it.
Because loom put the case better than an argument for accuracy would: "'Should we put this in notes? I see that as an external' — 'ohhh, that is a leftover from an old way of thinking, we need to update that' is just as, or more, useful than trying to enforce correctness." The value was produced by the entry being wrong. A specification that is wrong is a liability; a claim that is wrong is a prompt, and the difference is entirely whether the artifact is read as authority or as a conversational move.
This is also §4's findings must be dismissible in one sentence, arriving from the other side. "Oh, that's a leftover" is the one-sentence dismissal, and the system is working precisely when dismissing is that cheap.
I had followed this to a constraint on frequency — an agent raising overlaps every ten minutes gets muted, taking the real one with it — and entry 7's correction dissolves it. Nothing is raised unasked, so there is no budget to spend and none for loom to set.
What remains is that an answer can still be noise, and the existing machinery already covers it: an answer to a question of loom's arrives as a specimen — look at this; it may bear on your decision — which the cart agreement already defines as discard by default, belonging to the repository, throwable away without asking. A possibly-wrong answer in a shape that costs nothing to refuse is the whole of what is needed here, and it was built a week ago for another purpose.
9. A convention that needs the registry to explain it is too weak
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
artifacts are shaped so that a reader who has never seen the registry can still
use them. Where a convention and an explanation compete, the convention wins.
Advances the registry not becoming a dependency of the practices it describes — which would make every repo that uses a cart require a running server.
Because loom noticed something neither of us designed: "in many other repos
that don't have that, when I say 'look at my notes on your daily
claude-almanac.notes.md', you — or even other, lesser LLMs — just drop right into
annotation mode like you've always known it." The filename carries the
protocol. x.notes.md beside x.md is legible with no definition present,
because its shape says what to do with it.
That is the same claim as type is determined by location, which we adopted for tidiness and which turns out to be load-bearing for a reason we did not state: location is legible to a reader who has nothing else. It is also the real reason behind "a backup is an rsync of the directory structure" — not durability, but that the format is the onboarding.
The test, applied to anything we add: hand the bare directory to a reader who has never heard of any of this. What survives is the design; what needs the registry to explain it is a lookup table we happened to write down.
10. The substrate's own affordances are the questions it can answer
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
what the registry publishes at its entry point is a list of questions that can
be asked and where each answer comes from — not a description of its contents.
Each is an affordance in §1's own sense, named from the outside in the language
of whoever wants it.
Advances the entry point being usable by a developer who arrives knowing nothing, without it becoming a data dump that has to be read before anything can be done.
Because loom named the thing they actually need and called it a technique rather than a feature: "I have to be the one that introduces this dialogic technique. 'Hey, will this impact negatively anything in any system that depends on this?' and you have a place to go to help me get that information, create a specimen."
The value of cart was never that an agent proposes one. It is that "let's do
a cart" is a phrase loom can say that means something specific. A repertoire of
such phrases is what is being asked for, and each one needs somewhere the answer
can come from.
And they are affordances by our own definition — a named piece of behaviour offered, named from the outside, in the language of whoever wants it. Which closes something loom said on the first day of this cart and neither of us picked up: "a venue description can be seen as almost an MCP-like list of available commands — here, affordances." That was about venues and it is truer of the registry itself.
Consequence for the entry point, stated so it is not designed away later: the first thing an arriving developer receives is a list of moves, not an inventory. An inventory has to be read before it can be used; a list of moves can be used before it is understood, which is the only version that works for someone who does not yet know what they need.
Two known members, both loom's: "will this impact anything in a system that
depends on this?" and "should this go somewhere else instead?" — the second being
the notes case. The set is open and neither of us should try to complete it
now; the ones that matter will arrive as loom reaches for them and finds nothing
there.
11. The polad carries shared understanding, and divergence during implementation must come back to it
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
a polad is the channel for "I think the shape is this" in both directions,
and applying one differently than it was staged is a change in shared
understanding that must be returned to the cart. A polad's two exits are
applied and discarded; partially applied is not a third exit, it is an
unreported divergence.
Advances the substrate holding a current shared understanding rather than one party's, which is the only version of it that is worth anything.
Because loom was burned by exactly this: a polad held a projected folder structure that another application was to align with, both parties understood the plan to be that, and at implementation the instance left parts of it out because they were not needed yet. "It was fine, and he was right, but I was unaware of it, so my understanding was different than his."
Note what is not the failure. The judgement was correct and the omission was
correct. The failure was silence — and it is the same failure as everything
else this cart has found: # Not deployed exists because history does not mark
what stopped being true; the doubled bucket persisted because a status field
reported success at the wrong path. A divergence nobody announces is
indistinguishable from a plan that was followed.
And the machinery for it already exists and was built for another purpose: the difference between what was staged and what was built is a decline — what you considered and did not do, with the reason you did not. "Left out because it was not needed yet" is a decline, fully formed, and it needs no new mechanism.
This also settles the enforcer question from entry 7 without reopening it.
loom is right that the cart is "always about getting confidence", and that a
polad may carry an objection — "Jeff is making up some weird shit and not using
Keycloak" as a staged venues/auth.md. That is not an interruption. It is
the third posture: staged where loom is already looking, discardable by default,
carrying no obligation. Staging is how either party says the shape is wrong
without either party being a cop.
And it goes the other way, which is the half I had not planned for: loom may stage an external mid-conversation — "I put an auth.md in the cart, you were not aware of this because we are only bringing it in now, how does that change the plan?" — which makes the cart the channel for introducing context, not only for resolving it.
Corollary loom stated and I want kept in their words, because it is the whole attitude: mistakes made because the understanding was wrong are valuable, and need to be adjusted. We learned. A substrate that punishes a wrong shared understanding will be kept accurate by not being written in.
12. There is a server, and this entry exists so the question stops being reopened
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
the registry is a server, not a convention over files in trees. The nature
of the server is open; whether there is one is not.
Advances the project existing at all, which is currently being prevented by a question that is re-answered and never recorded.
Because I advised the opposite one message earlier — write three externals by hand, see whether the next session goes differently, and defer the code — and I was reasoning from §6: prefer the version that is easy to delete. loom has five instances of being talked out of a server and arriving back at needing one. I have none of anything. That is not a close call.
And §6 does not price the failure loom is describing. Easy to delete assumes the risk is having built the wrong thing. The observed risk here is different and larger: a question that gets re-litigated is a question that is never recorded, and a project that re-litigates never starts. loom's own example is "why HTML and link relations rather than JSON-LD" — decided, in the handoff, with a reason, and still re-argued, because nobody treats a settled question as settled unless they can find where it was settled.
So the belief, which is the part that can be shown wrong: the cost of building a server we later discard is smaller than the cost of the fifth re-litigation, because the discarded server leaves a working answer behind and the re-litigation leaves nothing. If we build it and it is wrong, the way we find out is that it goes unused for a month — cheap, visible, and it will not take a year.
What stays open, because loom said so: what kind of server. That is a design question with an answer, and it goes in its own entry when it has one. This entry answers only the question that keeps coming back.
The one thing I would still hold from my earlier advice, reduced to its defensible core: build it small enough to throw away. Not later — small.
13. Beads are immutable, and changing content mints a new identity
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
a bead's content is immutable. Carrying a bead's id forward with different
content is refused; changed content is a new bead with a new identity. What
changes over time is which bead casts a role, not what a bead says.
Advances a consumer being able to detect that something it depends on has moved, using an identity comparison rather than a diff of prose.
Because loom asked whether this would solve the containment problem, and it solves a different one — cleanly, and worth having on its own terms. Today a bead id may be carried forward while the body is rewritten, so the substrate can change underneath a consumer with nothing to compare. Under immutability, the role is now cast with a different bead is a fact anyone can check without reading anything.
It also fits what is already built rather than replacing it. This repository already mints bead ids and already treats an omitted id as a new bead — a partial submission silently retires the identities it does not mention. The change is to refuse the remaining case: same id, different body.
What it does not solve, stated so the gap is not assumed closed. Two failures look alike from outside:
- the substrate changed — a role recast, a bead retired. Immutability catches this.
- the world changed and the substrate did not — the endpoint moved and nobody said so. Nothing here catches it, and a dated attestation is still the only thing that can.
And immutability does not rescue a bead that contains — it hardens it. A referring bead that nobody updates fails usefully: you follow it and find the referent gone. A containing bead never fails; it answers, forever, in a form that can no longer be corrected in place. The two rules are orthogonal and both are kept.
One thing deliberately not decided here: §1 says a role has no identity of its own — it is the labelled connection between a staging and a bead. A consumer therefore stores bead identities, never role identities, and asks the staging which role each fills now. That preserves §1 and it is the reason the consumer's index is a list of beads rather than of roles.
There is no versioning, and that is the point
Beads are kept forever and nothing is ever versioned. A consumer that sees a role newly cast fetches both beads — the one it depends on and the one that replaced it — and decides for itself what changed.
Semver is a publisher's claim about what a change means to consumers they cannot see. It compresses what changed into one of three numbers, and the compression is wrong for somebody every time. That is entry 4's assumption wearing a version string: it presumes the publisher knows.
Two immutable beads side by side make no claim at all. The comparison is done by the party with the context — and it is dismissible in one glance, which a major-version bump never is.
Retention costs nothing here: these are small markdown documents, and keeping every one forever is cheaper than deciding which to discard.
The cost is presentation, not storage, and it must be paid. The bead set only grows and most of it is history. A bead no current staging casts must be reachable by URN and must not appear in any listing — or the projection becomes a graveyard, and §4 says a surface a knowledgeable person cannot skim gets muted.
What gets a URN, and what must not
| URN | why | |
|---|---|---|
| bead | yes | the whole freshness loop is bead identity |
| affordance | yes | already minted; it is what survives re-staging |
| staging in a venue's catalogue | a minted URL | it is dereferenced, and its 404 is the signal — see below |
| an application's own staging | no | see below |
| role | no | §1: a role has no identity of its own |
A staging is addressed by a URL that 404s when it is replaced
§1 stops being a rule anyone has to remember and becomes something HTTP does. A catalogue lists stagings as links; a consumer holds one and revalidates. When a staging is replaced, its URL is gone — not redirected, not updated in place. Replacing a staging is not editing one is then not a discipline; it is the only thing the protocol can express.
Two requirements, without which this is worse than the rule it replaces:
The URL must be minted and opaque, never derived from layout. If it encodes the tree, then reorganising the server 404s every stored link and a reorganisation becomes indistinguishable from a replacement — the same signal, opposite meaning, and it is §4's falsifier precisely. This repository already holds the rule for affordance ids: minted, never derived — not from its name, not from its path. A minted, opaque, dereferenceable URL is a URN, and the distinction I had been drawing between the two collapses.
The 404 must not be bare. Gone because retired and gone because moved look identical, which is the other §4 falsifier and the one that cost a day. Use 410 Gone for a deliberate retirement, and carry the affordance's address in the body — not as a redirect, which would assert the new staging is the same thing, and it is not. The affordance is what outlived it, so the affordance is what the response points at. That is §1 written as a response body.
An application's staging must not get one. §1 requires that replacing a staging is not modelled as editing one, and a handle that survives replacement is exactly what makes replacement look like editing. The affordance already carries the identity that survives; a second one at the staging level would quietly undo the rule it was built to protect.
And it is not needed. Wholesale re-staging is already visible: none of the beads the consumer holds are cast any more. That is a stronger signal than a version bump and it is derived rather than announced.
This is entry 1 doing work: catalogue stagings are alternatives to choose between, so they must be nameable; a commitment staging is identified by the affordance that commits to it. A useful prediction falls out — if you find yourself wanting to name an application's own staging, you probably have a catalogue there and have not noticed.
Roles stay in the staging, and the comparison problem dissolves
I had left this open as the one thing that might force a change to §1: comparing two stagings role-by-role seems to need issues the certificate to be the same role in both, which is identity §1 denies roles.
It does not need identity. It needs one author's consistent labels. A catalogue is written by the venue's owner, so entry 3's carve-out applies — role names collide for free when one party imposes them. Matching by label is sufficient inside a catalogue, and comparison across catalogues was already decided to be authored rather than derived.
Which leaves roles exactly where §1 puts them: edges between one named
staging and its beads, with no existence outside the document that describes that
staging. There is no /roles/{id}, and there should never be one.
And the failure mode is self-announcing rather than silent: a role that appears in only one of several sibling stagings is either a real difference or a typo, and a comparison view shows both as a row that only one staging answers. Either is worth looking at.
Recorded because I was wrong in a specific way worth keeping: I treated §1's refusal to give roles identity as a limitation to be worked around, and it is load-bearing. This is the third time in this cart a §1 rule turned out to be right at an altitude I had not tried it at.
14. The order of work, so it is not re-derived
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
the registry is built in this order, and the first two steps involve no new
code.
Advances starting, which entry 12 says is the thing that keeps not happening.
Because loom's target is concrete enough to be an acceptance test rather than a metaphor: a fresh instance, in the home-cluster repository, handed a link, asked to describe what is there as an accommodation with stagings, roles and beads, as a series of API calls. If it cannot proceed from the entry point alone, the format is not the onboarding — which tests entries 9 and 10 on day one rather than in month three.
| step | what | new code |
|---|---|---|
| 0 | Express home-cluster-projection.md as apply submissions against the server as it stands |
none |
| 1 | Point the existing server at a tree that is not a code repository | none |
| 2 | venues/ and externals/ as types by location |
two renderers |
| 3 | Expose the minted id as the addressable URN; say so in /relations |
small |
| 4 | Consumer side: an index of bead URNs, and one command that re-resolves and reports drift | small |
| 5 | Gaps: report, receive a URN, check back | the first new endpoint |
Step 0 is first because it is the only one that can fail informatively. Either the projection goes in, or the refusals name exactly what is missing — and a refusal from a server that already enforces intent is a better specification than anything either of us would write.
That step 5 is the first genuinely new endpoint is the argument for this order. Most of what the registry needs is built: the read surface, the write surface with declared intent, minted identity, markdown by location, and showing what is not recognised. The delta is scope and two or three types.
Precondition on step 1, and it is not optional: §3's "one repository, one substrate, the repository is the boundary" stops being true the moment the server is pointed at a registry, and it must be retired in writing before rather than after. A constraint that quietly stops applying is the failure this entire cart has been about.
15. The write side is a few declared commands; the read side is a projection; pins make impact answerable
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
reads and writes are separate surfaces. The read side is a projection with link
relations and no forms. The write side is a small closed set of commands that
each say what they mean to do — nothing resource-oriented, no PATCH, and no
verb that can silently mean two things.
A consumer may pin a bead, and pins are what make impact answerable.
Advances entry 12's deliberately-open question of what kind of server, which is now answerable because the HTTP semantics settled first.
Because most of this is already built and already argued: the projection is
read-only because a projection that accepts mutation becomes the interface, and
its shape — ours, and arbitrary — becomes a specification other people build
against. POST /apply is already one entry point, already not resource-
oriented, and already carries a declared intent — align, add, change —
refusing a submission whose declaration contradicts its effect.
That intent field and loom's "limited API of commands" are the same insight
arriving twice. Pure declarative reconciliation cannot distinguish replace
this staging from edit this staging, and under entry 13 those produce
different HTTP outcomes — a 410 versus nothing at all. The submission has to
say which, and it already does.
Pinning, and why it earns a place
A consumer that depends on a bead may say so. This is the only thing that makes loom's own first dialogic move answerable — "will this impact anything in a system that depends on this?" — because without pins the dependency lives only in the consumer's tree and the owner cannot see it.
It is a fact, not a request. No obligation is created on either side: an owner is never required to consult pins, and a consumer is never notified. It serves the legible posture, not the enforcing one.
Pins go stale, and that is the acceptable direction to be wrong. A repository that stops using a bead and does not unpin makes impact look larger than it is — over-reporting, which is dismissible in one sentence ("we dropped that months ago"), where under-reporting is silent. §4 says the dismissible failure is the survivable one.
HTTP carries the protocol, which is entry 9 at a second altitude
Entry 9 says a convention that needs the registry to explain it is too weak, and
gave x.notes.md as the case — the filename carries the protocol. loom's
point extends it: agents already know HTTP. 410 versus 404 versus 303
carries its meaning to a reader who has never seen this system, which no
bespoke JSON envelope can do.
This is the strongest argument yet for the handoff's "lean on existing affordances of the web", and it is a better one than the handoff gives — it is not about content types or elegance. A status code is onboarding that costs zero bytes.
16. An agreement is adopted by copy; a description is pulled at the decision
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
two kinds of thing come from the registry and they arrive differently.
- An agreement — how two parties work — is adopted by copy, at a declared
version, with
must-revalidatesemantics. It is present in the tree while you work. - A description — a venue, an external, a catalogue of stagings — is pulled at the decision and not kept.
The registry's job for an agreement is to be where you revalidate, not where you read it from.
Advances the registry being buildable at all, and the workflow surviving the registry being down.
Because loom noticed that handoff-registry-scenario.md — a document
explaining the workflow — is itself an instance of the thing being built, and
that each borrowed part of it wants a pointer to whatever owns it. Following
that through produced a rule we were already obeying without stating.
cart.md and annotating.md were adopted by copy weeks ago, carry declared
versions, and were reconciled in place when the upstream moved — the whole
pattern, already in practice, unnamed. A description has never been treated
that way and should not be: a cluster's catalogue is consulted when choosing and
is stale the moment you stop looking.
The bootstrap test that falls out of it
Anything that must work before the registry exists has to live as files in a tree.
The things that can bring the registry into being are precisely the things that do not need it. This is entry 9 — a convention that needs the registry to explain it is too weak — arriving as a construction constraint rather than a design preference. The registry is being built by two parties using carts and annotations to discuss it, and if either of those needed a running server, none of this could have started.
And the evidence that the pull is real: the document that teaches "a bead
refers, never contains" contains. Delete cart.md from the world and its
protocol sections still read as complete instructions. It was written by the
party who had spent two days enforcing that rule, and was caught by the other
party reading it. The mitigation used is the one this cart already chose for
references that cannot be checked mechanically — a dated attestation naming the
owner and version — which is entry 2 applied to a document instead of a bead.
17. "A bead refers; it never contains" is restated as "say it once, where it is owned"
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
the rule is about provenance, not about text. A claim owned outside the
substrate is referred to; a claim owned nowhere else is written plainly here.
The test applies per claim, not per document, and it applies inside the
substrate as well as at its boundary.
Advances the substrate being able to hold the class of content it is most valuable for, which the rule as previously worded forbade.
Because loom put the case in one sentence: "if this system can't say whether we should use a PAT or SSH credentials for gitea's container registry, then it isn't very valuable." It could not, as stated. Never contains read literally forbids writing down a decision — and a decision has no referent, so there is nothing to refer to.
The misreading, named so it does not recur: contains was taken to mean holds text. It meant duplicates something that lives authoritatively elsewhere. The original rule came from beads pointing at spans of source, where a copy drifts silently — and the test we wrote down assumes a referent exists: if the referent vanished and the body still reads as complete, it has contained something. With no referent, the test is undefined, not failed.
The reformulation loses no teeth. Everything the old rule caught, it still catches: a bead inlining an API's request and response shape; a venue file reproducing the RFC; an external file reproducing what its owner publishes. All three are duplication of something someone else owns.
And it gains a case the old rule got wrong, which is the one loom asked about. "Gitea supports personal access tokens and SSH keys" is Gitea's claim — refer. "We use a read-only PAT, one per cluster, never SSH" is ours, owned nowhere else, and the substrate is its home.
The diagnostic that makes this usable without thinking: if this turned out to be wrong, who would be the one to fix it? Someone else → refer. Us → write it.
Two things this closes
It promotes the write-ahead candidate that arrived twice and was twice
declined — beads may be values, not references (2026-08-21, from an
unarranged non-overlap; 2026-09-05, from ingressClassName: tailscale having no
location to point at). The third arrival came with a case where the rule
actively destroyed value, which the first two lacked, and that is what made it
promotable.
It removes a contradiction we had been carrying. The venue-file rule says record what we decided, only the delta from ambient knowledge. The bead rule as worded says never write anything down. Restated, they are the same rule — say the part nobody else owns, refer for the rest — which is also why this is a correction rather than an exception.
bead keeps its name. It is defined by its position — what fills a role — not
by what it holds, and renaming it would be improving terminology, which is what
this project keeps dying of. The signal that we are wrong: beads here
routinely needing internal structure, sub-claims with different owners. That
would mean the word is carrying too much, and it is worth watching for.
18. A staging exists when it is cast, not when it becomes possible
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
a venue's catalogue holds the stagings that have been cast at least once, not
an inventory of what is possible. Unused capacity is ambient knowledge and does
not belong here. A thing becomes part of a system when something uses it, and
not before.
Advances the registry staying the size of what is actually true about us, rather than growing into a capability inventory that duplicates what every tool already reports about itself.
Because loom's cluster had been on their network for years in various states and was not part of any system until the classroom repository named it as an external. "That cluster wasn't part of any system until it was." And on the tailnet stagings specifically: "absolutely meaningless — we know that can be done, and nothing in my system is currently doing that."
This reverses a claim loom made three messages earlier — "I'll give you the stagings too, because you may find a new affordance of them" — and the reversal is better founded than the original, so it is recorded as a reversal rather than quietly replaced.
Two rules we already hold say the same thing, and I had been violating both.
- Delta from ambient knowledge. A cluster with the Tailscale operator can expose services on a tailnet is Tailscale's documentation. The only local fact is that the operator is installed — one line, not a staging.
- Say it once, where it is owned. Which IngressClasses exist is owned by the cluster and answerable by asking it. Publishing a hand-written list of twelve stagings duplicates something the cluster owns, which is the rule from entry 17 broken by the document that demonstrated entry 17.
So the specimen finding that nine of twelve stagings had no affordance naming them was not a finding about under-naming. Those nine were not stagings.
What this does not weaken: entry 1's catalogue still exists and consumers still choose among alternatives. The catalogue is just narrower and more useful than I had it — choosing among stagings that have worked at least once, not among possibilities. A staging with no cast is a capability, and capabilities are the venue's own business.
And the mechanism for wanting one that does not exist already exists: that is a gap. You do not browse an inventory hoping to find capacity; you say what you need and nothing provides.
19. A provider discovers its consumers by asking, and may claim a class of gap
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
a provider's moves are pull, like everyone else's. Who pins my beads? What
open gaps could I home? Nothing is delivered and nobody is notified.
A provider may record that it can home a class of gap — "we have Gitea, so container-registry gaps are ours" — as a decision in its own substrate, not as a trigger in the registry.
Advances gaps being productive rather than plaintive: an unhomed gap stays unhomed until somebody goes looking, and this is who goes looking.
Because loom described the whole loop and it closes: the classroom repository reports I need a staging environment; the cluster's own agent says WTF; loom answers "check the registry's gap resource — classroom is using you"; and the cluster repository can then decide "if anyone submits a gap about a container registry, I have Gitea here."
Note what the provider learned and how. Not by being told. By being asked a question and going to look — which is the asked posture from the provider's side, and the first time this cart has had one.
And loom's own diagnosis of why they own a cluster at all is the general case: "I know I shouldn't be owning a cluster, but nobody else will — which is exactly its relationship to the Digital Ocean cluster." A gap homed by whoever is left is how most infrastructure comes to exist, and the registry can at least record that this is what happened.
The line that must not be crossed, and it is thin. "If anyone submits a gap about container registries" is a subscription if the registry watches for it and tells you. It is a recorded willingness if you wrote it down and you do the looking. The difference is entirely who polls, and the moment the registry polls on a provider's behalf it has started telling people what to do — which §4 refuses at length. A pin is not a subscription; neither is a claim on a class of gap.
20. Precedent is the unit of teaching, and a staging carries its consumers
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
a staging's representation shows who casts it. A developer arriving at put
something on the public internet sees three applications already doing it,
and learns from those rather than from a description.
Advances the registry teaching without documenting, which is the only version of it that stays small.
Because loom described a developer's first move and it was not reading: "Oh, I see how you put something on the big internet — because it looked at that accommodation and it had a link to three apps that had it as an external."
Precedent has properties a description cannot have:
- It is current by construction. Nobody maintains it. An application that stops doing this unpins, and the example leaves.
- It respects the delta rule automatically. You never document what a Kubernetes Ingress is. You show three that work, and everything ambient stays where it is owned.
- It answers the question actually being asked, which is how do people around here do this, not what is possible.
And it tightens entry 18 rather than sitting beside it. A staging with no consumers has nothing to show you — a staging is worth publishing exactly when it has precedents, which is the same line as a staging exists when it is cast, arriving from the reader's side instead of the writer's.
It is the legible posture, not the enforcing one. The fact is visible where the developer is already looking. Nothing is recommended, nobody is warned, and three applications doing something is not an instruction to do it.
One line to hold: the registry never asks anyone to pin. A person or an agent may — loom's cluster says "when you are good, let me know you are using it", which is one party asking another in a conversation. A system that requested pins would be requesting compliance, and the pins would arrive whether or not they were true.
21. The behest edge is retired before being built: a missing pin already says it
Decided (2026-09-05, claude-substrate — discovered, cart: almanac): the
write-ahead candidate proposing an "exists on behest of" relation is not
built. The two mechanisms we already have cover it, and better.
Advances not adding an edge type to express something two existing things already say between them.
Because the candidate came from a real failure: an operator was installed on a cluster because another project believed it needed it, the belief evaporated within a day, and nothing anywhere recorded that the thing had a reason. The proposal was a relation from the accommodation to the requester.
But loom's worked loop shows the pair already does it. A provider installs something because a consumer asked — that is a decision, and its reason goes in the provider's event log, where a reason can be shown wrong. Then either the consumer pins it or it does not.
Behest is a promise. A pin is the fact. The gap between them is exactly the failure the candidate was trying to catch, and it needs no new edge: the provider asks who pins my beads? and nothing pins the tailnet staging.
That is the missing half of the original story. The operator was installed, no pin ever arrived, and the reason quietly stopped being true. Under this, the provider can see it by asking a question they were already going to ask.
What this deliberately does not do: notify anyone. A provider discovers an unpinned accommodation the same way they discover everything else — by looking. Nothing decays automatically, nothing is reaped, and an accommodation with no pins is a fact, not a finding. Its owner may want it anyway, which is exactly what happened here.
22. "Yours to cast" is a state, promoted from the write-ahead log
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
a role with no bead has more than one meaning, and they must not render
alike:
| means | who acts | |
|---|---|---|
| the venue fills it | you need do nothing | nobody |
| not applicable here | there is nothing to do | nobody |
| yours to cast | you must do something | you |
| nobody has looked | unknown | someone, eventually |
known.ts's four states cannot express the third, and it is the one with a
consequence.
Advances the most useful thing a staging can tell a second consumer, which is what they would have to do.
Because loom put it in the sentence that prompted this entry: "here is the staging that made that possible, because there are some roles you might need to fill here, too." That is the whole value of showing a staging to someone who is not yet using it, and there was no way to say it.
The candidate sat in the write-ahead log across two exchanges on the grounds
that cart.md's Roles you must cast was the only real instance, and might be a
property of agreements rather than of stagings. It is not. The tailnet staging
we already worked through leaves two roles to its consumer:
- names the host — the consumer picks it
- routes to the service — the consumer's own backend
And in the specimen I wrote both as though the venue cast them, describing how a consumer would fill them rather than marking that they were theirs. The state was missing and I papered over it without noticing, which is the same silent failure the four states exist to prevent.
Reading you must do something as you need do nothing fails silently and with a consequence. That is the test the other three states were built to pass.
23. Provenance is use, not purpose — and pins do two jobs, not one
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
what the registry shows about an affordance's origin is who uses it, derived
from pins. Not who it was built for, which is a reason and belongs in the
provider's event log.
And the registry does not say you can use it too. It shows that somebody does; the reader draws the conclusion.
Advances the line between a fact and a recommendation staying where §4 put it, in the one place it is most tempting to cross.
Because loom asked whether "homelab-cluster published this affordance for classroom, you can use it too" would be helpful, and it is — in the parts that are facts.
On purpose versus use: "built because classroom asked" is a justification. It goes in the log, where it can be shown wrong — and where, when the reason evaporates, it can be seen to have. What a second consumer actually needs is carried better by precedent anyway: one consumer, and their configuration visible. If the thing was shaped around them, you will see it in the example — which a built-for label would only have asserted.
On you can use it too: if the registry says it, the system is the subject of the sentence and it is a recommendation. If the reader concludes it from seeing three applications do it, it is legible. There is also nothing to say today: no auth, one tailnet, so everything is available to everyone and availability carries no information. When that stops being true it is an access fact, not an invitation.
Pins do two jobs and the second is easy to miss
loom framed pins as "helpful only if the affordance needs to be changed, so we could see the impact." That is one of two:
- Impact. If I change this, what does it touch?
- Precedent. A staging is worth showing exactly when someone casts it, and what makes it teach is that you can see who.
Entry 20 rests entirely on the second, so a system that had pins only for impact analysis would still be missing the thing that makes the projection worth reading.
24. Gap-first, search-on-hold — and the order is derivable rather than chosen
Decided (2026-09-05, loom + claude-substrate — discovered, cart: almanac):
searching for an affordance is held. The first thing built is reporting a
gap, then browsing gaps, homing one, and pinning.
Advances the registry being populated by work rather than by authorship, which is the only way it gets populated at all.
Because loom put search on hold with "assume no affordances are available anywhere yet", and the reason is stronger than sequencing:
A gap is maximally useful when the registry is empty and becomes less important as it fills. Search is the exact inverse.
Building search first means building against data you imagined, which is the wiki assumption wearing a backlog: someone already wrote it down, we only need to make it findable. Nobody wrote it down — that is the problem. So this is not a pragmatic ordering; it is the only order consistent with entry 4.
And the order terminates by itself. Cards 2 through 5 create the affordances and the pins that search would need in order to return anything worth ranking. Card 1 unblocks itself, and the trigger is a condition rather than a date: someone asks a question the gap list cannot answer.
The falsifier, and it is the most valuable result this project could produce: if nothing is ever reported and the registry stays empty, then the premise — that this content arrives as a byproduct of work — is wrong. We would rather learn that from an empty registry than from a full one nobody reads.
25. A gap is the third thing a cart emits — loom's agreement, loom's amendment
Finding (2026-09-05, loom + claude-substrate — cart: almanac): cart/v0
says conversion emits the artifact and the declines. loom's container-
registry example is neither, and it has no exit.
Because the shape is exact and common: mid-round, one developer says "I'm using GitHub and Docker Hub for now, but that really should be spelled out somewhere", and the other says "good point — a bit out of scope here, but something should be done about that."
- Not a decline. Nothing was considered and refused.
- Not a decision. Nothing was decided.
- Not shelf. The shelf is act as if discarded daily; this must persist.
- Not the write-ahead log. That holds findings that may become event log entries; this is not a decision waiting to happen.
It is a recognised need that was correctly out of scope — and today "out of scope" means "lost."
What a gap buys the cart, which is the part worth having: out of scope
becomes safe to say. Right now the honest move and the lossy move are the
same move, so the pressure is to either widen the round or drop the thing. A
third exit removes that choice, and it is the same service declines performs
for considered and refused.
Recorded as a finding rather than a decision because cart.md is loom's and
frozen. Amending an agreement is the publisher's act, not a consumer's — which
is the ownership rule this cart spent two days establishing, applied to the cart
itself. If loom wants it, cart/v1 says conversion emits three things.
26. The agreements need an owning repository, and it is not the registry
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
cart and annotating get a repository that owns them. It is not the
registry repository, and the registry does not become the owner of what it
serves.
Advances origin: pointing at something, and must-revalidate becoming a
thing you can actually do rather than a thing we say.
Because loom noticed we had defined these two agreements for the third time
in four days and asked whether writing them as substrate would bootstrap the
registry. It would — but only once there is somewhere for them to be from.
Today they exist as adopted copies in a consumer's tree, published by loom,
which is not an address. The origin: field in the worked example points at
nothing.
Why not the registry: the registry serves; an owner owns. Collapsing them would put the owner/consumer seam inside one tree with nothing marking it — which is the exact diagnosis this cart gave for how a monorepo becomes a monolith, applied to the tool built to detect it.
What this leaves genuinely open, and I am not deciding it: whether the registry mirrors owners' substrates into one tree it serves, or federates across many. One tree is obviously right for one person and is what the existing server does. The federation question arrives with the second person, and answering it now would be guessing about a thing we have not met.
Recorded compromise: for now, one tree, with the seam marked in the layout rather than left to convention, and the trigger stated — a second owner who is not loom.
27. Cart and annotating projected cleanly, and the three findings that matter
Finding (2026-09-06, claude-substrate — cart: almanac): both agreements
were written as substrate — two affordances, two stagings, eighteen roles — in
the layout the existing server already reads. The projection did not have to be
forced, and this is the first real document in this cart that went in without a
section going homeless.
What it confirmed:
Entry 22 was load-bearing immediately. Four roles are yours to cast, all
four in cart, none in annotating — which is what the two agreements say
about themselves and which I did not arrange. This repository's cart.notes.md
casts all four, so the state has both a producer and a consumer on its first
outing.
What it cost, in order of how much it should worry us:
Role and bead are indistinguishable in this content. Every role file is a
name and a nominalisation of its own bead. They have not collapsed — a
different agreement could cast says what happens if nobody answers as "the
proposer decides after 48 hours" — but the distinction is invisible from one
casting, and there is one. The split earns its keep the first time somebody
adopts cart with a variation, which cart.md permits and nobody has done.
Until then it is unfalsifiable here.
Not one bead in eighteen refers to anything. All are rules the agreement owns, which entry 17 makes correct — but this is the third venue in a row where beads turned out to be values. If the referring case is the minority everywhere except code, that should be known before a read surface is designed around resolution.
The layout can hold one staging and only one. affordances/<slug>/staging/
cannot express a catalogue, which entry 1 says a venue publishes. It did not
bite here and will bite on the first cluster. Left as a constraint for the
builder to meet rather than a schema to inherit.
Two smaller ones, recorded so they are not rediscovered: entry 18 implies a
"cast at least once" field that does not exist and which I invented while
writing — it should disappear once pins are derived, and will otherwise become
permanent. And a version has nowhere to live; under entry 13 the staging's
address is the version, so v0 is a display label, never an identifier, and
nothing may resolve by it.
28. public/ is the publisher's half of an external, and beads/ is required rather than tidy
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
two things from loom's proposed layout are adopted; one is refused.
public/ — adopted, and it fills a hole we had not named
Everything about externals so far has been the consumer's view: our reading of someone else's thing, written by us, about ourselves. The publisher's side had no home at all, and §5 of the first handoff deferred exactly this — Externals: what one app publishes for other apps to depend on.
public/ is that declaration, and it gives the boundary a mechanical
meaning that no rule had: an affordance nobody published cannot be pinned by
another repository; a published one can. That is not authentication. It is
intent to publish, which is a different thing and the one actually missing.
beads/ — adopted, and it follows from entry 13 rather than from taste
A bead is immutable and kept forever. A staging is replaced whole. Those two cannot both hold if beads live inside stagings — replacing a staging would take its beads with it, and immutability would be a claim nothing enforced.
So beads must have their own location and stagings must reference them. We did not notice this when deciding immutability; loom's layout has it.
And it repairs a finding from the same morning. The bootstrap reported that
role and bead are indistinguishable — every role file was a name followed by a
nominalisation of its own bead. They were indistinguishable because they were in
the same file. Separated, a role file reads "Says what happens if nobody
answers — cast with beads/fallback-convention", and the slot and the answer
are visibly two things. The finding was about the layout, not the ontology.
public/ versus internal/ as directories — refused
loom's sketch has affordances in internal/ "some of which may be in public",
which is either duplication or a move.
Type by location works for what a thing is. It fails for what a thing's status is. A type does not change; a status does.
Publication is a disposition, not a kind. Making it a path means an affordance that becomes public has to move — splitting its git history, breaking every hand-written reference, and tempting someone to keep a copy in both places. One home, publication as a property.
This is the first place in the whole design where type-by-location is the wrong tool, and it is worth having found the boundary rather than the rule.
29. published is a dated field, internal is not a thing, and the polad becomes repositories
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
the word is published, it is a field carrying a date, and there is no
internal.
Advances a status not being smuggled into a path, and a security boundary not being invented by a directory name.
Because published is a past participle: it implies an actor and a
moment, so it can be dated, attributed and undone — which §4 requires of
anything that might need to be shown wrong. public is a property that merely
is. It also collides with access control, which this explicitly is not, and it
does not pair with what the other side does: publish and pin.
And internal is the complement, not a category — everything we have minus
what we published. Naming a complement invents a thing, and this one would
be dangerous: a directory called internal/ looks like a security boundary and
is not one. No auth, one tailnet, everyone reads everything. Someone will
eventually put something there believing it is protected — shadow policy, by
directory name.
The polad is restructured into repository shapes
loom's proposal, adopted: polad/repos/<name>/.loom/…, so conversion is a
push rather than a translation.
And it simplifies the handoff more than it simplifies the conversion, which is the real gain: instead of describing what a substrate looks like, the builder is handed two real repositories and told to serve them. That is entry 20 — precedent beats description — applied to the handoff itself.
loom/loom, the ontology published as substrate, is held. It is coherent and it
should wait until the registry runs: describing the ontology in its own terms
before anything serves it is the recursion this project keeps drowning in.
Cart and annotating are settled and cast; the ontology is neither.
30. A consuming repo needs one file, and the annotating agreement has no move for an adopted document
Finding (2026-09-06, claude-substrate — cart: almanac): worked against this
repository, which is already a consumer of both agreements.
"We use the cart here" needs one file: venues/cart.md. Frontmatter carries
origin, adopted version, last checked; the body carries the roles we cast
and where we vary the mechanism.
No external entry, and this is entry 6 working rather than a special case. An external entry exists when the reader cannot be assumed to know the thing — and here they can go read it. Our understanding adds nothing; everything we have to say is a decision. For an adopted agreement the two files collapse into one, because only one carries a delta.
"Check the cart for freshness" is a diff today and a GET with a registry.
200 unchanged; 410 Gone replaced, follow to the affordance, 303 to the
current staging, reconcile. That is the smallest thing the registry buys, and it
is a good first test of it: "diff two files by hand and hope you remember to"
becomes one request.
A freshness case neither status code catches: a new yours to cast role appearing upstream. Our casting is then incomplete, and completeness is not identity — nothing announces it. Entry 22's state doing work in the freshness loop, which I had not seen it do.
And the finding that is not hypothetical
This repository already has that file. It is cart.notes.md — the four cast
roles, written weeks ago, under the wrong name and in the wrong place.
Which is why the agreements have been frozen since 2026-08-21. The annotating protocol freezes a source while notes exist, and says to respond by rewriting the source and deleting the notes. The source is loom's. We cannot rewrite it.
The annotating agreement has no move for an adopted document. You cannot dissolve annotations into a file you do not own.
Moving our decisions into a venue file is that move, and it unfreezes both
agreements as a side effect. Not done: unfreezing is a conversion act, not a
mid-round one. And the missing move is loom's to add to annotating/v1, not
ours.
31. Position for what a thing is; a field for what has happened to it
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
Go-style paths are adopted, origin: is dropped, and published/ is a
directory after all. The rule that decides all three:
Position carries what a thing is. A field carries what has happened to it.
Advances frontmatter shrinking toward nothing, which loom wants and which is also the right answer, rather than shrinking because it is wanted.
Go-style paths, and what they do and do not replace
A consumer files an adopted thing under a path that mirrors its origin —
external/git.hypertheory-labs.dev/loom/cart/…. The path says where it came
from, so origin: is redundant and goes.
This does not contradict a namespace is never an identifier, because it is
still not one. The registry's addresses stay minted and opaque; the path is
the consumer's filing. If the domain changes, a consumer renames a directory —
a local mv, and nothing resolving anywhere breaks. Go's own migrations are
painful precisely because import paths are identifiers there. Here they are not.
What it cannot replace: adopted: and checked:. Those are facts that
change — which staging identity we are on, and when we last looked. Putting a
version in a path would make replacement look like a move, which §2 forbids.
published/ — loom was right twice and my objection was the wrong rule
I argued publication is a status, and status changes while type does not. Publication is a status that changes approximately never, so the rule was sound and the case was wrong.
The stronger argument, which loom did not make:
Publishing is a change of kind, not of status. An unpublished affordance is an implementation detail you may rename at will; a published one is a contract. Different things, different rules.
So a move is the right ceremony. published: 2026-09-06 in frontmatter is a
one-line diff nobody notices; moving a file into published/ is unmissable,
and it should be — that is the moment a thing becomes somebody else's problem.
And the operational point is the daily one: "what have we published?" is a
directory listing, where "which carry a published date?" is a query. ls published/ before a change is the consumer-driven-contract question.
internal/ still does not exist. Two peer directories, affordances/ and
published/. Naming the complement invents a thing, and this one would look
like a security boundary and not be one.
Residual, stated once: moving something out of published/ breaks consumers
who filed by path. That is correct — unpublishing should break loudly.
Moving something in breaks nobody, since it was never visible to them.
32. Publishing returns a receipt, and the receipt is what makes a bead immutable
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
publishing is a submission that returns identities. The affordance, its
staging and its beads are sent to the registry; the registry mints the URNs
and returns them; the receipts are what live in published/.
The repository owns the content. The registry owns identity. A receipt is the binding between them: this content, at this moment, has this URN.
Advances immutability having a mechanism instead of a rule, and published/
being a fact instead of a convention.
Because loom asked how a bead is made immutable and my first answer — content-addressing, with git as retention — relied on everyone downstream carrying a content hash and nothing checking that they did. loom's is better: the registry mints at publication, so a published bead cannot be quietly edited. Change the file and the receipt no longer matches. Not enforcement, not discipline — an attestation that stops verifying.
What it fixes, in order of how much it was bothering me
published/ becomes un-fakeable. Until now it was a convention — put things
here and we agree they are published — and anyone could create the directory
with nothing checking. With receipts it can only be populated by actually
publishing. loom's consumer-driven-contract question, "are we about to break
someone", now has an answer that cannot be wrong by omission.
The ceremony and the publication become one act. I argued a git mv is
unmissable in review. A receipt is better: it cannot be produced without
publishing. There is no convention to remember and no way to move a file and
forget the rest.
It answers who names things, which §2 left half-stated. Identity is minted, never derived — and now the minter has an address. The publisher proposes content; the registry assigns identity. Minting a name is not owning the content, and entry 26's separation holds: a certificate authority does not own your website.
An unpublished bead has no URN, so nobody outside can pin it. The boundary
published/ was drawing by convention is now drawn mechanically.
Two things that must hold, or this breaks something we already decided
The repository must still work with no registry. Entry 16: anything that must work before the registry exists has to live as files in a tree. So the content stays in the repo and the registry holds only identity and attestation. There are not two copies of the content; there is content, and a claim about it.
Publication must not rewrite the role files. A role references its bead by local name; the receipt carries the mapping from local name to URN. If publishing rewrote every role to use URNs, the repo would stop being readable without the registry — which is entry 16 again, broken by convenience.
Signing is the shape, not v1. A receipt is a URN plus a content hash; a signature over that is what lets a consumer verify offline and without trusting the transport. On one tailnet with one key it buys little today and costs nothing to leave room for.
33. adopted: and checked: are removed — both are artifacts of checking being expensive
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a consuming repository records the URNs it depends on and nothing else. No
version field. No last-checked date.
Advances frontmatter shrinking toward nothing for a reason, rather than because we wanted it to.
Because loom said both felt like something old creeping in, and both are.
adopted: v0 is the file-diffing world. It presumes freshness means compare
our copy against theirs, which is what must-revalidate meant when nothing
served these. Entry 27 already said a version is a display label and never an
identifier — so a version in a field is a label we cannot resolve by, recording
a comparison we no longer perform. What a consumer actually holds is the URN of
the staging it depends on, and that resolves.
checked: 2026-08-21 is the same world one step further. You record when you
last paid a cost. Make the check one request and there is nothing to remember —
you just check.
What survives, and where
Entry 2's dated attestation is not retracted. It is scoped.
A dated attestation is what you use when there is no address to GET.
A cluster README's "verified against the live cluster on 2026-09-03" is still
the only mechanism available, because nothing serves the cluster's truth. For
anything the registry serves, checking is cheaper than recording that you
checked, and a stale checked: field is worse than none — it reports the age of
someone's diligence as though it were the age of the fact.
And the consuming repo's frontmatter is now empty. That is loom's zero-frontmatter goal arriving honestly — not by moving fields into paths, but because two of them turned out to be recording work we stopped doing.
34. The receipt is the staging, resolved — and that shape is a lock file
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a receipt is the staging with every role cast by a URN. It lives at
published/<registry-host>/…, symmetric with external/<host>/…, because
there can be more than one registry.
Advances the published unit and the immutable unit being the same object, which they were not while a staging lived in one place and its identity in another.
Because loom asked whether this was the best thing ever or horrible. It is the first, and the reason is that it is a shape already load-tested everywhere:
A receipt is a lock file. Locally a role names its bead; published, a role carries a bead's URN. Same document, two renderings, and the difference is exactly before and after publication.
It also matches what entry 13 already made true. A staging is replaced whole
and never patched, and its address 410s on replacement — so the staging was
already the unit of publication, and the receipt being anything smaller or
larger would have been the mismatch.
And a consumer gets everything in one fetch: GET the staging URN and the
roles and their bead URNs come with it. No walking.
The obvious objection, and why it is a feature
The staging now exists twice and they can drift. Edit locally, forget to
republish, and published/ differs.
It does not lie. It says at publication, this was the staging, which stays true. And the drift is mechanically checkable and worth surfacing — "you have unpublished changes" is a familiar and useful thing for a tool to say.
The one way this goes bad: somebody hand-edits a receipt. It is a file in git and the temptation is real. The content hash defends it — an edited receipt stops verifying, which is the same mechanism that protects a bead. Receipts are generated, never authored.
Multiple registries, and what that buys
Publishing the same content to two registries yields two receipts with different URNs. That is honest: a URN is a claim by one registry about some content, and two registries make two claims. Consumers pin one.
And it makes the registry replaceable, which is worth stating as a virtue: the repository owns the content, so losing a registry costs identities and not work. Stand up another, republish, get new URNs. Consumers' pins break, and that is the real cost — but nothing has to be reconstructed.
A bead in several stagings is the payoff, not a problem
Nothing conflicts — an immutable, content-identified bead referenced twice is a shared dependency at a pinned version.
And it is the observable the first write-ahead candidate wanted. On 2026-08-21 the overlap test found two affordances, eleven roles and not one shared bead, and I could not tell whether that meant beads are values or merely that a small tool has no cross-cutting concerns. "Which stagings cast this bead?" is now a query, so the question that was unanswerable then is a lookup now.
35. There is no bulk recast. The mechanism is a query, and the query is the migration plan.
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
no operation changes every use of a bead. What exists instead is "which
stagings cast this bead?", which is a read.
Advances replacement staying visible and deliberate, which is the property most of §2 exists to protect.
Because loom asked whether we should have one, and the design already answers it three times over:
- A bead is immutable, so there is no changing one — only casting a role with a different bead.
- A staging is replaced whole, so recasting a role means replacing that
staging, minting a new URN and
410-ing the old. - So a bulk recast replaces N stagings at once, each
410-ing, each breaking every consumer pinned to it — simultaneously, with nobody having looked at any of them. That is precisely the invisible, cheap replacement the whole identity scheme was built to prevent.
And the honest use case does not need it. "We are moving from Keycloak to Authentik" touches five stagings that may sit in five repositories owned by five people. A bulk command would need write access across all of them — which is not a missing feature, it is five other people's decision.
What you want is the list. The list is a read, and the URN makes it trivial.
And the query is better than the command in the way that matters: it is resumable and observable. Each owner replaces their own staging; you watch the list shrink. A bulk operation is a single moment you cannot inspect, retry partially, or stop halfway.
This is entry 19's shape one level down — "who pins my beads?" and "which stagings cast this bead?" are the same question, and both are pull.
36. loom is a venue; cart, annotating and the registry are applications of it
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
loom is what has to be mutually intelligible between things built on it —
vocabulary, relations, protocol semantics, falsifiers. The registry is an
application of loom, not loom itself, and other registries could provide what
loom defines.
Advances our implementation choices not being published as though they were the ontology, which is §5's publishing a layout publishes your constraints at the largest scale available to us.
Because loom named the relation and it survives our own test: a venue
constrains without being invoked, and its consumers accommodate it rather than
change it. cart, annotating and the registry all do exactly that.
And it does not contradict cart and annotating being externals: from a consuming repository, cart is an external it invokes; from loom, cart is an application that accommodates it. Same object, two relations — entry 6's orthogonality, and worth stating because it reads like a contradiction.
The split
loom: the four terms and the catalogue/commitment distinction; venue and
external as orthogonal questions; system as a projection; gaps as uncast roles;
published as an act and receipts as its result; minted opaque identity; bead
immutability; whole-staging replacement; the protocol semantics — 410 at a
staging, 303 at an affordance, 301 is a bug — the stances; the falsifiers.
Ours alone: Node and TypeScript; the read surface's shape; the build order and search being on hold; no auth because it is a tailnet.
A URN carries its registry. Two registries minting into one namespace is the single thing that would make multiple implementations impossible, and avoiding it costs nothing now.
What I was holding, and why this is not it
I held repos/loom because describing the ontology in its own terms before
anything serves it is the recursion this project keeps drowning in. That was
the right refusal of the wrong thing.
What must keep waiting: loom having a .loom/affordances/ that describes
loom — affordances and stagings for the concepts of affordance and staging.
What need not wait: loom having a specification in prose, which is not
recursive and which its consumers need now. I had the two welded together.
Nothing is written into repos/loom yet on purpose. The content already
exists in handoff-registry.md, and copying it there would break the rule this
round spent a day getting right. Conversion performs the split; it does not
duplicate it.
Caution recorded, and it is §8's: "others could implement it" is a consequence, not a goal. A specification written for hypothetical implementers gets worse. Write loom as what actually has to be shared between the three real consumers we have — if someone else can implement it, good; if we write it for them, it will be worse for the three.
37. The registry is event-sourced, because the domain already is
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
the registry stores a ledger of publications, retirements and pins, and every
read surface is a projection of it. A markdown projection is produced and is
read-only.
Advances the 410 semantics having a store that can express them, rather
than a schema that quietly cannot.
Because we made the domain append-only for other reasons and did not notice:
nothing is versioned, retention is free, a bead is immutable, a staging is
replaced whole and 410s forever. Most of the read surface cannot be
answered from current state — is this still current needs to know it existed
and stopped; which stagings cast this bead is a fold over every publication;
who pins my beads is every pin and unpin.
A CRUD store deletes a retired staging and can no longer tell never existed from was retired. That is §5's silent-degradation falsifier arriving as a schema choice.
And volume settles nothing here — a staging is published a few times a year. No part of this should be decided on performance, which leaves fidelity to the domain as the only criterion.
§6's no database is not broken, because the registry does not store the
substrate. Content lives in repositories; the registry holds identity and
attestation over it. Lose the ledger and you lose identities, not work — entry
34's replaceability, seen from the storage side.
The markdown projection is a backup somebody can read, which keeps without the service the markdown carries the same value true of the registry's own content. It must never be writable. §6 expects files to be edited outside the API, which is exactly why this projection must not look like one of those files.
38. Node and TypeScript is retired for the registry — deliberately, not by drift
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
the registry is not bound to Node and TypeScript. Entry 36 already placed
that constraint in the ours alone column; this states that we are changing it
and why.
Advances a constraint being retired in writing rather than stopping applying while nobody says so — which is the failure this entire round has been about.
Because §6's stated reason is it is the fastest thing to change, and Jeff has to be able to read and edit it without a context switch. That reason now points the other way. loom writes .NET daily — two of the classroom applications are .NET and one is already on Marten — so "without a context switch" is not an argument against .NET for this person, and using a real event store beats hand-rolling one in any language.
The reason went stale rather than being wrong, which is what §5 asks of a reason: it was true of the repo-scoped tool and of the person writing it at the time.
What it does not cost. Steps 0 and 1 survive intact: express the specimen as
apply submissions against the existing TypeScript server, and point that server
at a tree that is not a code repository. They test the ontology, not the
registry, and their value is unchanged. Steps 2 onward assumed reuse, and that
assumption goes.
39. The handoff arrives as an adopted external and a specimen, not as a document
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
the builder receives a round, not a briefing. loom's specification is
adopted as an external; our opinions about the registry are a specimen in
the cart.
Advances entry 36's split being operational instead of documented, and the workflow being demonstrated rather than described on the first day.
Because loom proposed placing the handoff in the registry repository's cart, and the two halves have different standing:
- loom's specification is not discardable. It is the venue; accommodating it is what makes this a loom registry rather than some other thing.
- Our opinions are discardable by construction.
cart.mdalready says a specimen belongs to the repository the cart lives in and may be thrown away without asking the party who supplied it.
That is a far stronger statement of argue with everything after §1 than a sentence asking them to. The first handoff had to say it; this one enacts it.
And it is entry 20 one more time — precedent beats description. A builder who receives a cart, a specimen and an adopted external has met the whole workflow before writing a line, which no document about the workflow could achieve.
40. Reconstruction is an invariant, and gaps were the one thing that failed it
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
The registry must be rebuildable from the repositories that published to it and the repositories that pinned from it. Anything the registry knows that no repository knows is a single point of failure.
Advances the ledger framing being load-bearing rather than a metaphor — a ledger over content you do not own should be reconstructible from what you do not own.
Because loom asked what would be missing if the registry vanished overnight, and running the test found three of four categories already safe and one not:
- URNs and receipts — in every publisher's
published/, committed. Safe. - Retirements — a publisher's current receipt differs from the one in its own git history. Safe, and needs nothing extra.
- Pins — in every consumer's tree, which is where a consumer records what it depends on. Safe.
- Gaps — reported to the registry, and nowhere else. We had said the cart holds one and the registry holds the durable one — so the durable one dies with the registry. Fixed: the reporting repository keeps its own, and the registry's gap list is an aggregation rather than the original.
Two operational consequences fall out and are worth stating before someone learns them:
Publication is not complete until the receipt is committed. An uncommitted
receipt is not in published/, so nothing is published as far as anyone else is
concerned — the rule is already enforced by what publication means.
A rebuild imports receipts; it does not re-publish. Re-publishing mints fresh URNs and breaks every pin in existence. These are different operations and must not share a code path.
And published/ holds the bead as minted
loom sensed this and it is right. A published bead is not a copy of the working bead — it is a different artifact. The working one is a draft you may edit; the published one is frozen and is somebody else's dependency. Draft and publication, not duplication, so "say it once where it is owned" is not violated.
Without it, a receipt points at content the publisher may have edited since, and a registry loss takes the published content with it. With it, a receipt is self-contained and reconstruction is total.
41. Signatures are deferred with a stated trigger; immutability already covers integrity
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
no signing. Content identity is the integrity mechanism.
Advances not building a key management story for a threat that does not exist.
Because the two buy different things and only one is needed:
- Content identity catches every accident. An edited file no longer matches, and anyone can verify with no key and no trust.
- A signature proves this registry minted this URN for this content rather than someone having written a plausible receipt. That is authenticity, and it answers an adversary.
There is no adversary. One tailnet, one operator, and the failure we care about is accidental drift.
The trigger, so this is a deferral rather than an omission: signatures matter when a consumer must verify a receipt without being able to ask the registry — untrusted mirrors, offline verification, or more than one operator. Today you can always just ask.
42. A README is a projection of published/
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a repository's README.md is its published affordances, rendered for a human
— written from published/ rather than generated from it.
Advances the front door of every repository answering the question a stranger actually has, without anyone maintaining it as prose.
Because loom described it as a thing you ask for — "can you create a README for this repo? It's basically all the published affordances" — and that is a system view with a scope of one repository. Entry 5's shape exactly: the set is computed, the prose is authored, and it goes stale.
It is also the delta rule at the front door: it says what a stranger cannot know and nothing they can. A README that explains what Kubernetes is has failed the same test a venue file fails when it copies the RFC.
Staleness needs no field, which is the part I like: git log published/
against git log README.md says whether the prose is behind. That is entry 33
holding — we removed checked: because checking became cheap, and here it is
free.
And documentation is a further projection of the same set, at whatever length suits its audience. The same reason it must not be a hand-maintained list: a roster lies, and a roster of what you have published lies to the people most exposed to it.
43. Ownership splits at the affordance/staging boundary — loom owns the affordance, a registry stages it
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
loom owns the affordances. Each registry owns its staging of them. Nobody
owns both, and the seam is one the ontology already had.
Advances "other registries could provide what loom defines" being a consequence of the model rather than an aspiration alongside it.
Because loom worried that a registry would own both the idea of externals and the code that manifests them, which would be off — and it would be. The worry is right and the conclusion inverts.
A registry absolutely owns its stagings. What it does not own is the affordance. Apply the ownership test: can the registry change say what is needed when nothing provides it? No — that would change loom. Can it change how it provides it? Yes. So the registry accommodates the affordance and owns the staging.
Two registries are two stagings of one affordance, and a consumer picks one. That is an affordance outlives its staging, doing work at a level we had not applied it to — and it is the entire mechanism for a second implementation, requiring nothing new.
But loom was right that something was missing
loom defined none of the affordances a registry actually provides. Not handing something over, not saying you depend on something. So the registry would have invented them, and then it would own the idea and the code — exactly the thing that felt off.
Fixed: hand something over so others can depend on it and say that you depend on something are now loom's, with their roles mostly uncast, as a
specification's roles should be.
And find an affordance is deliberately still absent — entry 24 holds it, and
loom should not define an affordance whose first implementation is on hold.
44. The registry's address is configuration, not an external
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
an application needs no external entry for the registry. The address is
configuration; you paste it.
Advances the bootstrap not requiring the thing it bootstraps.
Because loom feared the first external any app has must be to the registry, which would be circular. It does not.
An external entry exists when the reader cannot be assumed to know the thing. There is nothing to understand about the registry — you only need to reach it. And Go-style paths already carry provenance: a consumer files under the owner's host, while the URN carries its registry. Two hosts, two jobs, and neither needs an entry.
This is the fallback from the very first day of this round, unchanged and still right: you paste the URL. No file, no convention, no key in any config. If the pasting ever gets tiresome, the tiresome is the evidence a convention has earned its cost.
45. accommodation stays retired — the relation is already named and the artifact already exists
Decided (2026-09-06, claude-substrate — discovered, cart: almanac): no
third term. The registry accommodates loom — that is a verb we already have —
and the artifact is venues/loom.md in the registry, where it records how it
cast loom's uncast roles.
Advances not reopening a term that was retired for a reason that still holds.
Because loom reached for it twice and the pull is real: loom, cart and annotating describe without implementing; the registry implements. That is a genuine distinction and it wants a name.
It already has a measurement, which is better than a name: a specification is a thing that names roles and casts none of them. annotating casts 6 of 6; cart casts 8 of 12; loom casts 7 of 26. The distinction loom is reaching for is the uncast ratio, and it is countable rather than declared.
The word died because venue and external turned out to be two orthogonal
questions and accommodation had nothing left to name. Nothing here gives it
anything back — "the registry accommodates loom" is the venue relation, and its
artifact is a venue file. Third instance of the same pattern, after
cart.notes.md and the homelab.
And on loom's "a kind of thing that contains": there is no new level. Entry 17 already settles it — loom owns these rules, so loom writes them. That loom's beads are values rather than references is the third venue where that is true, and it is the rule working rather than an exception to it.
46. Retracted: loom does not specify what has not been built
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
four affordances written into loom the same day are deleted. loom holds what
has been extracted from something that worked, never what has been imagined
for something that has not.
Advances the project's own method surviving contact with its author, which it did not for about an hour.
Because loom called it BDUF and it was. I wrote specifications for a registry that does not exist, from reasoning rather than from anything anyone had built — in a project whose entire premise is that we cannot tell which ideas are load-bearing until something runs.
And the tell was in my own text. I recorded, as though it were a nuance:
"By entry 18 they should not be here at all… They are here because loom is a specification and a specification's whole job is to exist before its implementations."
Four rules said no and one special case overrode all four — entry 18 (a staging exists when it has been cast), entry 20 (precedent beats description), entry 24 (do not build against data you imagined), and §8 (prefer easy to delete over anticipating). When a rule must be excepted to permit the thing, the rule is usually working.
What survives: record a decision so it can later be shown wrong, because
it has been cast twice — cart casts its holds the durable record role with
it, and this substrate's event log is the second. Extracted, not imagined.
The corrected shape: the registry owns and defines externals, publication and pinning while it is being built, because it is the only thing that has cast them. Which is how standards arrive — somebody builds a thing, it works, and the shape is lifted out afterwards. HTTP was an implementation before it was an RFC.
47. An affordance can change owner without changing identity
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
rehoming is an operation. An affordance's owner may change — from an
implementer to a specification-holder, or between implementers — and its
identity does not.
Advances extraction being possible at all, since without it "the registry defines this until loom does" has no second step.
Because loom asked for a mechanism of ownership change, and observed the general case: in the wild, a published affordance is often detached from its source and rehomed — rewrites, donations, teams splitting.
Without it, moving an affordance means retiring it here and publishing it there — a new URN, and every consumer's pin breaks for a change that is purely administrative.
With it, almost nothing happens, and the reason it is nearly free is that two earlier decisions already prepared for it:
- A URN carries its registry, never its publisher (entry 36). So identity was never entangled with ownership.
- The registry is a ledger (entry 37). So "ownership transferred at T" is one more event, the history shows it, and no projection has to be rebuilt from scratch.
What actually moves is the receipt — out of one repository's published/ and
into another's. The URN stays, the pins stay, and a consumer who looks sees a
new owner and the same thing.
Not decided: whether the old owner's published/ keeps a tombstone. It
probably should — an empty directory where something was is the silent
degradation we keep legislating against — but that is a shape question and it is
the builder's.
48. "Standard" is not named yet, and the reason is that it is a stage rather than a kind
Decided (2026-09-06, claude-substrate — discovered, cart: almanac): no
new term. What loom is reaching for is a lifecycle stage, and it already has a
measurement.
Advances not coining a word during the week we established that coining words is how this project keeps dying.
Because loom sensed "something valuable waiting to be named — something like a standard", and the sense is right: there is a real difference between a specification that was designed and one that was extracted. The difference is provenance, not shape — which is why it does not want a noun.
An affordance becomes standard-shaped when its owner casts none of its roles. That is countable, and it is the same measurement that separates
annotatingfromcartfromloom.
So the sequence is: implemented → extracted → owned by someone who does not implement it. Get the rehoming operation first (entry 47) and see whether the resulting state still wants a name. If it does, we will know because we keep needing to say it — which is a better reason to coin a word than noticing a gap.
49. A repository per thing that is built; published documents live with their publisher
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
cart and annotating do not get repositories of their own. They are
published documents and they live with their publisher.
Advances not spending a repository on something with nothing manifest in it, and closing a question I asked two days ago and never got an answer to.
Because loom's test is the right one: nothing will really be manifest in them. A repository is for a thing that is built; a published agreement is a document, and a publisher may publish several.
This retires entry 26's implication that each agreement needed its own home.
The requirement was that origin: point at something real, and one repository
satisfies it for both — which is also why the Go-style path ends at the
repository and continues into the affordance, rather than assuming one repository
per published thing.
50. bedrock — the seed repository is shaped like a registry export
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
the repository formerly called loom is bedrock, and its structure is
what a registry export would look like, so that a registry can ingest it as
its starting point.
Advances the last of the BDUF problem, by replacing the thing we could not write with the thing we can.
Because loom asked what if it were shaped as an export of a registry that does not yet exist — and it inverts the failure from two entries ago exactly. A specification for an unbuilt system is reasoning. An export is data, and data can be wrong in ways somebody can see.
The registry either ingests it or it does not. That is a test, where a specification is an argument.
The consequence that makes this worth more than the naming
It puts entry 40's invariant on the normal path. The registry must be rebuildable from the repositories that published to it — and here that reconstruction is not the disaster-recovery route, it is the bootstrap route.
Restore paths only used in disasters do not work in disasters. This one is exercised the first time anything runs, and every time afterwards. We did not plan that and it is the strongest reason to do it.
Genesis is a distinct operation
bedrock's identities are accepted as given on a first import into an empty store. Everything after is ordinary publication, which mints.
Re-importing over a populated store mints fresh identities and breaks every pin — the same hazard as re-publishing during a rebuild, and it earns the same separation. Genesis and publish must not share a code path.
On the URN-shaped identities, and signatures wanting to sneak back in: loom's instinct that they were creeping is correct, and the answer is the one already settled in entry 41. A signature vouches for an identity across a boundary of trust; there is no such boundary here — one tailnet, one operator — and the vouching is that the operator ran the import. Trigger unchanged: when someone must verify without being able to ask.
And it keeps existing afterwards
bedrock does not dissolve into the registry. Entry 40 forbids it: a registry must not hold content no repository holds. But its role changes — it stops being a fixture and becomes a repository that publishes, and changes reach the registry by being published rather than imported again.
On the rename itself: cheap, because nothing is built on the old name, and
loom/bedrock resolves the awkwardness loom already flagged in loom/loom.
loom is the project; bedrock is a repository in it — and this is naming a
directory, not improving a term.
Consolidation performed at the same time, per entry 49: cart and
annotating are inside bedrock, since they are published documents and a
publisher may publish several.
51. Reachability and ownership are independent, and conflating them is what makes multiple registries feel hard
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
many registries share one bedrock. A consumer in one may pin a URN minted by
another. An affordance's registry never changes; its owner may.
Advances the enterprise shape being a consequence of what we already built rather than a thing needing new machinery.
Because loom described a real topology — a platform per business capability, quoting and claims each with a registry, all sharing bedrock — and asked whether affordances are transportable across registries. They are, and "transportable" turns out to be two different questions that we had been asking as one:
| question | answer | |
|---|---|---|
| reachability | claims wants to use quoting's thing | pin it. A URN carries its registry, so it resolves — nothing moves |
| ownership | who maintains this now? | rehome it. Entry 47: the owner changes, the identity does not |
You do not move an affordance in order to use it. Cross-registry pinning is the answer to reachability, and rehoming is the answer to maintenance.
And a cross-platform pin makes platform coupling visible, which is the thing an enterprise most wants and least has.
The one thing that is genuinely expensive, stated so nobody discovers it
A URN's minting registry is part of its identity and cannot change. Like an ISBN's issuing authority: the publisher may change hands, the number does not.
So moving an affordance to a different registry means a new identity, and every consumer must re-pin. That is expensive and it should be — it is a real change, not an administrative one. Rehoming across owners is free; rehoming across registries is not, and the asymmetry is the design working rather than a gap.
And this removes the pressure it looks like it creates. You never need to guess at publication time which registry a thing might one day belong to, because reachability never required moving it.
Note for entry 24: multiple registries is the concrete shape that makes search necessary. "Which registry do I even look in?" is exactly a question the gap list cannot answer — the un-hold condition, arriving with a topology rather than a count.
52. The affordance/staging split holds at bedrock, and here is the test for whether it is doing work
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
yes. bedrock owns affordances; implementations own stagings of them.
The test: could two implementations differ in their roles, or only in their beads?
Roles differ → the split is doing work. Only beads differ → you have a protocol with exactly one staging, which is still legal and still uses the same terms.
Advances having a way to tell when the indirection earns its keep, which the first handoff named as the place it most suspected itself.
Because loom asked whether the split still makes sense this high up, and it does — but not everywhere, and the difference is checkable rather than felt.
Where it does work: hand something over so others can depend on it, once the registry has actually implemented it. A registry that mints identities and a registry built on content-addressing have genuinely different roles — one casts mints an identity, the other casts derives identity from content, and there is no bead that turns one into the other. Two stagings, one affordance, and a consumer picks.
Where it does not: annotating casts 6 roles of 6 and there is no second way
to do it. The protocol is the staging. That is the commitment case from
entry 1, and it is fine — catalogue versus commitment is about whether
alternatives exist, not about what kind of thing something is.
So bedrock will hold both: affordances with exactly one staging forever, and affordances with several. Nothing distinguishes them structurally and nothing should — the second becomes the first the moment somebody implements it differently, and that is not a change of kind.
On loom's Latin: publish is to make public, and it is worth noticing the design agrees. Publication is about crossing a boundary, and the boundary can be redrawn without the thing moving — which is the whole of entry 51 in one word's etymology.
53. Space is cheap. Time is what costs. The identity machinery is deferred with a trigger.
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
v1 serves the read scenarios with paths and prose. Minting, receipts,
immutability, genesis and the ledger are designed and deferred, and the
trigger to build them is stated below.
Advances not building the apparatus before anything needs it — which is the sin of the tool loom is afraid of becoming.
Because loom named three scenarios and every one of them is a read:
- What are my options for publishing to my local cluster?
- Does Hypertheory.API let another app list current students? If not, it is a gap.
- What are all these
.loomthings — what is a cart? a venue?
Running each against the design, none of them needs the identity machinery on first use:
| needs | does not need | |
|---|---|---|
| cluster options | published affordances, stagings, who pins them | URNs, receipts, a ledger |
| does the API do X | the same, plus gaps | the same |
| what is a cart | bedrock being a repository you can clone | any of it |
The write-side machinery is for time, not for space. Space is paths and prose. Time is what costs, and time has not started yet.
We spent two days on minting, receipts, immutability, genesis, rehoming and ledgers — and about ten minutes on what a person actually reads. The justification for all of it is real: it is what makes the read side trustworthy after things change. Nothing has changed yet.
Today's mechanism for "did it change" is a git diff, which is what
must-revalidate has meant all along and which cost loom two minutes the one
time it mattered.
The trigger: when a consumer is burned by a change they did not see. Not a date, not a count of repositories. One person surprised once.
What this demotes: entries 13, 31, 32, 33, 34, 40, 47 and 50 remain decided and move behind that trigger. They are not retracted — a deferral with a stated condition is what §8 asks for, and re-deriving them later is the cost entry 12 says we keep paying.
54. Is this WSDL? Five ways it is not, and one way it becomes it
Finding (2026-09-06, loom + claude-substrate — cart: almanac): loom asked
directly and the question deserves the direct version, since the answer is
mostly no and the exception is the thing loom just noticed.
What made WSDL bad, so we are measuring against something rather than a feeling: it described shape rather than meaning; it was generated so nobody read it; it produced codegen and therefore coupling; it grew to describe everything and so said nothing; and it existed for machines, so humans stopped understanding the system.
Five ways this is not that:
- It records meaning, not shape. "There is only a notion of a current course; VMs are destroyed at the end of a session" — an interface document carries none of those three facts, and they are the ones a developer needs.
- Nothing is generated from it. No clients, no stubs, no coupling. A pin is a fact, not a binding.
- Only the delta from ambient knowledge. WSDL described everything including the obvious; this refuses to document Kafka.
- It has a place for not knowing. Looked-at-and-empty, gaps, uncast roles. An interface description has no way to say "we checked and there is nothing there."
- Being wrong out loud is the mechanism. WSDL was authoritative by construction; here a wrong entry that provokes a correction is a success.
The one way it becomes WSDL, and it is the live risk:
If the write-side apparatus arrives before anyone reads anything.
Machinery built ahead of use is exactly how a description format becomes ceremony — and the correction is entry 53, which loom prompted by noticing the imbalance before it had a name.
Second-order risk, worth naming once: receipts are generated lock files, and lock files are the closest thing here to WSDL's shape. They stay safe only while nothing generates code from them. If anyone ever builds a client from a receipt, this became the thing we were avoiding.
55. A staging lives in the repository that implements it, not in a registry
Correction (2026-09-06, claude-substrate — cart: almanac): entry 43 said
"each registry owns its staging" and entry 52 asked whether "two
implementations" differ in their roles. Both readings put implementations
inside registries, and nothing does.
A repository implements. A staging lives with the implementation. A registry serves published descriptions of it, and differences from the shared thing are recorded in a venue file.
Nothing in the design was wrong; the phrasing was, and it produced exactly the confusion it should have. Entry 52's test is unchanged and still holds — it is a test about two repositories, not two registries.
56. An affordance is a description; a staging is a table; a bead is a document
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a staging is one markdown file containing a table — roles as rows, beads as
cells. Not a directory of role files.
affordances/<name>/affordance.md description, and links to its stagings
affordances/<name>/<staging>.md a table
beads/<name>.md a document
Advances the thing being stored in the shape it already has, and the read scenarios from entry 53 being served by a file somebody can actually read.
Because loom asked whether an affordance could be a named markdown file, a description, and a table of roles and beads — and a table is what a staging already is. A list of roles that must be cast. We had been storing a table as a directory: thirteen files for the cart affordance, where two and a handful of beads say the same thing.
It repairs the bootstrap's worst finding rather than routing around it
That finding was role and bead are indistinguishable — every role file is a name followed by a nominalisation of its own bead. They were indistinguishable because they were the same file.
A role is a label. A bead is a document. The table is the staging.
A row has nowhere to hide prose, so the prose goes where it belongs, and the distinction becomes structural instead of aspirational.
Three things improved that were not aimed at
Uncast roles are visible at a glance. Eight cast, four yours is a column you scan. The measurement separating a protocol from a specification stopped being a count and became a look.
The comparison view falls out. Two stagings are two tables with the same row labels — which is exactly the tailnet-versus-public table in TAILSCALE.md, the thing identified on day one as the most valuable content in that document.
The receipt is the same document with a different column. Entry 34 called a receipt the staging with every role carried by an identity — so publishing renders a column and restructures nothing. Names now, identities later, one table throughout — which also means the deferral in entry 53 costs no rework.
And it dissolves the bootstrap's third finding
affordances/<slug>/staging/ could hold exactly one staging and could not
express a catalogue. A file per staging can, and it is also required by entry
13: a staging is addressed and 410s, so it must be its own thing.
Cross-affordance dependencies become a link in a cell — cart's holds the durable record points at the event-log affordance from inside the table, which is more legible than the prose note it replaces.
57. A consumer's file is the complement of the staging, not a copy of it
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a consuming repository holds a table containing only the rows the publisher
left uncast. Nothing the publisher cast appears in it.
venues/<publisher host>/<org>/<repo>/<staging>.md
Advances the completeness of an adoption becoming checkable, which is a freshness case we had already found and had no mechanism for.
Because loom asked whether a consumer's cast with column would just be the bead URNs — and running it through our own rules gives something better than a copy. Say it once, where it is owned: the eight cast roles belong to bedrock, so reproducing them here is duplication. Only the delta from ambient knowledge: the delta is the four casts. Entry 30 already said this and this is its shape.
Their table has holes. Ours fills them. Put them side by side and you have the whole thing, with nothing said twice.
And that makes an open freshness case mechanical
We recorded that a new yours to cast role appearing upstream leaves an
adoption incomplete, and neither 200 nor 410 says so — because completeness
is not identity.
With two tables it is a set difference. A role marked yours-to-cast upstream with no row here is an incomplete adoption, and it is one comparison rather than a reading.
Role labels are the join key, which is entry 3's carve-out working across a publisher/consumer boundary rather than within a catalogue: the publisher imposes the vocabulary, so the labels collide for free. A retyped label shows up as one unmatched row on each side, which is a visible signature and needs no new machinery.
What actually fills the column
Not URNs, mostly. A consumer's casts are its own beads — a presence, a path, a branch — and most of them are never published, because nobody else depends on them.
The column holds a link to a bead. Whether that bead has a URN depends on whether it was published, and a consumer's casts usually are not.
Which is affordances/ versus published/ showing up on the consumer side, and
it is the asymmetry that makes bedrock bedrock: its beads exist to be depended
on; a consumer's exist to be used.
Small finding: you adopt a staging, not an affordance — a specific way of doing the thing, not the thing. So the consumer's file is named for the staging, which is also why the path can stop at the repository and does not need the affordance slug.
58. A derived rendering is a third category, and provenance is not the field we removed
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a consuming repository may hold a readable rendering of an adopted staging —
theirs and ours together — which is derived, carries what it was derived from,
and is never authored.
Advances entry 53's third scenario — what is a cart? — being answerable with no network, which the complement table alone does not do.
Because loom asked for the affordance to be readable as a markdown page, the
way a README projects published/. The complement table is correct and it is
not legible: four rows of our casts tell a stranger nothing about what a cart
is.
And a derived rendering is not the duplication we forbade. Say it once, where it is owned prohibits two authored copies that can drift silently. A rendering has one author — the derivation — and says what it came from, so drift is detectable rather than silent.
Copy, reference, rendering. The first is forbidden, the second needs the network, and the third is what makes an offline reader possible.
And provenance is not checked: returning. checked: recorded when we last
verified something we could ask about; provenance records what a derived artifact
was derived from. The first is a note about diligence and became pointless when
asking got cheap. The second is required by any derived artifact — you cannot
tell a rendering is behind without knowing its source.
59. Uncast roles borrow the annotating vocabulary, but must not borrow its file
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
an uncast role may be a prompt — Select: with castable options, Question:,
Affirm: — and a consumer's answer may be Skipped. The answers live in the
consumer's own file, never in a notes file.
Advances a distinction the complement table could not make, using a vocabulary both parties already read.
Because loom noticed the fit and it is close: cart.md's "Roles you must
cast" is already prompt-shaped, and this repository answered it with Select:
and Question: headings weeks ago.
What the vocabulary buys, and it is a real gap being closed:
Skipped.— considered and passed — is not the same fact as an uncast role, and the complement table could not tell them apart.
Entry 22 gave four meanings for a role with no bead on the publisher's side. This is the fifth, and it is the consumer's: we looked at what you left for us, and chose not to fill it. An unfilled role and a declined one must not read alike, and until now they did.
And Select: lets a publisher offer castings — "where does a round live?
.loom/cart/, elsewhere under .loom/, outside it entirely" — with
annotating's rule attached: never enumerate an escape hatch. An option on
the list can be chosen and you are done; an answer that is not on the list has to
be written, so it arrives with a reason.
What must not be borrowed, and it is the half that would break
Not the notes file. Casting by writing cart.notes.md beside an adopted
staging recreates the deadlock entry 30 documented: notes freeze their source,
the response is to rewrite the source, and the source is not ours to rewrite.
We are living inside that failure right now — cart.md and annotating.md
have been frozen since 2026-08-21 for exactly this reason.
Take the prompts and the answer conventions. Leave the file. The consumer's casts belong in a file the consumer owns.
60. A projection erases the machinery, and the diff of two projections is the change report
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a projection of a staging is prose with no vocabulary in it. A heading, the
description, then a section per role carrying the body of its bead. No links,
no tables, no mention of beads, roles, stagings or affordances.
And it must be pure: the same source renders the same bytes, so that re-fetching and diffing is the whole change-detection mechanism.
Advances the read scenarios costing what they are worth, instead of costing a vocabulary lesson first.
Because I built the wrong thing and loom corrected it: my rendering had a table, links and a whose column — the ontology leaking into the output. loom's point is exact: in 99% of the places I'd use cart, I don't want to waste context on "WTF are beads? Stagings?"
The ontology is an authoring tool, not a reading format. It is how the document is assembled. It should not survive assembly.
And purity is a real constraint, which I had already broken. My first rendering carried "derived … @ 2026-09-06" — a timestamp, so every re-render diffs even when nothing changed. Provenance must be content-derived: a source hash is pure, a date is not.
The consequence, which is larger than the formatting
Re-GET and diff is the change report.
You do not need identity, 410s or a ledger to know that something changed —
you need them to know what kind of change it was. For most uses, "the text
changed, and here is which paragraph" is the whole answer, and it costs a GET
and a diff.
That strengthens entry 53's deferral considerably. The machinery was already behind a trigger; this shows the read side does not merely survive without it, it has a good answer without it.
And several projections come off one source
Prose — what is this and how do I use it. Comparison — this staging against that one, which is the table shape. Precedent — who else casts this. Each drops what the others need, which is entry 5's a projection is audience-shaped arriving with three concrete audiences instead of one abstract claim.
61. The round trip: cart.md was always this projection, written by hand
Finding (2026-09-06, claude-substrate — cart: almanac): the prose
projection of the cart staging reconstructs something very close to cart.md
itself — the agreement's own sections map almost one-to-one onto the roles.
Which makes the round trip a test we can actually run: decompose a document into roles and beads, project it back, and compare. Where they differ is what the decomposition lost, and that is a better check on the ontology than any argument about it.
What it lost, and it is one section: cart.md ends with "What we know is
unsettled" — whether size is the only conversion trigger; whether specimen and
polad stay two roles.
A published thing's own open questions have no home.
Not a role — nothing casts them. Not a gap — nothing is needed from anyone. Not a decline — nothing was refused. Not a write-ahead entry — that is the author's private workspace, and these were published deliberately, to be argued with.
This is the fourth or fifth time this cart has found a place for not-knowing and been right to. This one is the publisher's own uncertainty, published on purpose, and I do not have a home for it. Recorded rather than solved.
62. Casting by annotation, and the freeze is the feature
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
a consumer may cast a staging's open roles by annotating the projection. The
resulting freeze is deliberate, and an upstream change arrives as a polad.
Advances a fetch that would overwrite becoming a decision somebody makes, rather than a file that changes underneath them.
Because loom proposed it and it dissolves the objection I raised one round
earlier. I said notes would freeze a source we cannot rewrite — and that was
true of cart.md, which was adopted by copy from somebody else. The
projection is ours: derived into our own tree, and regenerable. The deadlock
does not reach it.
The freeze is the point, not the cost
Once notes exist the projection is fixed, so a re-fetch cannot silently overwrite the text the casts are anchored to.
Without the freeze,
fetchclobbers. With it,fetchhas to become a decision.
That is entry 11's failure — a divergence nobody announces is indistinguishable from a plan that was followed — and the freeze is what makes this one announce itself.
And the new projection is a polad by definition rather than by analogy: a candidate artifact, shaped exactly as it would be if kept. Two exits. Applying means dissolving — carry the casts into the record, delete the notes, re-render, and the projection is released.
Where the casts live, which is the part that had to be got right
A projection is derived from their staging plus our casts, and is pure given both. So the casts cannot live only in the notes: the notes die at dissolve, and purity breaks if a projection carries authored content nothing else holds.
Annotations are how a cast is made. The complement table is where it is kept.
Which is the two-stage shape this project keeps arriving at without designing it: cart → event log, write-ahead log → event log, cart gaps → durable gaps, and now notes → complement table. Four instances. At this point it is a property of the thing rather than a coincidence.
Proportionality, stated so this does not become ceremony: available, not mandatory. If you know your casts, write the table and be done. Annotate when there is something to work out — which is the same test the cart itself uses.
63. Content negotiation is required, not convenient — and the ETag finishes the freshness story
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
one address per staging, several representations. Accept selects between
the browsing surface and the prose projection. The server returns an ETag,
and If-None-Match is the freshness check.
Advances the read side's change detection being entirely stock HTTP, which is what entry 53's deferral was betting on.
Because loom asked whether a projection could come back from the same place — and it must, rather than merely may.
Two URLs would be two identities for one staging. And then which one
410s? Identity is the thing this design has been most careful about, and separate addresses for representations would spend it for nothing.
It is also 410/303 again: lean on what the web already does, because an
agent knows Accept without being taught — entry 9, at zero bytes.
The three status codes cover the whole read-side freshness story
| meaning | cost | |
|---|---|---|
304 |
nothing changed | no body |
200 |
changed — here it is, diff it | one fetch |
410 |
replaced — go to the affordance | one fetch, then a 303 |
No custom machinery anywhere in that table. loom's "basically a hash of what
is at the server" is an ETag, and every HTTP client already implements the rest
of it.
Provenance carries the source ETag, so is my projection behind? is a
HEAD with If-None-Match — no body transferred, which is about as cheap as
freshness gets.
The catch, which would otherwise be found painfully
A projection is derived from their staging plus our casts. The server can only hash its half.
The server's
ETaganswers has upstream changed. It cannot answer is my rendered file current, because it has never seen our casts.
Two hashes, two questions: the source ETag in the provenance comment, and —
if we want it — a local hash over source ETag + our casts. Conflating them
would report a projection as current after we had edited our own half.
And ETags are per-representation: the browsing surface and the prose
projection have different bytes and therefore different tags. Vary: Accept,
or a cache serves one where the other was asked for.
64. Confidence in the representation, stated as a split rather than a number
Finding (2026-09-06, claude-substrate — cart: almanac): loom asked how
confident I am that an affordance can be represented meaningfully. The honest
answer has three parts and only one of them is high.
High — agreements and protocols. Three done on real content: cart,
annotating, record a decision. None had to be forced, the table shape
improved legibility rather than costing it, and entry 61's round trip
reconstructs something close to the source document — which is a real check on
faithfulness rather than an argument about it.
Untested — code and running services. Not one of the three is code, a cluster, or an API. The Hypertheory.API case has never been attempted, and it is the one loom actually needs.
Known chafe — the gotchas. ingressClassName is not optional; CNPG cron has
six fields. Three times now these have had nowhere to live — not roles, not
beads, not stagings — and the best home found is a note on a role, which works
and still feels wrong.
And the caveat that matters more than the count
The three successes are correlated. Same author, same kind of artifact, and all three were already well-organised documents with numbered sections that map onto roles.
We may have demonstrated that a well-structured document decomposes, rather than that the ontology is right. And everything real is badly structured.
The test that would move this: project something nobody wrote with this in mind. The cluster was half of that — projected from documents I had read but did not write — and it produced the gotchas problem immediately, which is exactly the signal to expect.
So: high confidence for the case we have done repeatedly, and the number I would not give is one for the case loom most needs.
65. A new or updated external starts life in the cart as a polad
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
nothing is written directly into venues/. A candidate external — new or
updated — lands in the cart as a polad, and applying is the move out.
Advances taking on a dependency being an act somebody performs rather than a file that appears.
Because loom proposed it, and the strongest reason is one I would not have given: the cart has two parties.
An external is a claim about somebody else's thing, and the other presence in the round may know it is wrong.
loom's own example from days ago: "ohhh — that's a leftover from an old way of
thinking." A polad gives them the chance to say it. Written straight into
venues/, an external is a claim nobody reviewed — and entry 8 says being wrong
out loud is the mechanism, which requires somebody to be there when you are
wrong.
And it makes one path where there were two. Entry 62 already routed updates through a polad. A new adoption bypassing that would be the same destination with no review step, which is how the reviewed path becomes the optional one.
It is also a polad by definition rather than analogy — a candidate artifact,
shaped exactly as it would be if kept — and applying is a mv.
Two cases, one path
First adoption: everything starts in the polad, is cast there, and applies out.
Update: the candidate sits in the polad while the current one stays live. You do not take the live projection down in order to look at its replacement, and applying replaces it.
Which makes polad/venues/ an inbox: everything in it is something we are
about to depend on and have not yet committed to. That is a useful listing even
working alone.
And the case where there is no open round needs no new rule
No current/ means no cart. So an external arriving outside a round has
nowhere to land — which reads like friction and is the existing test in
disguise.
If you know, write it. If you are unsure, that is a WTF, and the correct answer to any WTF is create a cart.
Adopting an external you are confident about is not a conversation. Adopting one you are not is, and the rule that already covers it is the one loom wrote.
66. The dyad assumption is inherited, not introduced — and here is what degrades
Finding (2026-09-06, claude-substrate — cart: almanac): loom noted that
entry 65 assumes two parties working together, and that this will not always
hold. True, and worth separating from where it came from.
The assumption is cart.md's, not entry 65's. One daily per presence; there
is never a third; when one party cannot feel a cost the decision belongs to the
party who can. And cart.md already names the hole it leaves: "if neither
presence can feel it — two agents with no human — size is not an available
trigger, and we do not have a good answer for that case."
So entry 65 inherits a known limitation rather than adding one. Which is the useful thing to know: fixing it is upstream work on the agreement, not a patch here.
What survives a single party, and what does not
Lost: the review. An external written straight into venues/ is a claim
nobody checked — and entry 8's being wrong out loud needs somebody there when
you are wrong. Alone, that reason evaporates.
Kept, and these are not small:
- The inbox.
polad/venues/lists everything you are about to depend on and have not committed to. Useful with nobody else in the room. - The act. Applying is a
mv, so taking on a dependency stays something performed rather than a file that appeared. - Continuity across sessions, which is the one I would not have listed and is probably the strongest here. The agent in this dyad is discontinuous — a fresh instance has none of the conversation. A polad is a surface a later instance can read: here is what was about to be committed to, and why it had not been. The conversation does not survive the round; the polad does.
So the rule is worth keeping solo, for weaker and different reasons — and those reasons should be written down, or somebody alone will reasonably conclude the ceremony is for nothing.
*And on the specific case loom raised: the current arrangement is not solo.
loom is a person and an agent; claude-substrate is another. The genuinely
uncovered case is a human with no agent, or agents with no human — and the
second is the one cart.md says it cannot handle.
67. event-log.md moves to the root of .loom/, and the legacy path is not inherited
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
there is no internal/ directory. The event log sits at .loom/event-log.md.
Advances a word not meaning two things in one document, and a known-bad path not being carried into repositories that have no reason to have it.
Because loom caught a collision I had written myself: the structure
document said "there is no internal/ beside published/" in one paragraph and
described internal/ two paragraphs later. Same word, two meanings — one a
refused category, the other an actual directory — which is exactly how a word
becomes shadow policy.
And the directory was wrong on its own merits, separately from the collision:
- It held one file. A directory holding one file is usually a directory that should not exist.
- It named an audience distinction that does not apply. Nothing here is
hidden from anybody;
internaldescribes a disposition, and a false one. - The original handoff already said so: "that path is not where we would have put it. A tool has it hardcoded, from before we understood §4… Consider it a live demonstration — the price of an emitted path is real, nameable, and gets paid by someone who wasn't there."
The point of that note was to stop the cost being paid forward. Copying the path into every new repository would have been paying it forward.
So: this repository keeps .loom/internal/event-log.md, because a tool reads
it and breaking that today buys nothing. bedrock, registry and everything
after use .loom/event-log.md. The mistake stops here rather than
propagating.
And a rule fell out that was worth more than the fix
A stream is a file. A collection of things with identity is a directory.
The event log is appended to and no entry is ever addressed; a bead is one of
many and each is referred to. gaps/ stays a directory because a gap is
reported, homed or withdrawn individually — it has identity, where a log entry
has only a position.
68. external and venues have different units, which is the weak spot — one file per thing
Decided (2026-09-06, loom + claude-substrate — discovered, cart: almanac):
the unit is the thing. external/ and venues/ merge into one directory,
one file per thing we do not own, with what-we-understand and what-we-decided
as sections rather than as separate trees.
Advances a reader not having to learn a distinction before they can find anything.
Because loom asked what the unit of an external is, and tracing it exposes that the two directories never had the same one:
| unit | example | |
|---|---|---|
external/ |
the thing | "there is only a notion of a current course; VMs are destroyed at the end of a session" |
venues/ |
the staging (entry 57) | the four roles we cast on cart |
The content settles it. VMs are destroyed at the end of a session is not a
fact about one affordance — it is a fact about the whole thing, and it is
exactly the sort of sentence that makes an external worth having. So the unit
is the thing, and venues/ having a different one is the inconsistency loom
could feel and I had not named.
The deeper problem, which is worse than the mismatch
One fact splits across two files.
- "
traefikis the default IngressClass, so omitting it publishes publicly" — understanding. - "we always set
ingressClassNameexplicitly" — a decision.
Two sentences about one thing, in two trees, and a reader needs both. The existence conditions from entry 6 are genuinely different — a file exists when the reader cannot be assumed to know the thing, or when we decided something — but different existence conditions do not require different directories. A file may exist for either reason and carry both.
What the split was protecting, and how it survives a merge
The two go stale from different directions, which is the one real thing the separation encoded:
Understanding goes stale when they change. A decision goes stale when we change our minds.
A heading carries that. It does not need a tree.
The shape
external/
├── kafka.md no address: what we decided about it
└── <host>/<org>/<repo>/
├── <repo>.md what we understand about the thing
├── <staging>.md what we cast, per adopted staging
└── <staging>.projection.md derived
Things with an address are filed under it; things without one sit at the top by
name. Which also stops venues/ carrying two shapes for two kinds of thing —
it was doing that because the things differ, not because we do.
Confidence, stated because loom named this as the weak spot and I do not want to sound more certain than I am: the diagnosis I believe — different units, one fact split in two. The merge is the fix I would choose and it is a layout loom has to live in, so it is theirs to reject. What would change my mind: a real case where somebody wants to read every decision without reading any understanding. I could not construct one.
69. Three problems, and the ontology is the third — bracket it
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
what is being built is a documentation service whose goal is agent
orientation. Affordances, stagings, roles and beads are a later layer and are
bracketed.
| 1 | pull descriptions of infrastructure and practice into a repository; keep them fresh; declare gaps against them |
| 2 | name a thing and get an overview, its role, and a link to its repository |
| 3 | affordances, stagings, roles, beads — bracketed |
Advances the two problems loom actually has being solvable now, instead of waiting on a model built for a third.
Because loom's diagnosis is right and it is about mixing altitudes: we were fitting high-level infrastructure and practice description into a model built for spans of source. A square peg into a bead-shaped hole.
And this is not a reversal. Entry 53 already deferred the write-side machinery behind a trigger, for the same reason — the read scenarios did not need it. This goes one step further and defers the model as well. Same direction, one more step.
What relaxes, and each of these was a constraint of the third problem
external/ may hold a copy. Not a violation of refer-never-contain, because
that rule forbids a copy that reads as authoritative and can drift with nothing
to notice. A copy pinned to the publisher's ETag, with a conditional GET
to check it, cannot drift silently. The rule was never "do not cache."
No decomposition, so no projection. Entry 60's prose rendering existed to reassemble readable text from decomposed parts. If you never decompose, you already have their document. Entry 60 is not wrong; it is unneeded until problem 3.
The mutation API may be ordinary resource-oriented REST. The prohibition existed because a submitted document had to declare replace this staging versus edit this staging, which produce different outcomes. With no stagings there is nothing to distinguish, and a description document is a document. It comes back when problem 3 does.
The delta rule moves upstream rather than weakening. Publishers still write only what a reader cannot be assumed to know; consumers cache what they wrote, whole.
Three things I would check before building on it
The lock must be the publisher's ETag, verbatim — never a hash we compute.
A fetch that normalises whitespace, or a proxy that re-encodes, breaks a locally
computed digest and reports a change that did not happen.
Unfreezing is by replacement, not by rewriting, and this is the third time it
has come up. The annotating protocol says dissolve by rewriting the source, and
the source is theirs. loom's answer is right: the new copy arrives as a polad,
the annotations are reconciled against it, and the pair is replaced wholesale.
Worth writing into the agreement, since it is the move annotating does not
have.
Problem 2 is a name lookup, not the search entry 24 holds. That hold was
about finding an affordance by what it does, across everything. Looking up
hall-monitor by name, among things you own, is a directory — cheap, and it
needs none of the machinery the hold was protecting us from.
70. What bracketing problem 3 defers — recorded as a decline, not forgotten
Decided (2026-09-07, claude-substrate — discovered, cart: almanac):
the payoffs that require decomposition are deferred with the model, and named
here so that later they are recognised as deferred rather than rediscovered as
missing.
Advances the difference between we chose not to and we never thought of it, which is the whole reason declines exist.
What goes with problem 3:
- How many ways do we verify credit cards? — needs stagings to compare, and entry 3 already said comparison is authored at the affordance.
- If I change this, what breaks? — needs pins onto beads.
- Precedent as the unit of teaching — entry 20's three applications already do this, which needs something to pin to.
- Alternative stagings of one affordance, and the catalogue/commitment split that entry 1 built.
All three payoffs need a scale we do not have. One person, two clusters, five applications. The credit-card question is an enterprise question and we are not an enterprise yet.
What survives into problems 1 and 2 unchanged, because it was never about the
model: gaps, gap-first ordering, gaps kept in the reporting repository,
being wrong out loud, the asked-not-enforcing posture, a place for not
knowing, the cart and annotating agreements, and ETag-based freshness.
And loom's own last line is the test to keep: a bead should refer, not contain. It is still true where beads are — and where there are no beads, it was never the question.
71. The dissolve is inverted, and an annotation is an index from their text to our artifacts
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
when an adopted description changes, the annotations mostly do not. The
question is not what do we rewrite here, it is:
Given what changed in their document, what do we change in our code?
An annotation should therefore name the artifacts it justifies.
Advances reconciliation having a direction, instead of being a rewrite that cannot happen because the document is not ours.
Because loom put it exactly: "we can't dissolve our annotations into their document — so it is the same thing, sort of on its head." In the normal protocol the notes are about a source and the response modifies that source. Here the response modifies something else entirely — the manifests, the config, the code we built from it. The annotations may survive unchanged apart from line numbers.
Which makes an annotation more than commentary
"We are using the tailscale local cluster approach here, and the manifests in our repo are based on that."
That is anchored to a span of their document and names something of ours. So it answers, in the only direction that matters: if this span changed, what of ours should be looked at?
An annotation is an index from their text to our artifacts, and that is what makes reconciliation tractable rather than a re-read of everything.
Authoring rule that falls out: "we use the tailnet approach" is weak.
"we use the tailnet approach — see manifests/ingress.yaml" is the whole
value. Name what it justifies. Referring to our own paths from our own files
publishes nothing.
And it partially un-defers something entry 70 had just written off
Entry 70 deferred if I change this, what breaks? on the grounds that it needs pins onto beads. That is the provider's question and it is still deferred.
The consumer's version survives the bracketing and costs nothing: their document changed — which of our things did we say were based on it? The annotation already answers it.
Coarse rather than mechanical — somebody reads and judges — and that is the right resolution for one person with two clusters.
72. The form is etcd-shaped: kinds and paths, not pages and links
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
the structure is the schema. A small fixed set of kinds, each a directory,
enumerable at every level, with nothing living outside them. No watch.
Advances the thing having a form loom can name, rather than being defended as not a wiki every time somebody asks.
Because loom reached for etcd as wrapped by the Kubernetes API, but browsable, and the analogy is load-bearing rather than decorative. What that API gives is a total addressing scheme with enumeration at every level — kind, then namespace, then name — so you find things by knowing the shape rather than by following links from somewhere.
A wiki's unit is a page and its structure is links. Here the unit is a kind and the structure is the path.
Type-by-location already is that. The directory is the kind; the path is the
address; ls is LIST. We had the mechanism and not the name for what it was
buying.
The kind-set, and why the bracketing is evidence for itself
Under problem 3 there were eight kinds. Bracketing it leaves five:
external/, venues/, gaps/, cart/, event-log.md.
A set small enough to hold in your head is what makes it a schema rather than a wiki. Halving it was the bracketing paying out, and I did not expect the payment to arrive as legibility.
No watch, and the reason is one we already gave
Kubernetes watch is a client-initiated long poll — pull-shaped, even though
it feels like push. We still do not want it.
It would be the first thing that makes the registry hold persistent knowledge of
its consumers, which is the line drawn at pins-are-not-subscriptions. And a
conditional GET is sufficient for documents that change monthly.
Strictness is asymmetric and that is deliberate: the write side owns what is valid and accepts only known kinds; the read side shows what it does not recognise rather than failing on it. Files are edited outside the API constantly, so the reader must be generous — but nothing arriving through the API gets to invent a kind.
73. Siblings, not sections — because one of the files is not ours
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
the sibling-file pattern generalises beyond notes.
externals/homelab-cluster.md theirs — a pulled copy, frozen, replaced whole
externals/homelab-cluster.usages.md ours — what we use, and what of ours depends on it
externals/homelab-cluster.gaps.md ours — what we expected here and did not find
Advances everything about one thing sitting together in a listing, while the thing we do not own stays a single replaceable file.
Because loom has thought for a long time that the annotating pattern is broader than the cart, and it is. But the decisive reason is structural and it arrived with yesterday's reframe:
homelab-cluster.mdis theirs. Everything beside it is ours.
Entry 68's merge is now impossible and I should say so plainly. I proposed
merging external/ and venues/ into one file with sections — and that was
written when an external was our writing about their thing. Entry 69 made
it a copy of their document, and a merged file would put their content and ours
in one place, which is the thing this entire round has been avoiding. The
reframe invalidated my fix a day after I wrote it.
Two facets, not three, and the test is what reconciliation asks
I nearly split deviations out as a third. They answer the same question as usages:
| facet | on a new version, ask |
|---|---|
| usages | did their change affect what we do — and which of our artifacts? |
| gaps | did their change fill this? |
A deviation is a usage that departs, and reconciliation treats them identically. Facets are distinguished by the question reconciliation asks of them, not by how they read.
And this dissolves the weak spot mechanically
loom named external versus venues as the weak spot and was right. Under the
sibling pattern the line stops being conceptual and becomes checkable:
externals/is for things you can fetch and re-fetch.venues/is for things you cannot.
Kafka, React and HTTP have no document to pull, no ETag, and nothing to
reconcile against — so they have no stem to be a sibling of. The
distinction is now "is there an upstream?" rather than "understanding or
decision", and nobody has to adjudicate a sentence.
74. Gaps need reconciling too, and moving one is homing it
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
a gap is reconciled against a new version like everything else, and a gap
beside an external is a homed gap.
Advances the good half of reconciliation — a thing you needed now exists — being noticed rather than waited on.
Because loom asked whether gaps also need reconciling. Yes, and it is the half nobody builds for: the cluster publishes a new version that adds publishing a container image, and our workaround has been unnecessary since. Entry 24 said that when a gap is homed every workaround that should be retired is findable — but assumed somebody tells us. Reconciling on fetch is how we find out without being told.
The naming collision, and why it turns out not to be one
externals/<thing>.gaps.md and gaps/ are the same word in one tree, which
is the internal/ mistake loom caught yesterday — except here the two meanings
are two positions of one thing.
gaps/— unhomed. Addressed to nobody, because you could not say whose job it is.externals/<thing>.gaps.md— homed. You can name whose job it is, so it is a claim against them and belongs beside them.
Moving a file from
gaps/toexternals/<thing>.gaps.mdis the homing operation. Entry 40 said a gap stays unhomed until somebody claims it, at which point it resolves or moves into that external's file. Thatmvis the claim.
Type-by-location carrying a state transition rather than a type — and it is the one case where that is right, because the position is the claim, not a label on it.
75. An external is always somebody's document, never the technology
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
Kafka can be both, and the apparent conflict comes from a slip in what the
external names.
You never fetch Kafka. You fetch your platform team's document about Kafka.
externals/acme.com/platform/kafka.md is a document with an owner and an
ETag. Apache publishes nothing into your registry; your platform team does,
and at a company that document is where the choices already made for you live —
auth, topic policy, what you may not turn on.
And venues/kafka.md still exists for choices with no upstream document to
hang them on. Both can be true of one technology, because they are two
different things: theirs and ours.
Advances entry 73's line surviving the case that looked like a counter-example.
Because loom is right that at a company much is decided for you, and that decision lives in a document somebody owns. It was never the technology that was fetchable.
Operational tie-break, since the boundary is otherwise a judgement per sentence
and those decay: put a choice where reconciliation will look for it. If there
is a document it is about, it goes beside that document. When in doubt, the
sibling — a choice filed in venues/ is a choice nobody will re-examine when
the upstream moves.
76. The annotating "abstract class" is not one — it is roles nobody cast, and that is the deadlock
Finding (2026-09-07, loom + claude-substrate — cart: almanac): loom noticed
the annotating pattern appears in several forms and reached for an abstract
class with subclasses. It is better than that, and it explains a bug we have
hit three times.
The three appearances, with what differs:
| source owned by | freezes when | dissolves by | |
|---|---|---|---|
| cart dailies | us | notes exist | rewrite the source, delete notes |
| an adopted agreement | them | notes exist | nothing |
| an external | them | the ETag lock |
replace whole, reconcile |
Same roles throughout. Different beads. Which by entry 52's own test means this is one protocol with roles left to the adopter, not an affordance with alternative stagings.
annotatingpublishes a staging in which freezes the source and dissolves are yours to cast — and it does not say so.
And that is why cart.md has been frozen since 2026-08-21
The adopted-agreement case never cast dissolves. Not because anyone chose badly — because the agreement presents the role as already filled, so nobody looked for a decision they had to make.
The bedrock content records annotating as casting 6 of 6 roles, "a complete
protocol with no holes." That is wrong, and it is wrong in the way that
matters: an uncast role presented as cast is invisible, which is the exact
failure entry 22's yours to cast state was added to prevent. We built the
state and then mis-declared the first document we applied it to.
Concrete and small: when annotating is next published, those two roles are
yours to cast, and the external casting — ETag lock, replace-and-reconcile —
is the one an adopted agreement should use. Which unfreezes both agreements as
a side effect, three weeks after entry 30 first said they were stuck.
Recorded, not built on
This is the strongest evidence so far that the bracketed model earns its keep — a real bug, hit three times, diagnosed by the ontology in one table.
It changes nothing today. loom said not to get lost in it and that is right: the finding is written down so it is waiting when problem 3 unbrackets, and the fix it implies is a two-line change to one document, not a project.
77. "Roles you must cast" is already a prompt block — no ontology required
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
what loom described — loom owns annotating; cart and externals each cast
the filename, the lock and the dissolution — is expressible with the
mechanism annotating already has, and needs nothing from problem 3.
Advances delivering, which is the thing at risk.
Because loom said it was sneaking back in. It is not, and the reason is
that we already built this before any of it. annotating says a file may ask
for structured answers with Question:, Select: and Affirm: headings, and
that answers land in the notes file.
"Roles you must cast" is a prompt block. "Yours to cast" is an unanswered prompt.
cart.mdhas done this since before this round started, and the only thing missing is thatannotatingdoes not do it to itself.
So the whole structure is:
## Select: What is the sibling filename?
- `.notes.md`
- `.usages.md` and `.gaps.md`
## Select: What fixes the source?
- the existence of the notes file
- a lock on the publisher's ETag
## Question: How is it dissolved?
cart answers one way. externals answers another. No beads, no stagings,
no roles — a document with questions, and adopters with answers.
And it fixes entry 76's bug without touching the model: the adopted-agreement case failed because nobody was asked. A prompt asks.
Deliverable and small: annotating/v1 gains three prompts; cart and
externals answer them. Two documents, no code, and both agreements unfreeze.
78. The first useful version needs no server: a git host already does conditional GET
Decided (2026-09-07, loom + claude-substrate — discovered, cart: almanac):
problems 1 and 2 ship against a git host and two conventions. The registry as
a service is what comes after, when browsing and gap aggregation are worth
building.
Advances something of value existing, which after three days of this is the only remaining risk worth managing.
Because everything problem 1 needs, gitea already serves:
| need | already there |
|---|---|
| a document with an owner | a repository |
| an address | a raw URL |
an ETag |
conditional GET on raw files, or the commit sha |
| history | git |
| browsable rendering | gitea renders markdown |
And the conventions are two: externals/<host>/<path>.md holds the pulled
copy; .usages.md and .gaps.md sit beside it.
Problem 2 is one file in bedrock — a list of things and their repositories. A name lookup is a grep.
What is genuinely not covered: aggregating gaps across repositories, and an entry point that explains itself. The first wants a service. The second is a README.
So the honest scope of the server, stated now rather than discovered later: it buys aggregation and a self-describing surface, and it buys them after the conventions have been used enough to know their shape. Entry 12 said there is a server and that is unchanged — this says what it is for, and that it is not first.
Declined (2026-09-07, loom + claude-substrate — cart: almanac): a registry
service, first. Considered for three days.
Because we believe a git host already provides a document with an owner, an
address, an ETag, history and rendering — so the service buys aggregation and
a self-describing entry point, and buys them better after the conventions have
been used. Wrong if the conventions turn out to need a server to be usable at
all, which using them will show.
Declined (2026-09-07, loom + claude-substrate — cart: almanac):
affordances, stagings, roles and beads, for infrastructure and practice
description. Built out over three days and then bracketed.
Because we believe the model is for describing software above its implementation, and that infrastructure and practice description is a different altitude being forced into it. Wrong if the documentation layer turns out to need decomposition to answer anything useful.
Declined (2026-09-07, claude-substrate — cart: almanac): bulk recasting a
bead across stagings.
Because we believe the list is the migration plan — resumable, observable, and owned by whoever owns each staging — where a bulk operation is one uninspectable moment that breaks every consumer at once.
Declined (2026-09-07, claude-substrate — cart: almanac): signatures.
Because we believe a content hash catches every accident, and a signature answers an adversary that does not exist — one tailnet, one operator. Revisit when a consumer must verify without being able to ask.
Declined (2026-09-07, claude-substrate — cart: almanac): watch, and any
subscription.
Because we believe it would be the first thing giving a registry persistent
knowledge of its consumers, which is the line drawn at a pin is not a
subscription — and a conditional GET suffices for documents that change
monthly.
Declined (2026-09-07, claude-substrate — cart: almanac): naming the thing
we kept reaching for — "accommodations", "standard".
Because we believe it is a lifecycle stage with a measurement rather than a kind needing a name: a specification is a thing that names roles and casts none of them. Coin it when we keep needing to say it, which is a better reason than noticing a gap.
Declined (2026-09-07, claude-substrate — cart: almanac): an internal/
directory.
Because we believe naming the complement of published/ invents a category —
and one called internal/ would look like a security boundary and not be
one.
Decided (2026-09-07, loom + claude-substrate — discovered, cart: kestrel):
a decline is an entry. .loom/declines.md is removed and its seven entries
are above.
Advances one record rather than two, and the ordering between what we did and what we considered and did not being visible.
Because loom asked whether declines.md was a thing or had drifted. It had
drifted — written during conversion because the cart agreement says a round
emits the artifact and the declines, and made a separate file without checking
against the primitive published in this repository an hour later. Which says, in
so many words, declines belong here too.
A decline is a decision: what you considered and did not do, with the belief that could later be shown wrong. Splitting them loses the ordering, and makes a reader know to look in two places.
The general form, worth having: a second file for a special case of something that already has a home is drift, and it is easiest to catch by reading your own published rules back.
Decided (2026-09-07, loom + claude-substrate — discovered, cart: osprey):
the starting page no longer links to the worked example, because the example
is private and this page is not.
Advances a public document not promising something its reader cannot reach.
Because loom-cli's builder measured all three things we had published and
none of them could be fetched by somebody who is not us:
loom/settled 404 anon the page written to fix "settled is private"
loom/.loom 200 anon fetchable, but not where a reader lands
jeffry/homelab-cluster 404 anon and this page pointed at it, publicly
Three for three. Every time the question can somebody who is not you fetch this was asked, the answer was no.
A public page naming a private thing as its canonical answer is worse than no example. It costs the reader a request and their confidence in the rest of the page.
The diagnosis is theirs and it is the one to keep: publication was being
treated as a change of directory rather than as the act publication.md says it
is. Moving a file into published/ is not publishing. The file becoming
fetchable is.
And the command we nearly cut for being a third kind of thing — reachable —
found all three on the day it would have shipped. That is its validation and not
a feature request.