diff --git a/.loom/cart/current/claude-substrate-osprey.md b/.loom/cart/current/claude-substrate-osprey.md new file mode 100644 index 0000000..1f5de6b --- /dev/null +++ b/.loom/cart/current/claude-substrate-osprey.md @@ -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.* diff --git a/.loom/cart/current/name b/.loom/cart/current/name new file mode 100644 index 0000000..1c1ba7e --- /dev/null +++ b/.loom/cart/current/name @@ -0,0 +1 @@ +osprey diff --git a/.loom/cart/current/specimens/proposed-spec.md b/.loom/cart/current/specimens/proposed-spec.md new file mode 100644 index 0000000..163c9d7 --- /dev/null +++ b/.loom/cart/current/specimens/proposed-spec.md @@ -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 ` **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 ` + +**Fetch one document into `.loom/externals//`.** *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 ` + +**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.* diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/annotating/annotating.md b/.loom/externals/git.hypertheory-labs.dev/loom/annotating/annotating.md new file mode 100644 index 0000000..9b1f7e6 --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/annotating/annotating.md @@ -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 24–48 — jeff +> with a cap of thirty seconds + +Where did thirty come from? + +## lines 40–47 — 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. diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/loom-directory.md b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/loom-directory.md new file mode 100644 index 0000000..ee0f49c --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/loom-directory.md @@ -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.** diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/publication.md b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/publication.md new file mode 100644 index 0000000..eb0cb63 --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/publication.md @@ -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.* diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md new file mode 100644 index 0000000..970d4b4 --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md @@ -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.** diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md new file mode 100644 index 0000000..a46446a --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md @@ -0,0 +1,28 @@ +# Sibling facets + +**For any file `x.md`, other things said about it live beside it as +`x..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.* diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/starting.md b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/starting.md new file mode 100644 index 0000000..e5fba07 --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/bedrock/starting.md @@ -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. diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.md b/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.md new file mode 100644 index 0000000..46a2d5d --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.md @@ -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. diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md b/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md new file mode 100644 index 0000000..b0a1a36 --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md @@ -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.* diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.md b/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.md new file mode 100644 index 0000000..7a564ad --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.md @@ -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//.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/.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. diff --git a/README.md b/README.md index 163c9d7..abe1985 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,11 @@ # 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 -document you depend on, and find out when it changed.* +**Start in the cart:** [`.loom/cart/current/`](.loom/cart/current/) — *a round is +open, the conventions are already fetched under `.loom/externals/`, and the spec +is a **specimen**, which means you may discard it.* ---- - -## 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 ` **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 ` - -**Fetch one document into `.loom/externals//`.** *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 ` - -**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.* +**`bedrock` and `externals` are not discardable.** *Read them as given; argue with +everything else.*