a round, four externals, and the spec as a specimen

The repository is furnished rather than scaffolded — every file has content and a
reason, which is the distinction the spec itself draws when it says to resist
building init as a scaffolder.

Four externals fetched and locked under .loom/externals/: bedrock, externals,
annotating, cart. Copies, not to be edited, with a usages file beside cart
casting three of its four roles and leaving one open. The one left open is what
isolates work in progress, and the usages file says how we cast it wrong once and
why the belief was more useful than a preference would have been.

The spec moves out of the README and into the cart as a specimen, which makes it
discard-by-default — that is a stronger statement of "argue with this" than a
sentence asking them to. bedrock and externals stay non-discardable, since
accommodating them is what makes this a loom tool.

The daily says where we expect to be wrong: whether check can say anything useful
about a 404, and whether hash-as-lock survives a fetch that normalises anything,
which we have only tested against gitea.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 12:29:53 -04:00
co-authored by Claude Opus 5
parent 304b854dbd
commit 81c4917b9f
13 changed files with 863 additions and 81 deletions
@@ -0,0 +1,65 @@
# claude-substrate — cart `osprey`
**Opened before you arrived**, so that the first thing here is a round rather
than a briefing.
---
## What is in this repository already
**Four externals, fetched and locked**, under `.loom/externals/`. *`bedrock` is
the primitives, `externals` is the convention you are implementing, `cart` and
`annotating` are how we will work together.* **They are copies. Do not edit
them** — a facet goes beside a file, never into it.
**One specimen: [`proposed-spec.md`](specimens/proposed-spec.md).** *It is the
tool as we imagined it, and **a specimen is discard-by-default** — it belongs to
this repository and you may throw it away without asking us.* **That is not
politeness; it is what a specimen is.**
> **`bedrock` and `externals` are not discardable.** *Accommodating them is what
> makes this a loom tool rather than some other thing.* **Read them as given.
> Argue with the specimen.**
## What we think this is
**A fetcher and a comparator, and it should stay one.** *Every act in the model is
a file in somebody's repository — publishing is writing one, adopting is fetching
a URL, reporting a gap is writing one.* **Nothing sends a service a request.**
**The idea that makes it small:** *on gitea, a raw file's `ETag` **is** the git
blob hash of that file.* **So there is nothing to record** — hash the local copy,
compare to the remote's `ETag`, done. *Verified on both a public and a private
repository.*
## Where we expect to be wrong
**Where the conventions chafed.** *A thing you had to do twice. A rule you worked
around to make a command sane.* **Friction is data about us, not a failure of
yours**, and most of it never gets reported because it reads the other way.
**Whether `check` can say anything useful about a `404`.** *Over HTTP, "gone" and
"you lost access" are the same response.* **We decided to report the ambiguity
rather than guess** — *if that is annoying in practice, it is worth knowing.*
**Whether the hash-as-lock survives contact.** *It assumes the local copy is
byte-identical to the remote. **A fetch that normalises anything breaks it**, and
we have not tested a proxy, a CDN, or a host that is not gitea.*
## What we would ask you not to do
**Do not build `init` as a scaffolder.** *Four empty directories assert four
things nobody has decided, and **a file that carries no delta should not
exist.*** *If `init` earns its place, it creates one file and asks one question.*
---
## Question: what isolates your work while a round is open?
*The one role we left uncast — see
[`cart.usages.md`](../../externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md).*
## Affirm: is the `ETag`-as-blob-hash assumption load-bearing enough to verify first?
*If it does not hold, most of the specimen collapses and it is better to find out
in an afternoon than in a week.*
+1
View File
@@ -0,0 +1 @@
osprey
@@ -0,0 +1,85 @@
# loom-cli
**Not built yet.** *This is the spec, written while it was fresh.*
**A small tool for the operations a person should not do by hand:** *fetch a
document you depend on, and find out when it changed.*
---
## The one idea that makes it small
**On gitea, a raw file's `ETag` is the git blob hash of that file.** *Verified on
both a public and a private repository — the header and `git rev-parse` return the
same value.*
> **So there is nothing to record.** *`git hash-object <the local copy>` **is** the
> lock. Compare it to the remote's `ETag` and you have your answer.*
**No lock file, no state, no `pull` metadata to drift.** *And it catches a case we
had not considered: **if somebody edits the local copy, the hash stops matching
and `check` reports it** — which is correct, because an adopted copy that has been
edited is no longer a copy of anything.*
## Commands
### `loom check`
**For every file under `.loom/externals/`:** *compute its hash, `HEAD` its source,
compare.*
| result | means |
|---|---|
| **hashes match** | nothing changed |
| **hashes differ** | **upstream moved** — the new copy is a candidate, not a replacement |
| **local hash differs from what the remote had at last fetch** | *somebody edited a copy that is not ours* |
| **`404`** | **gone, or access lost — these are indistinguishable over HTTP.** *Say so; do not pick one.* |
**Report. Do not fix.** *A changed external becomes a polad in the cart, and
somebody decides.*
### `loom pull <url>`
**Fetch one document into `.loom/externals/<host>/<path>`.** *The path mirrors the
URL, so nothing has to record an origin.*
**Do not create a `.usages.md`.** *An empty facet asserts that we have something
to say and we do not — **a file that carries no delta should not exist.***
### `loom publish <path>`
**Verify a document in `.loom/published/` is fetchable by somebody who is not
you.** *One anonymous request.*
> **Publishing is not an act you can complete alone.** *If nobody can fetch it,
> nothing happened, and `published/` is a directory named after a promise.*
### `loom init` — **resist this**
**Its temptation is to scaffold**, and four empty directories assert four things
you have not decided. *The useful version creates `.loom/README.md` and asks one
question.* **Creating the tree is not a feature.**
## Credentials
**Read over HTTP with a read-only token. Write over ssh.** *Split by operation,
not by whether a repository is public.*
**Name the token for its scope — `loom-read`.** *A token cannot be asked what it
may do: `/api/v1/repos/…` reports the **account's** rights, not the token's.
**Scope is set once at creation and invisible afterwards, so the name is the only
record.***
*Token in `~`, never in a repository.*
---
## Why so little
*Every act in the model is a file in somebody's repository.* **Publishing is
writing one; adopting is fetching a URL; reporting a gap is writing one; homing it
is an `mv`.** *Nothing sends a service a request.*
**So this is a fetcher and a comparator, and it should stay one.** *If it starts
holding state, ask what it knows that no repository knows — that is the thing that
will rot.*
@@ -0,0 +1,158 @@
# Agreement — annotating
**v0.** Depends on nothing. Everything else depends on this.
**How two parties mark up each other's files without either of them having to
diff anything.** Deliberately minimal — this is the most-depended-on thing
published here, so a revision costs everyone downstream an afternoon.
---
## The protocol
**Whoever wrote a file does not annotate it. Whoever annotates a file does not
edit it.** That is the whole of it, and it exists so that neither party has to
reconcile two versions of the same document.
## Where annotations go
**For any file `plan.md`, annotations go in `plan.notes.md`, beside it.**
The source file is never modified. **A notes file is never annotated** — there is
no `plan.notes.notes.md`.
**A file stays editable until it has annotations.** Until then nobody has
responded and revising it is just continuing to write. **Once notes exist the
source is fixed**, because the quotes in those notes would come loose. **Deleting
the notes releases it.**
## The format
**A heading giving the location and author, a blockquote of the exact source
text, then the comment.**
```markdown
# Annotations for plan.md
## line 13, col 2448 — jeff
> with a cap of thirty seconds
Where did thirty come from?
## lines 4047 — jeff
> ## Venue
> This application will be run locally. It will be browser based, but
>
> to be in the position of rewriting this portion when that happens.
Tauri is the likely landing spot, not Deno.
```
**Rules:**
- **Quote the source markdown, not the rendered text.** The quote is how a reader
finds the span, and **it is the only part that must be exact.**
- **Quote exactly what was annotated.** A phrase selection quotes the phrase; a
block selection quotes the block.
- **Elide long quotes** with `…` on its own line — first line, ellipsis, last
line.
- **Coordinates are optional.** If you can quote accurately but are unsure of
line numbers, write `## — jeff` and leave them off.
- **An entry with no quote at all** is a comment on the whole document. It sorts
first.
Entries appear in document order.
## Asking for a specific kind of answer
**A file may ask for structured responses using heading keywords.** Any heading
level works; the prompt block runs until the next heading of the same or higher
level.
```markdown
## Question: Which storage adapter for v1?
Freeform. Anything the reader wants to say.
## Select: Which of these should land this sprint?
- Block-level anchoring
- Prompt widgets
- Notes file format
## Affirm: Ship behind a flag?
```
`Select` uses the first bulleted list in its block as the options; **any number
may be chosen.** `Affirm` is yes or no. **Only the first word of the heading is
matched, case-insensitively** — whatever follows is free text.
**Answers land in the notes file like everything else, restating the question so
the notes stand alone if the source is discarded:**
```markdown
## Question: Which storage adapter for v1?
**Answer:** File System Access API. No server process this early.
## Select: Which of these should land this sprint?
- [x] Block-level anchoring
- [ ] Prompt widgets
- [x] Notes file format
**Note:** prompt widgets can wait until the format settles.
## Affirm: Ship behind a flag?
**Answer:** No.
```
**Every response type may carry an additional freeform note**, so there is never
a need for an "other" option.
### A rule for whoever writes the prompt
> **Never enumerate an escape hatch.** No *"other"*, no *"it depends"*, no *"leave
> it to the developer."*
**Not because those answers are wrong — because they are always available
anyway**, and enumerating one changes what it costs.
**An escape hatch on the list can be selected and you are done.** Nothing to
write, no reason given, nothing for anyone to see later. **An answer that is
*not* on the list has to be written down**, which means it arrives with a reason
attached.
> **Enumerating "other" converts a recorded deviation into an unrecorded
> compliance.**
**Put only specific, castable options on the list.** If none of them fits, the
person will say so in their own words, and **their own words are the thing you
wanted.**
**A prompt may be skipped on purpose, and that is recorded**, so *considered and
passed* survives as something different from *never got to it*:
```markdown
## Question: Should the shelf survive a restart?
**Skipped.**
```
## What this does not constrain
**Prompts do not limit where else annotations may go.** A file with five prompts
may also carry thirty freeform annotations anywhere in it, and a file with no
prompts is annotated exactly the same way.
**The keyword set is open.** An agreement that uses this one may declare
additional keywords with meaning particular to it. **A reader that does not
recognise a keyword treats the heading as an ordinary annotation** — it must not
drop it.
---
## Roles you must cast
> **Nothing.** This agreement casts all of its own roles. It is the base, and a
> base with local variation in its mechanism would break everything resting on
> it.
**You may still vary it** — in a notes file beside your copy, with a reason, like
anything else. **But understand what it costs:** every agreement you adopt that
depends on this one is now being read through a mechanism the publisher does not
know about.
@@ -0,0 +1,22 @@
# The `.loom` directory
**A repository that uses loom keeps everything in `.loom/` at its root.**
> **Type is determined by location.** *The directory a file is in says what it
> is.* **So frontmatter carries almost nothing, and often nothing at all.**
**A stream is a file. A collection of things with identity is a directory.** *A
log is appended to and no entry is ever addressed; a thing that is referred to
individually gets its own file.*
**Everything not recognised is shown rather than rejected.** *These files are
edited by hand and by agents, constantly, and that is supported. A reader that
fails on an unfamiliar directory is wrong.*
## What is fixed
**Only `.loom/` itself, and that a directory means a kind.** *Which kinds exist
is not fixed here — conventions built on this one declare their own, and a
repository has only the ones it uses.*
**Nothing outside `.loom/` is loom's business.**
@@ -0,0 +1,31 @@
# Publication
**What a repository has handed over for others to depend on lives in
`.loom/published/`.**
> **Publishing is a change of kind, not a change of status.** *An unpublished
> document is an implementation detail you may rename at will. **A published one
> is a contract.***
**So publication is an act somebody performs** — a file moves in — **and the move
is a diff that shows up in review.** *A flag in frontmatter is a line nobody
notices; this is the moment a thing becomes somebody else's problem, and it
should be unmissable.*
**`ls .loom/published/` is *what have we committed to?*** — and it is the question
to ask before changing anything.
## What is published is a document
**Prose, in whatever form its readers need.** *If a repository organises its
thinking some other way internally, that is its own business:* **a consumer
cannot tell whether it did, and must not need to.**
> **What is not exported is not hidden.** *The rest of the repository is there to
> read. It is simply not what you depend on.*
## There is no directory for what is unpublished
**Everything else is simply elsewhere in the repository.** *Naming the complement
invents a category — and a directory called `internal/` would look like a
security boundary and not be one.*
@@ -0,0 +1,29 @@
# Recording decisions
**An entry says what was decided, what it is believed to advance, and the belief
that could turn out false.**
> **Only a stated belief can later be shown wrong.** *"To satisfy §4" is a
> citation, not a reason — it points at a label.* **A constraint whose reason
> cannot be found gets re-litigated as a defect by whoever inherits it.**
**Appended, newest last, never revised.** *A later entry supersedes an earlier
one by saying so.* **A revised record cannot show that anybody changed their
mind, which is most of what a reader wants from it.**
**No status, no assignee, nothing to process.** *The moment an entry can be "not
yet applied", somebody must track which ones are — and the record becomes a work
tracker, which is how a thing that outlives its authors turns into a queue nobody
reads.*
## Declines belong here too
**What you considered and did not do, with the reason you did not.**
*Almost nobody writes these, and three weeks later somebody proposes the thing
you already rejected — because the rejection lived somewhere that did not
survive.*
## Where it lives
`.loom/event-log.md`. **A stream, so a file.**
@@ -0,0 +1,28 @@
# Sibling facets
**For any file `x.md`, other things said about it live beside it as
`x.<facet>.md`.**
> **A facet is owned by whoever wrote the facet, never by whoever wrote `x.md`.**
**That is the whole of it.** *It exists so that two parties can both write about
one document without either of them reconciling two versions of it.*
## What a convention using this must decide
**Three things, and it must say all three or its adopters will not know they had
a choice.**
- **What the facet is called.** *`notes`, `usages`, `gaps` — the name is the
contract; a reader recognises the pair on sight.*
- **What fixes `x.md` while the facet exists.** *Something must, or the facet
comes loose from what it is about.*
- **How the pair is released.** *There must be an answer. **A convention that
does not say leaves its adopters stuck**, and they will not notice, because a
question nobody asks looks like a question already answered.*
## Why the facet is never owned by the source's author
**Because the source may not be theirs to change.** *An adopted document belongs
to whoever published it; a facet beside it is the only place a reader has to put
what they think.*
@@ -0,0 +1,68 @@
# Starting one of these in a repository
**Make a `.loom/` directory. Everything else follows from two questions.**
---
## What can a reader not already know?
**That is what you publish**, in `.loom/published/`, as ordinary documents.
**Not what your service does** — its code says that. **Not how Kubernetes or
Kafka or React work** — that is somebody else's documentation, and a copy of it
here goes stale silently while reading as though it were true.
> **Write the delta.** *"The default ingress class publishes to the internet, so
> omitting it is how you leak something."* **Nobody could have guessed that, and
> nothing else here will tell them.**
**A good page ends with how to check it is still true**, and a date. *Nothing
serves the truth about a running system, so the only mechanism is somebody
looking.*
## What do you need that nothing provides?
**That is a gap**, in `.loom/gaps/`.
**The test: could you say whose job it is?** *If yes, it is a claim against them
and belongs with them. If no, it is a gap.*
**Record what you are doing instead***"we are using Docker Hub for now."*
**Forgetting it was provisional is the disease; the provisional answer is fine.**
---
## Then, only if you have something to say
**`.loom/externals/`** — a copy of somebody's document you depend on, with what
you use and what you expected and did not find, beside it. See
[externals](https://git.hypertheory-labs.dev/loom/externals).
**`.loom/venues/`** — choices you made about something with no document to hang
them on. *Kafka, React, HTTP.*
**`.loom/event-log.md`** — decisions, if you are keeping them here.
> **A file that carries no delta should not exist.** *If you do exactly what
> somebody else's document says, write nothing — recording the adoption is
> ceremony.*
## Write a `.loom/README.md`
**Say what is in `published/` and why somebody would want it.** *Link the
documents. One line for the curious about what `.loom` is.*
**Nobody should need to understand any of this to use it** — *they are documents
in a folder, and a reader who ignores the vocabulary entirely should still get
what they came for.*
---
## Look at one instead of reading this
**[`jeffry/homelab-cluster`](https://git.hypertheory-labs.dev/jeffry/homelab-cluster)**
— *six documents, one gap, no decomposition, and a `README` that says what the
root documents are for and what these are for.*
**It is a better answer than this page**, and if the two ever disagree, it is
right.
@@ -0,0 +1,262 @@
# Agreement — the cart
**v0.** Depends on `annotating/v0`.
**How two parties work out what something means before one of them changes it.**
---
## Why
An AI has no good way to say *"I need to know more from you before I do this, and
I suspect you don't know either, and I don't want to make you look stupid."*
Neither do most people — **they assume the lightbulb will arrive once enough
content has been offloaded on them.**
**The cart gives that sentence somewhere to go where it does not read as an
obstruction.**
### Say what you will do if nobody answers
**Every open item states its own fallback.** *"If unanswered I will store an
opaque string and not link it."*
**This is what stops the cart from stalling.** Without it an unanswered question
is an unresolved obligation and work stops; with it, **silence is a usable
reply** and the round can close with items still open in a way both parties
understand.
> **Silence means proceed. It never means dropped.**
*A fallback that quietly becomes the decision is worth knowing about later, so it
is worth being able to find them.*
## The cart is the round, not the folder
**A cart is one bounded exchange.** It starts, it runs, it **converts**, and then
it is gone. The directory is only where a cart happens.
**What ends a cart is size, not time.**
> **A cart converts when it exceeds what two people can hold at once.**
*A whiteboard's virtue is that it is bounded and erasable: when it fills, you have
to decide what matters. Cover the walls and you have removed the only thing it
was doing for you.*
**A cart that cannot convert and a commit that cannot be reviewed are the same
failure.** The sphere got bigger than a head.
## Two files. Only ever two.
**One daily per presence.** One for the owner of the code. One for **everyone
else**, collapsed into a single voice.
**A presence is not a person.** Three people on the outside speak as one, or they
do not speak — and **they do their disagreeing somewhere else first, in their own
cart, where they are the two parties.** Carts nest by party-pair, not by topic.
**There is never a third file.** `jill-daily.md`, `bill-daily.md`,
`im-the-ceo.md` is a chat log; a chat log has no bound; **something with no bound
never converts.**
**The test, when someone wants a third:** *what happens to this file when the
person changes?* **If it transfers, it was a stake. If it dies, it was a person.**
*If someone with authority wants in: have them write it as a venue constraint for
the org instead. **An authority claim entering as a venue fact carries a reason
and can be shown wrong. Entering as a voice in a cart, it cannot.***
## Only dailies get annotated
**Anything else you put in someone's cart is a specimen.**
> **Specimen** — evidence. *Look at this; it may bear on your decision.* It
> belongs to the repository the cart lives in, **and they may throw it away
> without asking you.** Default: discard.
> **Polad** — a candidate artifact, shaped exactly like what it would become,
> staged so you can see whether it fits. Default: apply or discard.
**Promotion is explicit.** A specimen cannot drift into a kept file — **if you
want to keep it, it must be re-shaped as a polad and placed deliberately.**
*Otherwise `docs/` fills with things nobody threw away.*
**A specimen cannot travel between carts.** If it still matters in the next one,
**it is reintroduced, with a fresh reason** — *"this is still here because of line
1,237."* Anything that travels silently accrues standing nobody re-affirmed.
**So responding to a specimen means restating it in your own daily, in your own
words.** That costs precision and buys something better:
> **Pinning a defect in place lets you be exact without having understood.
> Restating surfaces the misunderstanding that pinning hides.**
## Conversion emits two things
**The artifact** — the code, the decision, the change — **and the declines.**
> **A decline is what you considered and did not do, with the reason you did
> not.**
**Almost nobody writes these.** Three weeks later someone proposes the thing you
already rejected, and the rejection is gone because it lived in an annotation
that died with the round.
**A decline needs no file of its own.** It is an entry in whatever durable record
you keep, and **it should say what you believed, not just what you chose** — only
a belief can later be shown wrong.
## Where a cart lives, and the shelf
**A fixed path, and at most two things in it:**
```
{wherever you put carts}/
├── current/
│ ├── name one meaningless word
│ ├── {x}-daily.md
│ └── {y}-daily.md
└── shelf/ remnants of the last round, if any
```
**The directory names are fixed on purpose.** A cart directory you can name
becomes `PROJ-1234` within a month, and then the cart *is* a ticket — with a
status, an assignee, and a backlog behind it. **The name goes in a file, where
nothing navigates by it.**
**Three states, visible in a listing:** no `current/` → no cart. `current/` alone
→ open, last round fully resolved. **`current/` and `shelf/` → carry-forward
pending.**
### Converting: whose act it is, and who decides when
**The cart lives in someone's repository, and it is theirs to reshape and theirs
to convert.** This is not a permission question.
**But converting ends a round the other presence may be standing in**, so:
> **Do not convert while someone is mid-thought. Ask whether they are ready. If
> you convert alone anyway, say so** — and expect them to reintroduce whatever
> they were in the middle of.
*The harm is smaller than it sounds: **the shelf catches what did not resolve**,
so nothing is destroyed. What is taken is the round, and a round is recoverable
by reintroduction with a reason.*
**If a tool offers a convert action, its confirmation should restate this and
nothing more.** A dialog that invents an obligation creates **shadow policy**
something everyone obeys, nobody agreed to, and which cannot be found or argued
with.
### Deciding *when* belongs to a presence that can feel the bound
**The size trigger assumes someone notices a cart getting full. Not every
presence has that instrument.**
An AI in a cart **will not feel it filling and will keep going until told to
stop** — not from carelessness; there is nothing there to feel it with. **Asking
would produce an answer, and the answer would be fabricated.**
> **When one party cannot feel a cost, the decision belongs to the party who
> can.** Sensor placement, not authority.
*The same reason an agent commits and does not push: the consequences of a push
land on people it cannot experience. **Say who owns the conversion call, in your
notes file, before you need it.***
**If neither presence can feel it** — two agents in a cart with no human — **size
is not an available trigger** and you need a different one. We do not have a good
answer for that case.
### A third trigger, from use
**A cart may want to convert when the ground shifts under it**, at any size —
*not* a change of subject, but a change of the coordinate system: the protocol
itself changed, or a venue moved, and the round you are in was framed by
something that no longer holds.
*Reported by the first adopter, from a cart that was small and still wanted to
end. It may be its own trigger or too rare to be worth naming.*
### The shelf
**One live cart. Ever.** *"Let's switch to another cart"* is how nothing ever
converts.
**The shelf holds what did not resolve when the last cart converted**, and
carrying something forward means **re-authoring it**, not moving it.
> **Act as if the shelf is discarded at least once a day.**
**Nothing enforces that, and it is still the useful rule**, because it sorts
without enforcing: **if you would mind losing it overnight, it was never shelf
material.** It is a polad to apply, an entry to log, or a decline to record.
**Which is why no polad ever goes in the shelf.** A polad has exactly two exits —
**applied or discarded** — and forcing that at conversion is the point, because
conversion is when you know most about it.
**If you find mid-round that you are in the wrong cart**, move it out of here
entirely — **not to the shelf**, which is a carry-forward slot with nothing to
carry into. Anything from it that still matters is **reintroduced later with a
fresh reason.** *And it owes one sentence: what the conversation turned out to
be. Not a justification — the name of the round you were actually in.*
## The cart's name
**Give the cart a meaningless name.** A uuid, or `watermelon`. **Meaningless on
purpose**, so it cannot accrue a category.
**Tag durable entries with it.** The tag says exactly one thing:
> **Everything tagged with this name was considered in a single sphere of
> understanding.**
**The cart is gone by then.** You keep the boundary without keeping the container
— so you can later ask *what else was in the room when we decided this*, which is
the question someone who was not there actually asks.
---
## Roles you must cast
**An agreement adopted with these uncast has not been adopted. It has been
filed.**
## Select: Where does a cart live?
- `.loom/cart/`
- somewhere else under `.loom/`
- outside `.loom/` entirely
- there is no directory; carts happen somewhere that is not the repository
## Select: What is the durable record that declines go into?
- an event log file in the repository
- ADRs
- commit trailers
- an issue tracker
- nothing yet — declines are written and lost, knowingly
## Select: What isolates work in progress while a cart is open?
- a local branch
- a worktree
- a separate clone
- nothing; work happens in place
## Question: Who are the two presences, and what happens to each file if that person leaves?
*Name them. If either answer is "the file dies," you have a person and not a
stake, and the cart will grow a third file within a month.*
---
## What we know is unsettled
**Whether size is the only conversion trigger.** Size is what has been observed —
a cart might also want to convert on a **change of subject** at small size, and
there is no example either way.
**Whether `specimen` and `polad` stay two roles.** They are two here because
promotion is a deliberate act. If specimens routinely turn out to persist without
anyone promoting them, that is one role with a discovered property, and this is
wrong.
@@ -0,0 +1,35 @@
# Usages — `cart`
**Three of the four roles are cast below. One is left for you, deliberately.**
| role | cast with |
|---|---|
| where a round lives | `.loom/cart/` |
| what durable record declines go into | `.loom/event-log.md` |
| who decides when the round ends | **loom** |
| what isolates work in progress | **yours to cast** |
**The first two are what every other repository here does**, and differing would
be a surprise rather than a choice. *Change them if you have a reason; you will
not be arguing with anyone.*
**The third is not a preference.** *An agent cannot feel a round filling and will
keep writing until told to stop — **when one party cannot feel a cost, the
decision belongs to the party who can.** Performing the conversion is yours,
because the directory is in your repository.*
## Why the fourth is left open
**A branch, a worktree, a separate clone, or nothing — work happens in place.**
*We cast this one wrong once and it is worth telling you how. **We answered
"nothing; work happens in place"** and wrote down the condition that would falsify
it: a second writer arriving. **The condition that actually arrived was
different** — the work stopped being revertible, because there was nothing yet to
revert to.*
> **The prediction was right that the answer would go stale, and wrong about the
> mechanism.**
*Which is the argument for stating a belief rather than a preference: **ours was
shown wrong in a way we could see**, and a preference could not have been.*
@@ -0,0 +1,72 @@
# Externals
**How a repository records what it depends on, and finds out when it changes.**
## Pull what you use
**You fetch a copy of somebody's document and keep it** at
`.loom/externals/<host>/<path>.md`. *The path says where it came from, so nothing
has to record an origin.*
**Pull what you need to understand, not everything it depends on.** *A document
you fetch may refer to others; follow one when you hit something you do not know.
**Pre-resolving that is how you get a `node_modules`.***
## Two facets beside it
- **`.usages.md`** — *what we use, and **which of our artifacts depend on it***
- **`.gaps.md`** — *what we expected here and did not find*
> **A usage that does not name what it justifies is half a usage.** *"We use the
> tailnet approach" is weak. **"We use the tailnet approach — see
> `manifests/ingress.yaml`" is the whole value**, because it answers the only
> question reconciliation asks.*
## Freshness is a conditional request
**Locked on the publisher's `ETag`, verbatim — never a hash you compute.** *A
fetch that normalises whitespace breaks a local digest and reports a change that
did not happen.*
| | |
|---|---|
| **`304`** | nothing changed |
| **`200`** | changed — the new copy is a candidate, not a replacement |
| **`410`** | gone — follow whatever the response points at |
## Reconciliation runs the other way
**You cannot fold your facets back into their document.** *So the question is
not what do we rewrite here:*
> **Given what changed in theirs, what do we change in ours?**
**The facets usually survive unchanged.** *What moves is the manifests, the
config, the code that a usage named — which is why a usage names them.*
**And gaps reconcile too**, which is the half nobody builds for: *a new version
may have filled one, and nothing will tell you.*
**The new copy replaces the old pair wholesale.** *There is no merging a document
you do not own.*
## Venues, for things you cannot fetch
**`.loom/venues/<name>.md`** — *Kafka, React, HTTP.* **Choices we made about
something with no document to hang them on.**
*If there is a document, hang it there instead: **put a choice where
reconciliation will look for it.***
## Gaps that belong to nobody
**`.loom/gaps/`** — *a need with no owner.* **The test: could you say whose job
this is?** *If yes, it is a claim against them and belongs in their `.gaps.md`.*
> **Moving a file from `gaps/` into an external's `.gaps.md` is how a gap gets
> homed.** *The position is the claim.*
**A gap records the local workaround beside the need.** *"We are using Docker Hub
for now."* **Forgetting it was provisional is the disease; the provisional answer
is fine** — and when the gap is finally homed, every workaround that should be
retired is findable.
+7 -81
View File
@@ -1,85 +1,11 @@
# loom-cli # loom-cli
**Not built yet.** *This is the spec, written while it was fresh.* **Not built.** *A small tool for the operations a person should not do by hand:
fetch a document you depend on, and find out when it changed.*
**A small tool for the operations a person should not do by hand:** *fetch a **Start in the cart:** [`.loom/cart/current/`](.loom/cart/current/) — *a round is
document you depend on, and find out when it changed.* open, the conventions are already fetched under `.loom/externals/`, and the spec
is a **specimen**, which means you may discard it.*
--- **`bedrock` and `externals` are not discardable.** *Read them as given; argue with
everything else.*
## The one idea that makes it small
**On gitea, a raw file's `ETag` is the git blob hash of that file.** *Verified on
both a public and a private repository — the header and `git rev-parse` return the
same value.*
> **So there is nothing to record.** *`git hash-object <the local copy>` **is** the
> lock. Compare it to the remote's `ETag` and you have your answer.*
**No lock file, no state, no `pull` metadata to drift.** *And it catches a case we
had not considered: **if somebody edits the local copy, the hash stops matching
and `check` reports it** — which is correct, because an adopted copy that has been
edited is no longer a copy of anything.*
## Commands
### `loom check`
**For every file under `.loom/externals/`:** *compute its hash, `HEAD` its source,
compare.*
| result | means |
|---|---|
| **hashes match** | nothing changed |
| **hashes differ** | **upstream moved** — the new copy is a candidate, not a replacement |
| **local hash differs from what the remote had at last fetch** | *somebody edited a copy that is not ours* |
| **`404`** | **gone, or access lost — these are indistinguishable over HTTP.** *Say so; do not pick one.* |
**Report. Do not fix.** *A changed external becomes a polad in the cart, and
somebody decides.*
### `loom pull <url>`
**Fetch one document into `.loom/externals/<host>/<path>`.** *The path mirrors the
URL, so nothing has to record an origin.*
**Do not create a `.usages.md`.** *An empty facet asserts that we have something
to say and we do not — **a file that carries no delta should not exist.***
### `loom publish <path>`
**Verify a document in `.loom/published/` is fetchable by somebody who is not
you.** *One anonymous request.*
> **Publishing is not an act you can complete alone.** *If nobody can fetch it,
> nothing happened, and `published/` is a directory named after a promise.*
### `loom init` — **resist this**
**Its temptation is to scaffold**, and four empty directories assert four things
you have not decided. *The useful version creates `.loom/README.md` and asks one
question.* **Creating the tree is not a feature.**
## Credentials
**Read over HTTP with a read-only token. Write over ssh.** *Split by operation,
not by whether a repository is public.*
**Name the token for its scope — `loom-read`.** *A token cannot be asked what it
may do: `/api/v1/repos/…` reports the **account's** rights, not the token's.
**Scope is set once at creation and invisible afterwards, so the name is the only
record.***
*Token in `~`, never in a repository.*
---
## Why so little
*Every act in the model is a file in somebody's repository.* **Publishing is
writing one; adopting is fetching a URL; reporting a gap is writing one; homing it
is an `mv`.** *Nothing sends a service a request.*
**So this is a fetcher and a comparator, and it should stay one.** *If it starts
holding state, ask what it knows that no repository knows — that is the thing that
will rot.*