Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
289c654706 |
@@ -1,4 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
.astro/
|
|
||||||
.loom/cart/
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# `.loom` — loom/docs
|
|
||||||
|
|
||||||
**This repository publishes nothing.** *It is a reader.*
|
|
||||||
|
|
||||||
**What is here is `externals/`** — a copy of every document the `loom`
|
|
||||||
organisation publishes, locked to the version it was fetched at. *The site at
|
|
||||||
[loom.hypertheory-labs.dev](https://loom.hypertheory-labs.dev) is those copies,
|
|
||||||
rendered, plus four hand-written section fronts.*
|
|
||||||
|
|
||||||
**Nothing in `externals/` is edited here.** *A copy that differs from its source
|
|
||||||
is the only evidence that something moved while nobody was watching, so editing
|
|
||||||
one destroys the mechanism this repository exists to demonstrate.*
|
|
||||||
|
|
||||||
*What we hold about a document goes in a facet beside it — `.usages.md` for what
|
|
||||||
of ours depends on it, `.gaps.md` for what we expected and did not find.*
|
|
||||||
Vendored
-15
@@ -1,15 +0,0 @@
|
|||||||
# loomctl locks — one record per adopted document.
|
|
||||||
# path<TAB>url<TAB>etag[<TAB>visibility]
|
|
||||||
# The url is resolved: a short form would follow whatever the default branch
|
|
||||||
# is at the time you ask. visibility is what the source could be read as when
|
|
||||||
# it was fetched, because that is checked once and the copy is durable.
|
|
||||||
git.hypertheory-labs.dev/loom/annotating/annotating.md https://git.hypertheory-labs.dev/loom/annotating/raw/branch/main/.loom/published/annotating.md "9b1f7e6ca92f2339b2d433686c27845362944bdb" public
|
|
||||||
git.hypertheory-labs.dev/loom/bedrock/loom-directory.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/loom-directory.md "ee0f49cb900c0812678061971194325d9cba366a" public
|
|
||||||
git.hypertheory-labs.dev/loom/bedrock/publication.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md "c0d9231aa8eed3041c60b72da5735585457fbc90" public
|
|
||||||
git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/recording-decisions.md "d190eeae7929b9a99ecf09211d94e7e5b32635bd" public
|
|
||||||
git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md "a46446a34ccb8bfc533d3cce19f4c88548c4fa04" public
|
|
||||||
git.hypertheory-labs.dev/loom/bedrock/starting.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md "b9eefba0f4668a496ccfc6a1377277f0721456d2" public
|
|
||||||
git.hypertheory-labs.dev/loom/bedrock/venues.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/venues.md "35e940195e2ba9a30b4b4dc81a57f9383c709f64" public
|
|
||||||
git.hypertheory-labs.dev/loom/cart/cart.md https://git.hypertheory-labs.dev/loom/cart/raw/branch/main/.loom/published/cart.md "15331f1a9cc81bf61a44830cfbb7c274f4c2b119" public
|
|
||||||
git.hypertheory-labs.dev/loom/externals/externals.md https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md "772c67f460f0ff3b9563aee9b38a64c0f3423dbd"
|
|
||||||
git.hypertheory-labs.dev/loom/loom-cli/guarantees.md https://git.hypertheory-labs.dev/loom/loom-cli/raw/branch/main/.loom/published/guarantees.md "e8c697ea61df47838f38af7353e29d9ae3f7070c"
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
# 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.
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
# Usages — `annotating`
|
|
||||||
|
|
||||||
| what the document says | ours |
|
|
||||||
|---|---|
|
|
||||||
| notes freeze the source until they dissolve | `src/content/docs/annotating/index.mdx` |
|
|
||||||
| dissolving is all-or-nothing | *stated as the move people get wrong* |
|
|
||||||
|
|
||||||
**The guide states the annotate-versus-correspond discriminator**, *which is
|
|
||||||
**not in this document** — it was settled in a cart round and lives in
|
|
||||||
`cart.gaps.md` in another repository.* **So this page holds a claim its source
|
|
||||||
does not make**, *and no check will ever catch that.* **It is here because
|
|
||||||
somebody would otherwise have to rediscover it, which took us a round.**
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 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.**
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
## Publishing is not an act you can complete alone
|
|
||||||
|
|
||||||
**A file in `.loom/published/` that its intended readers cannot fetch is not
|
|
||||||
published.** *The move is the act, but the act is not finished until somebody who
|
|
||||||
is not you can perform the read.*
|
|
||||||
|
|
||||||
> **The failure is invisible from the inside.** *Every check available to the
|
|
||||||
> publisher passes — the file is there, the commit landed, the path is right.*
|
|
||||||
> **Only a reader can detect it, and a reader who cannot fetch it does not know
|
|
||||||
> the document exists to ask about.**
|
|
||||||
|
|
||||||
*The commonest form is a document that answers "why can you not see this?" and is
|
|
||||||
published **inside** the thing that cannot be seen.*
|
|
||||||
|
|
||||||
**So publication has a visibility, and it is a property of the act rather than of
|
|
||||||
the file.** *"Others" is not "everyone".* **Naming who the readers are is part of
|
|
||||||
publishing**, because a repository that depends on this one has to know whether it
|
|
||||||
is inside that set — *and because a reader who copies a document out inherits the
|
|
||||||
copy without inheriting the visibility it was published under.*
|
|
||||||
|
|
||||||
**The test is a fetch you did not perform.**
|
|
||||||
|
|
||||||
## Doing it
|
|
||||||
|
|
||||||
1. **Decide it is a contract and not a detail.** *This is the whole decision and
|
|
||||||
the rest is mechanics.*
|
|
||||||
2. **Move the file into `.loom/published/`.** *One commit, so the diff carries it.*
|
|
||||||
3. **Say who the readers are** — *if it is not obvious from where it landed.*
|
|
||||||
4. **Fetch it.** *From somewhere that is not your machine and not your account.*
|
|
||||||
5. **Record the decision**, if you keep a log.
|
|
||||||
|
|
||||||
**Step 4 is the one that gets skipped and the only one that can fail.** *Steps 1
|
|
||||||
to 3 are things you do; step 4 is a fact about the world that you find out.*
|
|
||||||
|
|
||||||
*Then say what it depends on. A published document that quietly requires a private
|
|
||||||
one has moved the problem rather than solved it.*
|
|
||||||
|
|
||||||
## 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.*
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Usages — `publication`
|
|
||||||
|
|
||||||
| what the document says | ours |
|
|
||||||
|---|---|
|
|
||||||
| the five-step procedure, and that step 4 is the one that fails | `src/content/docs/bedrock/index.mdx` — *the whole second half* |
|
|
||||||
| publishing is a change of kind, not of status | *why this repository publishes nothing: it only reads* |
|
|
||||||
|
|
||||||
**We restate the count — "five steps", "the fourth" — which is the one thing a
|
|
||||||
guide should not do.** *Kept deliberately, because the ordinal is the point of
|
|
||||||
the passage.* **If a step is inserted, the guide is wrong and the banner will
|
|
||||||
fire**, *which is the case this mechanism exists for.*
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# 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.*
|
|
||||||
|
|
||||||
## Compaction, and the entries it may never touch
|
|
||||||
|
|
||||||
**"Never revised" and "somebody must be able to read it" both hold, and far
|
|
||||||
enough out they conflict** — *a log nobody can get through has stopped being a
|
|
||||||
record.*
|
|
||||||
|
|
||||||
**Compaction is allowed. Dropping an entry is not the same as revising one**, and
|
|
||||||
the uncompacted log is in git history, which is where a reader who needs it goes.
|
|
||||||
|
|
||||||
> **An entry may be dropped when a competent reader could recover it by reading
|
|
||||||
> the artifact.**
|
|
||||||
>
|
|
||||||
> **An entry may never be dropped when it records a decline, a measurement, a
|
|
||||||
> belief that was shown wrong, or one entry superseding another.**
|
|
||||||
|
|
||||||
*Which means **compaction is safe in exactly the cases where the entry was
|
|
||||||
redundant with the artifact, and unsafe in exactly the cases the log exists
|
|
||||||
for.*** **The four protected kinds are the ones with no other home:** *the code
|
|
||||||
shows what was built and can never show what was refused, what was measured, what
|
|
||||||
turned out false, or that somebody changed their mind.*
|
|
||||||
|
|
||||||
**A compaction is itself a decision, and gets an entry.**
|
|
||||||
|
|
||||||
## Where it lives
|
|
||||||
|
|
||||||
`.loom/event-log.md`. **A stream, so a file.**
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# 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.*
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# 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.* **It describes what a home cluster
|
|
||||||
offers to somebody deciding whether to put something on it**, and a reader who
|
|
||||||
has never heard of any of this still gets what they came for.
|
|
||||||
|
|
||||||
**It was private, and being unable to link to it is what made the split obvious.**
|
|
||||||
*The operational tree is a version inventory and a target list; the pages telling
|
|
||||||
somebody what to decide are neither.* **So the manifests moved to a private
|
|
||||||
sibling and the documents stayed** — *and the documents got better for being
|
|
||||||
written to a stranger, which was not the reason for doing it.*
|
|
||||||
|
|
||||||
> **A public page naming a private thing as its canonical answer is worse than no
|
|
||||||
> example**, and this page did exactly that until somebody measured it.
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Venues
|
|
||||||
|
|
||||||
**`.loom/venues/<name>.md` — a choice you made about something that has no
|
|
||||||
document of its own to hang it on.**
|
|
||||||
|
|
||||||
*Kafka. React. HTTP. The organisation you work for.* **Things that constrain what
|
|
||||||
you build without ever being invoked, and without publishing anything at you that
|
|
||||||
you could adopt.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Venue and external are two questions, not two kinds of thing
|
|
||||||
|
|
||||||
**`external` is *who published it*. `venue` is *whether you had to decide
|
|
||||||
anything about it*.** *They are orthogonal. A thing may be both, either, or
|
|
||||||
neither.*
|
|
||||||
|
|
||||||
| | published at us | we recorded a choice | files |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Kafka, React, HTTP | yes, by the world | yes — *protobuf for schema* | venue only |
|
|
||||||
| a service another team owns | yes, by them | not yet — the use is in the code | external only |
|
|
||||||
| a cluster your own org publishes | yes | yes | both |
|
|
||||||
| a library you use exactly as documented | yes | no | neither |
|
|
||||||
|
|
||||||
**Both files are governed by the same rule, applied to two different ambients:**
|
|
||||||
|
|
||||||
> **An external entry exists when the reader cannot be assumed to know the
|
|
||||||
> thing.** *A venue file exists when you made a choice worth recording.*
|
|
||||||
|
|
||||||
**Nobody needs telling what Kafka is, so there is no external entry** — *but "we
|
|
||||||
use protobuf for schema" is a choice, so there is a venue file.* **Nobody outside
|
|
||||||
can know what your internal API is for, so there is an external entry** — *and
|
|
||||||
until using it requires an exception or a preference, there is nothing to
|
|
||||||
accommodate and no venue file, because the actual use is in the code.*
|
|
||||||
|
|
||||||
*This is why sorting things into two bins does not work and people keep trying.
|
|
||||||
The bins were never the problem. The question was.*
|
|
||||||
|
|
||||||
## A venue file is an account of your own decisions
|
|
||||||
|
|
||||||
**Which is what makes it local by construction.** *An external is a copy of
|
|
||||||
somebody's document and could in principle be centralised.* **A venue file has no
|
|
||||||
upstream** — *there is nobody to reconcile with, because the only party who
|
|
||||||
decided anything was you.*
|
|
||||||
|
|
||||||
**So it does not go stale the way a copy does.** *It goes stale when you change
|
|
||||||
your mind and do not say so*, which no fetch will ever detect.
|
|
||||||
|
|
||||||
> **A venue file that carries no delta should not exist.** *If you use the thing
|
|
||||||
> exactly as documented, write nothing.* **Recording the adoption is ceremony.**
|
|
||||||
|
|
||||||
## If there is a document, hang the choice on the document
|
|
||||||
|
|
||||||
**Put a choice where reconciliation will look for it.** *When the thing you
|
|
||||||
decided about publishes something you have adopted, the choice belongs in the
|
|
||||||
`.usages.md` beside that copy and not in a venue file* — **because that is the
|
|
||||||
file somebody opens when the document moves**, *and a venue file is not.*
|
|
||||||
|
|
||||||
*The venue file is for what is left: the constraints of a thing that publishes
|
|
||||||
nothing you can hold.*
|
|
||||||
|
|
||||||
## A venue is where an authority claim belongs
|
|
||||||
|
|
||||||
**Because a venue fact carries a reason and can therefore be shown wrong.**
|
|
||||||
|
|
||||||
*The alternative is worse and common: somebody with authority enters a
|
|
||||||
conversation as a voice, and "we do it this way" arrives with no reason attached
|
|
||||||
and nothing that could contradict it.* **Written as a venue constraint, the same
|
|
||||||
instruction becomes a claim about the world**, *and a claim about the world is
|
|
||||||
something a later reader can discover was false.*
|
|
||||||
|
|
||||||
## Checking it is still true
|
|
||||||
|
|
||||||
**Nobody will tell you.** *There is no publisher to ask, no `ETag`, and no
|
|
||||||
conditional request that will ever come back `200`.*
|
|
||||||
|
|
||||||
**So a venue file needs what a published document needs: a date, and the reason
|
|
||||||
underneath the choice.** *The reason is the whole of it* — **"we use protobuf for
|
|
||||||
schema" is unfalsifiable a year later; "we use protobuf because the consumers are
|
|
||||||
in three languages and JSON schema drift was costing us a release a quarter" can
|
|
||||||
be checked against a world that may have changed.**
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
# Agreement — the cart
|
|
||||||
|
|
||||||
**v2.** 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.**
|
|
||||||
|
|
||||||
## Converting destroys, so extract first
|
|
||||||
|
|
||||||
**The cart is not committed. So conversion does not archive it — it deletes it.**
|
|
||||||
*Every earlier version of this agreement was wrong about that by accident: a
|
|
||||||
committed cart left its dailies in history, and "gone" was only ever true of the
|
|
||||||
tree.*
|
|
||||||
|
|
||||||
> **At conversion the cart is the only copy of everything in it.** *The dailies,
|
|
||||||
> the annotations, the answers to every prompt, and the write-ahead log.*
|
|
||||||
|
|
||||||
**So before converting, extract:**
|
|
||||||
|
|
||||||
- **every answer to a `Question`, `Select` or `Affirm`** — *an answer lands in a
|
|
||||||
notes file, a notes file dies at dissolve, and an untracked cart has no history
|
|
||||||
to fall back on.* **A prompt answered and not extracted was never answered.**
|
|
||||||
- **every write-ahead entry that survived** — *promote or discard, and both are
|
|
||||||
acts.*
|
|
||||||
- **every fallback that quietly became the decision** — *say so in the entry, in
|
|
||||||
those words.*
|
|
||||||
|
|
||||||
**Staging as you go turns conversion into a move rather than a rewrite.** *Keep
|
|
||||||
the write-ahead log current during the round and there is nothing to reconstruct
|
|
||||||
at the end, which is when you have least appetite for it.*
|
|
||||||
|
|
||||||
*Two rules in this agreement now both push toward loss — **act as if the shelf is
|
|
||||||
discarded daily**, and **the cart is not committed.** They are both right and
|
|
||||||
together they mean **nothing in a cart is safe by default.***
|
|
||||||
|
|
||||||
## 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 — **which must outlive the cart**, *and therefore cannot be inside it* — and **it should say what you believed, not just what you chose** — only
|
|
||||||
a belief can later be shown wrong.
|
|
||||||
|
|
||||||
## The cart is local, and that is what keeps it to two files
|
|
||||||
|
|
||||||
**A cart is two developers working side by side.** *Everything else — the wider
|
|
||||||
team, the people who need to know, the thing that has to be tracked — is issues,
|
|
||||||
chat, whatever you already have.* **This is not that channel and it does not scale
|
|
||||||
into one.**
|
|
||||||
|
|
||||||
> **So the cart is not committed.** *It lives in the working tree of the machine
|
|
||||||
> the two presences share, and `.loom/cart/` is ignored by version control.*
|
|
||||||
|
|
||||||
**The reason is not tidiness. A committed cart grows a third file by itself.**
|
|
||||||
*The rule above defends against somebody asking for one; **version control does
|
|
||||||
not require anybody to ask.*** *Anyone who can clone can add `joe-rose.md`, and
|
|
||||||
then `sue-rose.md`, and the agreement's defence — **what happens to this file when
|
|
||||||
the person changes?** — is never invoked, because nobody ever had the
|
|
||||||
conversation.*
|
|
||||||
|
|
||||||
**This is also what makes a round actually end.** *Committed, a cart is gone from
|
|
||||||
the tree and permanent in history — **so "gone" means "no longer live" and
|
|
||||||
negotiation stays quotable forever.*** **Ignored, gone means gone**, which is what
|
|
||||||
the round dying was for.
|
|
||||||
|
|
||||||
*The cost, stated: **two presences who do not share a filesystem cannot use a
|
|
||||||
cart.*** *That is a real limit and it is the right one — if you need a medium
|
|
||||||
between machines, you need the other channel, and reaching for a cart there is
|
|
||||||
how it becomes a chat log.*
|
|
||||||
|
|
||||||
## What is not yet a decision goes in the write-ahead log
|
|
||||||
|
|
||||||
**A round produces things that are neither questions nor decisions:** *something
|
|
||||||
observed, something that may turn out to be noise, something you would kick
|
|
||||||
yourself for losing and cannot yet justify writing down.*
|
|
||||||
|
|
||||||
**`event-log.wal.md`, in the cart.** *Findings, not decisions.* **Nothing in it is
|
|
||||||
durable and nothing in it has been decided.**
|
|
||||||
|
|
||||||
**At conversion, each entry either becomes an entry in the durable record or is
|
|
||||||
discarded.** *Same two exits as a polad, and for the same reason: **conversion is
|
|
||||||
when you know most about it.***
|
|
||||||
|
|
||||||
> **Write the reason it is not yet an entry.** *An observation you cannot justify
|
|
||||||
> promoting is worth keeping; **one you have not said why you are hesitant about
|
|
||||||
> will be promoted by whoever finds it, on the strength of it having been written
|
|
||||||
> down.***
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Usages — `cart`
|
|
||||||
|
|
||||||
| what the document says | ours |
|
|
||||||
|---|---|
|
|
||||||
| a round is two presences, two files, and a name in a file | `src/content/docs/cart/index.mdx` — *the worked example is a real round* |
|
|
||||||
| the cart is not committed; converting destroys | *the example says so, and it is the part most likely to go stale* |
|
|
||||||
| durable entries are tagged with the round name | *the example ends on a tagged entry* |
|
|
||||||
|
|
||||||
**The guide quotes a `Select` verbatim from a real cart.** *If the shape of an
|
|
||||||
open item changes — the fallback line especially — the quote is wrong and the
|
|
||||||
banner will not say so*, **because the banner fires on the whole document
|
|
||||||
moving, not on the sentence we depended on.**
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
# Externals
|
|
||||||
|
|
||||||
**How a repository records what it depends on, and finds out when it changes.**
|
|
||||||
|
|
||||||
## Pull what you use
|
|
||||||
|
|
||||||
**You fetch a copy of somebody's document and keep it** at
|
|
||||||
`.loom/externals/<host>/<path>.md`.
|
|
||||||
|
|
||||||
> ~~*The path says where it came from, so nothing has to record an origin.*~~
|
|
||||||
> **This was false and it was load-bearing.** *A stored path is short enough to
|
|
||||||
> read and therefore too short to resolve: it drops the host's routing, the
|
|
||||||
> branch, and — worst — the `published/` segment, **which is the whole contract.***
|
|
||||||
|
|
||||||
**The path is for a person. The origin is recorded in the lock**, resolved: host,
|
|
||||||
route, branch, and full path.
|
|
||||||
|
|
||||||
*Record the **resolved** URL and not the short form. A host may redirect a short
|
|
||||||
form to whatever the default branch is **at the time you ask** — so a lock holding
|
|
||||||
one is locked to a moving target, and a rename of the branch reports as a change
|
|
||||||
in the document.*
|
|
||||||
|
|
||||||
**Pull what you need to understand, not everything it depends on.** *A document
|
|
||||||
you fetch may refer to others; follow one when you hit something you do not know.
|
|
||||||
**Pre-resolving that is how you get a `node_modules`.***
|
|
||||||
|
|
||||||
### Confidentiality does not travel with the copy
|
|
||||||
|
|
||||||
**Adopting is copying.** *So a document from a repository somebody may not read
|
|
||||||
ends up in a repository they may* — **and the publisher loses control of it at the
|
|
||||||
moment of adoption**, because the copy's visibility is governed by your repository
|
|
||||||
and not by theirs.
|
|
||||||
|
|
||||||
> **Do not adopt from a source less readable than the repository you are adopting
|
|
||||||
> into.** *If you may read it and your readers may not, copying it publishes it.*
|
|
||||||
|
|
||||||
**Two ways out, and the second is better when it is available.**
|
|
||||||
|
|
||||||
**Reference-only** — *record the lock and fetch on demand, keep no copy.* **You
|
|
||||||
give up reading it offline**, which is most of what a copy is for, *and you keep
|
|
||||||
the dependency recorded and checkable.*
|
|
||||||
|
|
||||||
**Ask them to publish** — *the thing you needed was almost certainly not the
|
|
||||||
confidential part.* **A repository that must stay private can still have a public
|
|
||||||
sibling that publishes**, and the split is usually along a line that already
|
|
||||||
exists: **the operational tree is what is sensitive; the pages telling somebody
|
|
||||||
what to decide are not.**
|
|
||||||
|
|
||||||
## Two facets beside it
|
|
||||||
|
|
||||||
- **`.usages.md`** — *what we use, and **which of our artifacts depend on it***
|
|
||||||
- **`.gaps.md`** — *what we expected here and did not find*
|
|
||||||
|
|
||||||
> **A usage that does not name what it justifies is half a usage.** *"We use the
|
|
||||||
> tailnet approach" is weak. **"We use the tailnet approach — see
|
|
||||||
> `manifests/ingress.yaml`" is the whole value**, because it answers the only
|
|
||||||
> question reconciliation asks.*
|
|
||||||
|
|
||||||
## What a lock holds
|
|
||||||
|
|
||||||
**One record per adopted document, in `.loom/externals/.locks`:**
|
|
||||||
|
|
||||||
- **where it was fetched from** — *resolved, as above*
|
|
||||||
- **the publisher's `ETag`** — *verbatim*
|
|
||||||
- **the visibility the source could be read at, when it was fetched**
|
|
||||||
|
|
||||||
**It is committed**, because the thing it locks is committed, and *a lock that
|
|
||||||
travels separately from what it locks is the drift this is meant to prevent.*
|
|
||||||
|
|
||||||
> **A document with no lock is not broken; it is unlocked.** *Report it and fetch
|
|
||||||
> again.* **Do not adopt whatever the remote currently serves as the lock** — that
|
|
||||||
> asserts your copy is the one being served, which is the thing you were going to
|
|
||||||
> check.
|
|
||||||
|
|
||||||
### Why visibility is in the lock and not left to the moment
|
|
||||||
|
|
||||||
**Access is checked once and the copy is durable.** *A token gets you the read;
|
|
||||||
nothing revisits it afterwards.* **So the adoption's legitimacy rests on the
|
|
||||||
relative visibility of two repositories — and that is changeable by a checkbox, a
|
|
||||||
year later, by somebody who never saw the adoption.**
|
|
||||||
|
|
||||||
> **Record `public` or `not-public`. Never `private`.** *An anonymous request can
|
|
||||||
> tell you a repository is not public. It cannot tell two repositories private to
|
|
||||||
> **different people** apart* — **which is exactly the case where private into
|
|
||||||
> private genuinely widens access.** *Record the word you can verify.*
|
|
||||||
|
|
||||||
**Checking it costs one request per run, not one per document**, *because the only
|
|
||||||
thing that must be current is **your own** visibility.* **Re-check a source only
|
|
||||||
when the alarm would fire** — *you are public, it was recorded not-public* — *and
|
|
||||||
if it has since gone public, update the lock and say nothing.*
|
|
||||||
|
|
||||||
*A stored visibility decays in both directions, which is why the alarm re-checks
|
|
||||||
rather than trusting the record.*
|
|
||||||
|
|
||||||
## Freshness is a conditional request
|
|
||||||
|
|
||||||
**Locked on the publisher's `ETag`, verbatim — never a hash you compute.** *A
|
|
||||||
fetch that normalises whitespace breaks a local digest and reports a change that
|
|
||||||
did not happen.*
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| **`304`** | nothing changed |
|
|
||||||
| **`200`** | changed — the new copy is a candidate, not a replacement |
|
|
||||||
| **`410`** | gone — follow whatever the response points at |
|
|
||||||
| **`404`** | **unresolved.** *Report both readings; do not pick one* |
|
|
||||||
|
|
||||||
**`404` is two different answers wearing one status.** *The document was
|
|
||||||
withdrawn, or you no longer have access — **and over HTTP they are
|
|
||||||
indistinguishable**, because a host that distinguished them would leak the
|
|
||||||
existence of things you may not see.*
|
|
||||||
|
|
||||||
> **Say both. Do not guess.** *They want different actions — re-pull elsewhere,
|
|
||||||
> versus ask somebody for access — and a tool that picks one will be wrong half
|
|
||||||
> the time silently.*
|
|
||||||
|
|
||||||
*Over ssh they **are** distinguishable — permission denied against repository not
|
|
||||||
found — so a client that has both transports should say which it used.*
|
|
||||||
|
|
||||||
## Reconciliation runs the other way
|
|
||||||
|
|
||||||
**You cannot fold your facets back into their document.** *So the question is
|
|
||||||
not what do we rewrite here:*
|
|
||||||
|
|
||||||
> **Given what changed in theirs, what do we change in ours?**
|
|
||||||
|
|
||||||
**The facets usually survive unchanged.** *What moves is the manifests, the
|
|
||||||
config, the code that a usage named — which is why a usage names them.*
|
|
||||||
|
|
||||||
**And gaps reconcile too**, which is the half nobody builds for: *a new version
|
|
||||||
may have filled one, and nothing will tell you.*
|
|
||||||
|
|
||||||
> **A facet describes the local pair.** *A gap is true of **the copy you hold**,
|
|
||||||
> not of the document upstream — so a publisher fixing their end does not close
|
|
||||||
> it.* **It closes when you fetch the new copy and replace the pair**, and until
|
|
||||||
> then it is still true of what is in your tree.
|
|
||||||
|
|
||||||
**What survives a closed gap is not the gap. It is what the gap justified.** *If
|
|
||||||
you recorded a local workaround, ask whether it is retired or merely no longer
|
|
||||||
provisional* — **the second is the common case and it is invisible in the code**,
|
|
||||||
which is why it is an entry in your own log. *Somebody inheriting your workaround
|
|
||||||
will go looking for the gap that justified it, and find nothing.*
|
|
||||||
|
|
||||||
*A closed gap is **not** a decline. A decline is what you considered and did not
|
|
||||||
do; a closed gap is what you needed and got.*
|
|
||||||
|
|
||||||
**The new copy replaces the old pair wholesale.** *There is no merging a document
|
|
||||||
you do not own.*
|
|
||||||
|
|
||||||
## A choice about something you adopted does not go in a venue file
|
|
||||||
|
|
||||||
**Put a choice where reconciliation will look for it.** *When the thing you
|
|
||||||
decided about publishes a document you hold a copy of, the choice belongs in the
|
|
||||||
`.usages.md` beside that copy* — **because that is the file somebody opens when
|
|
||||||
the document moves.**
|
|
||||||
|
|
||||||
*Venues are for what is left: things that constrain you without publishing
|
|
||||||
anything you could adopt.* **See
|
|
||||||
[venues](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/venues.md).**
|
|
||||||
|
|
||||||
## Gaps that belong to nobody
|
|
||||||
|
|
||||||
**`.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.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Usages — `externals`
|
|
||||||
|
|
||||||
| what the document says | ours |
|
|
||||||
|---|---|
|
|
||||||
| a copy, a lock, and facets beside it | **the entire repository** — every reference page is an adopted copy |
|
|
||||||
| the lock is the publisher's etag, verbatim | `scripts/generate.mjs` reads it and never computes one |
|
|
||||||
| do not adopt from a source less readable than yours | *why this site can hold only public documents* — `README.md` |
|
|
||||||
| the lock's fourth column is visibility | `src/content/docs/externals/index.mdx` — *quotes `public` / `not-public`* |
|
|
||||||
|
|
||||||
**The guide reproduces the lock's column order.** *If a fifth column is added,
|
|
||||||
the code tolerates it and the guide is wrong.*
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# What `loomctl` guarantees
|
|
||||||
|
|
||||||
**Not what it does — `--help` says that, and the commands will change.** *This is
|
|
||||||
what will keep being true while they do.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## It grants no access, and records where the copy came from
|
|
||||||
|
|
||||||
**`loomctl` reads what your credentials already let you read.** *Everything it
|
|
||||||
does is possible with copy and paste.* **It is a mast, not a lock** — *the point
|
|
||||||
is to make the wrong thing deliberate, not impossible.*
|
|
||||||
|
|
||||||
> **What it adds over a paste is provenance.** *A pasted document cannot answer
|
|
||||||
> "where did this come from, and were we allowed to have it" — not because the
|
|
||||||
> question is hard, but because the evidence is gone.*
|
|
||||||
|
|
||||||
## It never writes over the network
|
|
||||||
|
|
||||||
**No push, no publish, no `POST`, no token that needs write scope.** *Every byte
|
|
||||||
it writes is a file in your working tree.* **Committing and pushing are yours**,
|
|
||||||
*because the consequences of a push land on people a tool cannot experience.*
|
|
||||||
|
|
||||||
**A credential given to `loomctl` should never carry write scope**, *and if one
|
|
||||||
does, nothing here will use it.*
|
|
||||||
|
|
||||||
## Freshness is a conditional request against the publisher's `ETag`
|
|
||||||
|
|
||||||
**Stored verbatim, opaque, never a hash we compute.** *A fetch that normalises
|
|
||||||
anything breaks a local digest and reports a change that did not happen.*
|
|
||||||
|
|
||||||
*This is `externals`' rule and `loomctl` is a second holder of it. **It is stated
|
|
||||||
here so that the tool holding it is a fact somebody can find**, and so that
|
|
||||||
breaking it is visible rather than silent* — **a specimen was written in this
|
|
||||||
repository that computed a hash instead, and it was wrong within a day.**
|
|
||||||
|
|
||||||
**The URL in a lock is resolved.** *A short form follows whatever the default
|
|
||||||
branch is at the time you ask, so a branch rename would report as a change in the
|
|
||||||
document.*
|
|
||||||
|
|
||||||
## It reports; it does not repair
|
|
||||||
|
|
||||||
**Nothing is overwritten.** *A document that moved upstream is written to a
|
|
||||||
staging area as a candidate, and taking it is a separate act.* **A local copy
|
|
||||||
that differs from what the publisher serves is left alone** — *it is the only
|
|
||||||
evidence that something changed while nothing was watching.*
|
|
||||||
|
|
||||||
**`add` adopts what is absent and refuses what is already adopted.** *A document
|
|
||||||
that is present but unlocked is locked only when the bytes are identical to what
|
|
||||||
the publisher serves*, **so a lock's claim — this copy is the one being served —
|
|
||||||
is verified rather than assumed.**
|
|
||||||
|
|
||||||
## The lock file
|
|
||||||
|
|
||||||
**`.loom/externals/.locks`, one record per adopted document, tab-separated,
|
|
||||||
ordered by path.**
|
|
||||||
|
|
||||||
```
|
|
||||||
path <TAB> url <TAB> etag [ <TAB> visibility ]
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`path`** — *relative to `.loom/externals/`, and **for a person to read**.* **It
|
|
||||||
does not round-trip to a URL**; *it drops the route, the branch, and the
|
|
||||||
publisher's `.loom/published/`.*
|
|
||||||
- **`url`** — *the resolved origin, branch and all.*
|
|
||||||
- **`etag`** — *the publisher's, verbatim, including its quotes.*
|
|
||||||
- **`visibility`** — *optional. What the source could be read as **when it was
|
|
||||||
fetched**: `public` or `not-public`.* **Never `private`** — *an anonymous
|
|
||||||
request tells those two apart and nothing finer.*
|
|
||||||
|
|
||||||
**Records with three fields remain valid.** *A repository does not stop working
|
|
||||||
because the tool learned something new.*
|
|
||||||
|
|
||||||
## What is not promised
|
|
||||||
|
|
||||||
**The command surface.** *Names, flags and output are `--help`'s business and may
|
|
||||||
change. Nothing should parse them.*
|
|
||||||
|
|
||||||
**The generated orientation file's format.** *It is byte-deterministic within a
|
|
||||||
version so that its diff is readable; it is not stable across versions.*
|
|
||||||
|
|
||||||
**That visibility is precise.** *The signal distinguishes public from not-public
|
|
||||||
and nothing finer, so it cannot see two repositories private to different people
|
|
||||||
— which is the case where adopting between private repositories genuinely widens
|
|
||||||
access.* **The tool says so where it reports it, rather than implying a verdict it
|
|
||||||
has not earned.**
|
|
||||||
|
|
||||||
**That anything is checked when you are not looking.** *Nothing here runs on a
|
|
||||||
schedule, and a document nobody checks is a document nobody is checking.*
|
|
||||||
|
|
||||||
## What is promised about the things that are not promised
|
|
||||||
|
|
||||||
**The command surface may change. This page will say when it does.**
|
|
||||||
|
|
||||||
*A consumer whose README tells a person to type `loomctl external add loom/<repo>
|
|
||||||
<doc>.md` depends on that command by name — **not by parsing it, which is why no
|
|
||||||
check would ever catch a rename**. The thing that moved would not be a document
|
|
||||||
they adopted.*
|
|
||||||
|
|
||||||
> **So the exclusion is narrower than "you are on your own".** *We do not promise
|
|
||||||
> the surface holds; **we promise that a change to it lands here**, on a page you
|
|
||||||
> can lock* — **and a lock on this page reports `200` the day a command is
|
|
||||||
> renamed.**
|
|
||||||
|
|
||||||
**That is the whole mechanism, and it costs one section:**
|
|
||||||
|
|
||||||
### Surface changes
|
|
||||||
|
|
||||||
*Append-only, newest last. Nothing yet — the commands have not been renamed since
|
|
||||||
this page existed.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Verified fetchable by somebody who is not us on 2026-09-08.*
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Usages — `loomctl` guarantees
|
|
||||||
|
|
||||||
**This site is built with the tool.** *Every reference page in it was adopted with
|
|
||||||
`external add`, and the reconcile that produced this one went through `check` and
|
|
||||||
`apply`.* **So "what of ours depends on it" has a short, real answer for once.**
|
|
||||||
|
|
||||||
| what the page guarantees | what of ours leans on it |
|
|
||||||
|---|---|
|
|
||||||
| `check` reports and does not fix | **`npm run check` in CI.** *A build that repaired what it found would commit on a runner, with no cart and nobody to decide* |
|
|
||||||
| the `ETag` is the publisher's, verbatim | `scripts/generate.mjs` — *the guide stamps in frontmatter are those etags; a computed hash would fire the stale banner on every whitespace change* |
|
|
||||||
| it never writes over the network | *the deploy holds no write credential — `git-sync` clones anonymously and `loomctl` is never on the cluster at all* |
|
|
||||||
| `add` refuses what is adopted, and stages a differing copy | *how all eight original documents were locked after being fetched by hand, without one byte being rewritten* |
|
|
||||||
|
|
||||||
## What we depend on that the page explicitly does not promise
|
|
||||||
|
|
||||||
**The command surface.** *`scripts/publish-site.sh` and the README quote
|
|
||||||
`loomctl external add loom/<repo> <doc>.md` verbatim.* **The page says names,
|
|
||||||
flags and output are `--help`'s business and nothing should parse them** — *we do
|
|
||||||
not parse them, but a person following our README will type them.*
|
|
||||||
|
|
||||||
> **So a renamed command breaks our documentation and not our build**, *and no
|
|
||||||
> check will catch it*, **because the thing that moved is not a document we
|
|
||||||
> adopted.**
|
|
||||||
|
|
||||||
**Half-closed upstream, 2026-09-08.** *`guarantees.md` grew an append-only
|
|
||||||
`Surface changes` section.* **The promise is not that the surface holds — it is
|
|
||||||
that a change to it lands on the page we already lock**, *so a rename reports
|
|
||||||
`200` here the day it happens.*
|
|
||||||
|
|
||||||
> **The record is not that the thing never changes. It is that the change is
|
|
||||||
> findable by whoever depended on it.**
|
|
||||||
|
|
||||||
**The half still open is ours and cannot be closed by them:** *our README tells a
|
|
||||||
person to type a command, and a person typing is not a dependency any lock
|
|
||||||
watches.* **Same class as the sub-page staleness limit: the lock watches
|
|
||||||
documents, and some of what we lean on is not a document.**
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<!-- Generated by `loomctl orient`. Safe to overwrite; nobody typed it.
|
|
||||||
Regenerate after adopting a document or reconciling a change. -->
|
|
||||||
|
|
||||||
# What this repository depends on
|
|
||||||
|
|
||||||
Copies of other people's documents are kept under `.loom/externals/`, at a path
|
|
||||||
that says where each came from. **They are copies: do not edit them.** Anything
|
|
||||||
you want to say about one goes in a file *beside* it, never into it.
|
|
||||||
|
|
||||||
Three moves, and each has a document that owns the rule:
|
|
||||||
|
|
||||||
- **A copy is wrong, or you needed something it does not say** — write it in
|
|
||||||
`<name>.gaps.md` beside the copy.
|
|
||||||
- **What of ours depends on a copy** — write it in `<name>.usages.md` beside it.
|
|
||||||
- **A source changed** — `loomctl external check` says so and stages the new copy;
|
|
||||||
`loomctl external apply` takes it. Neither edits anything on its own.
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/annotating/annotating.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/annotating/raw/branch/main/.loom/published/annotating.md
|
|
||||||
- what of ours depends on it: `.loom/externals/git.hypertheory-labs.dev/loom/annotating/annotating.usages.md`
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/bedrock/loom-directory.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/loom-directory.md
|
|
||||||
- what of ours depends on it: **nothing recorded** — no `.usages.md`
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/bedrock/publication.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md
|
|
||||||
- what of ours depends on it: `.loom/externals/git.hypertheory-labs.dev/loom/bedrock/publication.usages.md`
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/recording-decisions.md
|
|
||||||
- what of ours depends on it: **nothing recorded** — no `.usages.md`
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md
|
|
||||||
- what of ours depends on it: **nothing recorded** — no `.usages.md`
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/bedrock/starting.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md
|
|
||||||
- what of ours depends on it: **nothing recorded** — no `.usages.md`
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/cart/cart.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/cart/raw/branch/main/.loom/published/cart.md
|
|
||||||
- what of ours depends on it: `.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md`
|
|
||||||
|
|
||||||
## `git.hypertheory-labs.dev/loom/externals/externals.md`
|
|
||||||
|
|
||||||
- source: https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md
|
|
||||||
- what of ours depends on it: `.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.usages.md`
|
|
||||||
|
|
||||||
## This repository publishes
|
|
||||||
|
|
||||||
See `.loom/published/`. What is handed over for others to depend on is
|
|
||||||
there; the rest of the repository is not hidden, it is simply not what
|
|
||||||
anybody depends on.
|
|
||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
# Handoff
|
|
||||||
|
|
||||||
**What this is, what is decided, and what is left.** *Written so another instance
|
|
||||||
can continue without reconstructing the reasoning.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The one-paragraph version
|
|
||||||
|
|
||||||
**This site is the `loom` conventions rendered for people, and it is an instance
|
|
||||||
of the thing it documents.** *Every reference page is a copy of a document
|
|
||||||
published by another repository, adopted with `loomctl external add`, locked to
|
|
||||||
the etag it was fetched at.* **The hand-written pages are four section fronts,
|
|
||||||
and they are stamped with what they were written against so that a source moving
|
|
||||||
puts a banner on them.**
|
|
||||||
|
|
||||||
*It exists because nine repositories of prose is more than a person can hold, and
|
|
||||||
it was the first idea in this project aimed at a **human** reader.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What is settled, and why
|
|
||||||
|
|
||||||
### Two kinds of page
|
|
||||||
|
|
||||||
**Generated** — one per adopted document. *Overwritten every build; nobody typed
|
|
||||||
it.* **Guide** — `index.mdx` per section. *Never overwritten.*
|
|
||||||
|
|
||||||
**The generator writes exactly one thing into a hand-written file: the `loom:`
|
|
||||||
block in its frontmatter.** *That block is machine-owned; everything below the
|
|
||||||
closing `---` is copied byte for byte.* **If you change this, change it here and
|
|
||||||
say so in the README**, *because a tool that edits a person's file without a
|
|
||||||
declared boundary is how the `CLAUDE.md` problem starts.*
|
|
||||||
|
|
||||||
### Staleness is rendered, not filed
|
|
||||||
|
|
||||||
**The build cannot tell whether a guide is still true — only a person can — so it
|
|
||||||
renders the question onto the page.** *A reader sees the doubt even if nobody has
|
|
||||||
reconciled it.*
|
|
||||||
|
|
||||||
> **The site degrades honestly rather than reading as authoritative and being
|
|
||||||
> wrong.**
|
|
||||||
|
|
||||||
**`npm run ack` is the person saying they re-read it. Nothing else clears the
|
|
||||||
banner.** *`npm run check` exits `1` on any stale guide, for CI.*
|
|
||||||
|
|
||||||
**Declined: having the build stage a polad into the cart.** *`loomctl external
|
|
||||||
check` does exactly that and it is right to* — **but a docs build runs in CI, in
|
|
||||||
worktrees, and on machines with no cart**, *and staging a polad is an act of
|
|
||||||
judgment, which is what keeps a cart worth reading.* **The build reports; a
|
|
||||||
person raises.**
|
|
||||||
|
|
||||||
### Prefer an example to an explanation
|
|
||||||
|
|
||||||
**Because an explanation is a second saying of a rule owned elsewhere, and it
|
|
||||||
goes stale silently.** *An example goes stale visibly — the artifacts in it are
|
|
||||||
the wrong shape.* **The four guides are written this way on purpose**; *keep it.*
|
|
||||||
|
|
||||||
### Sections are repositories
|
|
||||||
|
|
||||||
*Not hosts, not documents.* **`bedrock` with five pages is how a person holds
|
|
||||||
it**; *nine one-page sections is not.* **The host segment never appears in
|
|
||||||
navigation.**
|
|
||||||
|
|
||||||
### Everything here is public, and that is arithmetic
|
|
||||||
|
|
||||||
*`externals` forbids adopting from a source less readable than the adopting
|
|
||||||
repository. This site is on the public internet.* **So a document must be
|
|
||||||
published to appear here** — *which turns the widening rule from a sentence
|
|
||||||
somebody has to remember into an incentive.*
|
|
||||||
|
|
||||||
> **The hazard runs the other way now.** *Wanting to be in the site is pressure
|
|
||||||
> to publish things that should not be.* **Grep a repository's history for
|
|
||||||
> anything address- or version-shaped when it changes visibility** — *that has
|
|
||||||
> already caught one node address.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Where things are
|
|
||||||
|
|
||||||
```
|
|
||||||
scripts/generate.mjs the whole projection. Read this first; it is commented.
|
|
||||||
astro.config.mjs sidebar autogenerates from directories — names no document
|
|
||||||
src/content.config.ts the `loom:` frontmatter block's schema
|
|
||||||
src/content/docs/
|
|
||||||
index.mdx hand-written home
|
|
||||||
<repo>/index.mdx hand-written guide, stamped
|
|
||||||
<repo>/<doc>.md GENERATED — do not edit
|
|
||||||
.loom/externals/ the adopted copies and .locks — do not edit either
|
|
||||||
.loom/externals/**/*.usages.md what of ours depends on each document
|
|
||||||
```
|
|
||||||
|
|
||||||
**Two directories hold copies and neither may be edited**, *for the same reason:*
|
|
||||||
**a copy that differs from its source is the only evidence that something moved
|
|
||||||
while nobody was watching.**
|
|
||||||
|
|
||||||
## Adding a document
|
|
||||||
|
|
||||||
```sh
|
|
||||||
loomctl external add loom/<repo> <doc>.md
|
|
||||||
npm run generate
|
|
||||||
```
|
|
||||||
|
|
||||||
*Nothing else. No config to touch.* **A document that stops being adopted has its
|
|
||||||
page removed by the generator**, *because a page for something no longer depended
|
|
||||||
on reads as current and nothing will ever correct it.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What is left
|
|
||||||
|
|
||||||
**Deploy.** *Nothing is wired up.* `npm run build` *emits `dist/`; it needs to
|
|
||||||
land at `loom.hypertheory-labs.dev`.* **`npm run check` belongs in whatever runs
|
|
||||||
first**, *so a stale guide fails before it ships.*
|
|
||||||
|
|
||||||
**`.gaps.md` on the site's own externals — deliberately not started.** *This
|
|
||||||
repository is a real adopter and will find real gaps; publishing them would make
|
|
||||||
the site more than a mirror.* **Left out of v1 on purpose** — *it is pointed in a
|
|
||||||
way a plain mirror is not, and that is Jeff's call.*
|
|
||||||
|
|
||||||
**The `annotating` guide holds a claim its source does not make** — *the
|
|
||||||
annotate-versus-correspond discriminator, which was settled in a cart round and
|
|
||||||
lives in another repository's `cart.gaps.md`.* **No check will ever catch that.**
|
|
||||||
*It is recorded in `annotating.usages.md`. If `annotating` is ever amended to
|
|
||||||
include it, delete the paragraph rather than leaving two sayings.*
|
|
||||||
|
|
||||||
**Sub-page staleness is not detected.** *The banner fires when a whole document
|
|
||||||
moves, not when the sentence a guide depended on changed* — **so it fires on
|
|
||||||
edits that do not matter and cannot fire more precisely.** *That is the known
|
|
||||||
limit and the `.usages.md` files say what each guide actually leans on.*
|
|
||||||
|
|
||||||
**`externals.md` owes an amendment.** *The lock grew a fourth column
|
|
||||||
(`visibility`) and the published document still describes three.* **That is
|
|
||||||
`loom/externals`' to fix, not this repository's** — *filing it as a gap here
|
|
||||||
would be the right move if it is not done soon.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The check phrase
|
|
||||||
|
|
||||||
**Jeff's, and it is the one that keeps this on the rails:**
|
|
||||||
|
|
||||||
> **Does anything here depend on it?**
|
|
||||||
|
|
||||||
*If nothing does, it is probably problem 3 — affordances, stagings, roles,
|
|
||||||
beads — which is **bracketed**.* **This site documents problems 1 and 2 only.**
|
|
||||||
*Do not add a section waiting for beads.*
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# loom/docs
|
|
||||||
|
|
||||||
**The loom conventions, rendered for people.** *Astro Starlight, published at
|
|
||||||
[loom.hypertheory-labs.dev](https://loom.hypertheory-labs.dev).*
|
|
||||||
|
|
||||||
> **This site is an instance of the thing it documents.** *Every reference page
|
|
||||||
> is a copy of a document published by another repository, adopted under
|
|
||||||
> `externals`, locked to the version it was fetched at, and checked against its
|
|
||||||
> source.* **Nothing is written twice.**
|
|
||||||
|
|
||||||
## Running it
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
npm run dev # generate, then serve
|
|
||||||
npm run build # generate, then build to dist/
|
|
||||||
npm run generate # just the projection
|
|
||||||
npm run check # exit 1 if any guide is stale — for CI
|
|
||||||
npm run ack # "I re-read the stale guides and they are still true"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Two kinds of page, and the difference is the design
|
|
||||||
|
|
||||||
**Generated** — one per adopted document, under `src/content/docs/<repo>/`.
|
|
||||||
*A rendering of somebody else's document.* **Safe to overwrite, because nobody
|
|
||||||
typed it.** *Do not edit one: your change is gone on the next build, and the
|
|
||||||
canonical copy is in `.loom/externals/` anyway.*
|
|
||||||
|
|
||||||
**Guides** — `index.mdx`, one per section. **Hand-written and never
|
|
||||||
overwritten.** *This is where a worked example goes, and the order to read
|
|
||||||
things in.*
|
|
||||||
|
|
||||||
> **Prefer an example to an explanation.** *An explanation is a second saying of
|
|
||||||
> a rule owned on one of the pages beside it, and it goes stale silently.* **An
|
|
||||||
> example goes stale visibly, because the artifacts in it are the wrong shape.**
|
|
||||||
|
|
||||||
## How a guide is kept honest
|
|
||||||
|
|
||||||
**Each guide is stamped with the etags it was written against**, in its
|
|
||||||
frontmatter. *When a source moves, the stamp and the lock disagree, and the build
|
|
||||||
renders a banner onto the page:*
|
|
||||||
|
|
||||||
> *Written against an older `cart`. The source has changed since — this page may
|
|
||||||
> no longer be true.*
|
|
||||||
|
|
||||||
**The build cannot tell whether the guide is still true. Only a person can.** *So
|
|
||||||
it asks, on the page, where a reader sees it too* — **the site degrades honestly
|
|
||||||
rather than reading as authoritative and being wrong.**
|
|
||||||
|
|
||||||
**`npm run ack` is you saying you re-read it.** *Nothing else clears the banner.*
|
|
||||||
|
|
||||||
### Why the build does not open a cart
|
|
||||||
|
|
||||||
*It reports; a person raises.* **Builds run in CI, in worktrees, on machines with
|
|
||||||
no cart** — *and staging a polad is an act of judgment, which is what makes a cart
|
|
||||||
worth reading.* **A tool may stage in its own directory; the cart belongs to the
|
|
||||||
people.**
|
|
||||||
|
|
||||||
## Adding a document
|
|
||||||
|
|
||||||
```sh
|
|
||||||
loomctl external add loom/<repo> <doc>.md
|
|
||||||
npm run generate
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nothing else.** *The sidebar autogenerates from directories and `astro.config.mjs`
|
|
||||||
names no document* — *a config that listed them would be a second place to update.*
|
|
||||||
|
|
||||||
**A document that stops being adopted has its page removed** — *a page for
|
|
||||||
something no longer depended on reads as current and nothing will ever correct
|
|
||||||
it.*
|
|
||||||
|
|
||||||
## Why everything here is public
|
|
||||||
|
|
||||||
**Not a policy — arithmetic.** *`externals` says do not adopt from a source less
|
|
||||||
readable than the repository you are adopting into. This site is on the public
|
|
||||||
internet.* **So its sources are public, or they are not its sources.**
|
|
||||||
|
|
||||||
*Which turns the widening rule from a sentence somebody has to remember into an
|
|
||||||
incentive: **a document that is not published does not appear here.***
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { defineConfig } from 'astro/config'
|
|
||||||
import starlight from '@astrojs/starlight'
|
|
||||||
import sidebar from './src/sidebar.json' with { type: 'json' }
|
|
||||||
|
|
||||||
// Nothing in this file names a document or a section. The sidebar is generated
|
|
||||||
// from .loom/externals/.locks, so adopting anything and re-running
|
|
||||||
// `npm run generate` is the whole of adding it — a config that listed them
|
|
||||||
// would be a second place to update and would go stale silently.
|
|
||||||
export default defineConfig({
|
|
||||||
site: 'https://loom.hypertheory-labs.dev',
|
|
||||||
srcDir: './src',
|
|
||||||
integrations: [
|
|
||||||
starlight({
|
|
||||||
title: 'loom',
|
|
||||||
description:
|
|
||||||
'Conventions for holding what a repository knows, so that somebody arriving — person or agent — can find it.',
|
|
||||||
social: [
|
|
||||||
{
|
|
||||||
icon: 'seti:git',
|
|
||||||
label: 'Source',
|
|
||||||
href: 'https://git.hypertheory-labs.dev/loom',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
editLink: { baseUrl: 'https://git.hypertheory-labs.dev/loom/docs/_edit/main/' },
|
|
||||||
// Generated by scripts/generate.mjs from the locks. Adopting from a new
|
|
||||||
// repository adds a section here with no edit to this file.
|
|
||||||
sidebar,
|
|
||||||
customCss: ['./src/styles/loom.css'],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Deploying this site
|
|
||||||
|
|
||||||
**`loom.hypertheory-labs.dev`, on the home cluster.**
|
|
||||||
|
|
||||||
**No container image is built.** *Two public images — `nginx` and `git-sync` —
|
|
||||||
and the site arrives over git.* **`git-sync` follows the `site` branch of this
|
|
||||||
repository**, *which holds `dist/` and nothing else*, **and `nginx` serves what
|
|
||||||
lands in the shared volume.**
|
|
||||||
|
|
||||||
> **This deliberately sidesteps the cluster's open gap on
|
|
||||||
> [publishing container images](https://git.hypertheory-labs.dev/jeffry/homelab-cluster/src/branch/main/.loom/gaps/publishing-container-images.md).**
|
|
||||||
> *Sidestepping is not closing:* **the first workload here that genuinely needs
|
|
||||||
> an image we built will still hit it**, *and this repository is not evidence
|
|
||||||
> that the gap is smaller than it looked.*
|
|
||||||
|
|
||||||
## Publishing a new version
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
npm run publish-site # commits dist/ to the `site` branch and pushes
|
|
||||||
```
|
|
||||||
|
|
||||||
*`git-sync` polls, and `nginx` serves the new files without a restart —* **there
|
|
||||||
is no rollout, because there is no image.**
|
|
||||||
|
|
||||||
## What is here
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| `namespace.yaml` | the `loom` namespace |
|
|
||||||
| `deployment.yaml` | `nginx` + `git-sync`, sharing an `emptyDir` |
|
|
||||||
| `service.yaml` | ClusterIP on 80 |
|
|
||||||
| `httproute.yaml` | pins to the `loom` listener and **carries no hostname of its own** |
|
|
||||||
|
|
||||||
**The `Certificate` and the Gateway listener are not here** — *they are cluster
|
|
||||||
infrastructure and live in `homelab-impl`, beside the other four.*
|
|
||||||
|
|
||||||
## Why the route carries no hostname
|
|
||||||
|
|
||||||
*Because `public-access.md` says so:* **a route that carries its own `hostnames:`
|
|
||||||
stops inheriting from the listener and stops being portable between staging and
|
|
||||||
production.** *It pins by `sectionName` and takes the hostname it is given.*
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# The site is served from a git branch, not from an image we built.
|
|
||||||
#
|
|
||||||
# git-sync clones the `site` branch of loom/docs into a shared emptyDir and
|
|
||||||
# polls it; nginx serves whatever is there. Both images are public, which is
|
|
||||||
# what lets this deploy while the cluster's container-registry gap is open.
|
|
||||||
#
|
|
||||||
# Anonymous clone: loom/docs is a public repository. Nothing here holds a
|
|
||||||
# credential, and a private repository would need one — which is the same
|
|
||||||
# unanswered question the registry gap names.
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: docs
|
|
||||||
namespace: loom
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels: { app: docs }
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels: { app: docs }
|
|
||||||
spec:
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 65533
|
|
||||||
volumes:
|
|
||||||
- name: site
|
|
||||||
emptyDir: {}
|
|
||||||
- name: nginx-conf
|
|
||||||
configMap: { name: docs-nginx }
|
|
||||||
initContainers:
|
|
||||||
# --one-time, so the pod is not Ready until the site is actually on
|
|
||||||
# disk. Without this nginx serves 404s for the first few seconds after
|
|
||||||
# every reschedule.
|
|
||||||
- name: git-sync-init
|
|
||||||
image: registry.k8s.io/git-sync/git-sync:v4.4.0
|
|
||||||
args:
|
|
||||||
- --repo=https://git.hypertheory-labs.dev/loom/docs.git
|
|
||||||
- --ref=site
|
|
||||||
- --root=/site
|
|
||||||
- --link=current
|
|
||||||
- --one-time
|
|
||||||
volumeMounts:
|
|
||||||
- { name: site, mountPath: /site }
|
|
||||||
containers:
|
|
||||||
- name: git-sync
|
|
||||||
image: registry.k8s.io/git-sync/git-sync:v4.4.0
|
|
||||||
args:
|
|
||||||
- --repo=https://git.hypertheory-labs.dev/loom/docs.git
|
|
||||||
- --ref=site
|
|
||||||
- --root=/site
|
|
||||||
- --link=current
|
|
||||||
- --period=60s
|
|
||||||
volumeMounts:
|
|
||||||
- { name: site, mountPath: /site }
|
|
||||||
- name: nginx
|
|
||||||
image: nginx:1.29-alpine
|
|
||||||
ports:
|
|
||||||
- { containerPort: 8080, name: http }
|
|
||||||
volumeMounts:
|
|
||||||
- { name: site, mountPath: /site, readOnly: true }
|
|
||||||
- { name: nginx-conf, mountPath: /etc/nginx/conf.d }
|
|
||||||
readinessProbe:
|
|
||||||
httpGet: { path: /, port: http }
|
|
||||||
initialDelaySeconds: 2
|
|
||||||
resources:
|
|
||||||
requests: { cpu: 10m, memory: 32Mi }
|
|
||||||
limits: { memory: 128Mi }
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Carries no hostname of its own, on purpose: it pins to the `loom` listener by
|
|
||||||
# sectionName and inherits the hostname from it. A route with its own
|
|
||||||
# `hostnames:` stops inheriting and stops being portable between environments.
|
|
||||||
apiVersion: gateway.networking.k8s.io/v1
|
|
||||||
kind: HTTPRoute
|
|
||||||
metadata:
|
|
||||||
name: docs
|
|
||||||
namespace: loom
|
|
||||||
spec:
|
|
||||||
parentRefs:
|
|
||||||
- name: traefik-gateway
|
|
||||||
namespace: traefik
|
|
||||||
sectionName: loom
|
|
||||||
rules:
|
|
||||||
- backendRefs:
|
|
||||||
- name: docs
|
|
||||||
port: 80
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: loom
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: docs-nginx
|
|
||||||
namespace: loom
|
|
||||||
data:
|
|
||||||
default.conf: |
|
|
||||||
server {
|
|
||||||
listen 8080;
|
|
||||||
# git-sync keeps `current` pointed at the checked-out worktree, so this
|
|
||||||
# path follows a new commit without nginx restarting.
|
|
||||||
root /site/current/dist;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Astro emits directory-style URLs: /bedrock/starting/ -> .../index.html
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ $uri/index.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Hashed assets are immutable; pages are not, and a cached page is how a
|
|
||||||
# corrected document keeps reading as true.
|
|
||||||
location /_astro/ {
|
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 404 /404.html;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: docs
|
|
||||||
namespace: loom
|
|
||||||
spec:
|
|
||||||
selector: { app: docs }
|
|
||||||
ports:
|
|
||||||
- { name: http, port: 80, targetPort: http }
|
|
||||||
Vendored
+48
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html><html lang="en" dir="ltr" data-theme="dark" data-has-hero class="astro-bguv2lll"> <head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>404 | loom</title><link rel="canonical" href="https://loom.hypertheory-labs.dev/404/"/><link rel="sitemap" href="/sitemap-index.xml"/><link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml"/><meta name="generator" content="Astro v5.18.2"/><meta name="generator" content="Starlight v0.36.3"/><meta property="og:title" content="404"/><meta property="og:type" content="article"/><meta property="og:url" content="https://loom.hypertheory-labs.dev/404/"/><meta property="og:locale" content="en"/><meta property="og:description" content="Conventions for holding what a repository knows, so that somebody arriving — person or agent — can find it."/><meta property="og:site_name" content="loom"/><meta name="twitter:card" content="summary_large_image"/><meta name="description" content="Conventions for holding what a repository knows, so that somebody arriving — person or agent — can find it."/><script>
|
||||||
|
window.StarlightThemeProvider = (() => {
|
||||||
|
const storedTheme =
|
||||||
|
typeof localStorage !== 'undefined' && localStorage.getItem('starlight-theme');
|
||||||
|
const theme =
|
||||||
|
storedTheme ||
|
||||||
|
(window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
||||||
|
document.documentElement.dataset.theme = theme === 'light' ? 'light' : 'dark';
|
||||||
|
return {
|
||||||
|
updatePickers(theme = storedTheme || 'auto') {
|
||||||
|
document.querySelectorAll('starlight-theme-select').forEach((picker) => {
|
||||||
|
const select = picker.querySelector('select');
|
||||||
|
if (select) select.value = theme;
|
||||||
|
/** @type {HTMLTemplateElement | null} */
|
||||||
|
const tmpl = document.querySelector(`#theme-icons`);
|
||||||
|
const newIcon = tmpl && tmpl.content.querySelector('.' + theme);
|
||||||
|
if (newIcon) {
|
||||||
|
const oldIcon = picker.querySelector('svg.label-icon');
|
||||||
|
if (oldIcon) {
|
||||||
|
oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script><template id="theme-icons"><svg aria-hidden="true" class="light astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M5 12a1 1 0 0 0-1-1H3a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Zm.64 5-.71.71a1 1 0 0 0 0 1.41 1 1 0 0 0 1.41 0l.71-.71A1 1 0 0 0 5.64 17ZM12 5a1 1 0 0 0 1-1V3a1 1 0 0 0-2 0v1a1 1 0 0 0 1 1Zm5.66 2.34a1 1 0 0 0 .7-.29l.71-.71a1 1 0 1 0-1.41-1.41l-.66.71a1 1 0 0 0 0 1.41 1 1 0 0 0 .66.29Zm-12-.29a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.71-.71a1.004 1.004 0 1 0-1.43 1.41l.73.71ZM21 11h-1a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2Zm-2.64 6A1 1 0 0 0 17 18.36l.71.71a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.76-.66ZM12 6.5a5.5 5.5 0 1 0 5.5 5.5A5.51 5.51 0 0 0 12 6.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0 3.5a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1a1 1 0 0 0-1-1Z"/></svg><svg aria-hidden="true" class="dark astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.64 13a1 1 0 0 0-1.05-.14 8.049 8.049 0 0 1-3.37.73 8.15 8.15 0 0 1-8.14-8.1 8.59 8.59 0 0 1 .25-2A1 1 0 0 0 8 2.36a10.14 10.14 0 1 0 14 11.69 1 1 0 0 0-.36-1.05Zm-9.5 6.69A8.14 8.14 0 0 1 7.08 5.22v.27a10.15 10.15 0 0 0 10.14 10.14 9.784 9.784 0 0 0 2.1-.22 8.11 8.11 0 0 1-7.18 4.32v-.04Z"/></svg><svg aria-hidden="true" class="auto astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg></template><link rel="stylesheet" href="/_astro/print.DNXP8c50.css" media="print"><link rel="stylesheet" href="/_astro/index.Bl8FUrqd.css">
|
||||||
|
<style>@layer starlight.components{:root{--sl-badge-default-border: var(--sl-color-accent);--sl-badge-default-bg: var(--sl-color-accent-low);--sl-badge-default-text: #fff;--sl-badge-note-border: var(--sl-color-blue);--sl-badge-note-bg: var(--sl-color-blue-low);--sl-badge-note-text: #fff;--sl-badge-danger-border: var(--sl-color-red);--sl-badge-danger-bg: var(--sl-color-red-low);--sl-badge-danger-text: #fff;--sl-badge-success-border: var(--sl-color-green);--sl-badge-success-bg: var(--sl-color-green-low);--sl-badge-success-text: #fff;--sl-badge-caution-border: var(--sl-color-orange);--sl-badge-caution-bg: var(--sl-color-orange-low);--sl-badge-caution-text: #fff;--sl-badge-tip-border: var(--sl-color-purple);--sl-badge-tip-bg: var(--sl-color-purple-low);--sl-badge-tip-text: #fff}[data-theme=light]:root{--sl-badge-default-bg: var(--sl-color-accent-high);--sl-badge-note-bg: var(--sl-color-blue-high);--sl-badge-danger-bg: var(--sl-color-red-high);--sl-badge-success-bg: var(--sl-color-green-high);--sl-badge-caution-bg: var(--sl-color-orange-high);--sl-badge-tip-bg: var(--sl-color-purple-high)}.sl-badge:where(.astro-avdet4wd){display:inline-block;border:1px solid var(--sl-color-border-badge);border-radius:.25rem;font-family:var(--sl-font-system-mono);line-height:normal;color:var(--sl-color-text-badge);background-color:var(--sl-color-bg-badge);overflow-wrap:anywhere}.sidebar-content .sl-badge:where(.astro-avdet4wd){line-height:1;font-size:var(--sl-text-xs);padding:.125rem .375rem}.sidebar-content a[aria-current=page]>.sl-badge:where(.astro-avdet4wd){--sl-color-bg-badge: transparent;--sl-color-border-badge: currentColor;color:inherit}.default:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-default-bg);--sl-color-border-badge: var(--sl-badge-default-border);--sl-color-text-badge: var(--sl-badge-default-text)}.note:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-note-bg);--sl-color-border-badge: var(--sl-badge-note-border);--sl-color-text-badge: var(--sl-badge-note-text)}.danger:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-danger-bg);--sl-color-border-badge: var(--sl-badge-danger-border);--sl-color-text-badge: var(--sl-badge-danger-text)}.success:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-success-bg);--sl-color-border-badge: var(--sl-badge-success-border);--sl-color-text-badge: var(--sl-badge-success-text)}.tip:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-tip-bg);--sl-color-border-badge: var(--sl-badge-tip-border);--sl-color-text-badge: var(--sl-badge-tip-text)}.caution:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-caution-bg);--sl-color-border-badge: var(--sl-badge-caution-border);--sl-color-text-badge: var(--sl-badge-caution-text)}.small:where(.astro-avdet4wd){font-size:var(--sl-text-xs);padding:.125rem .25rem}.medium:where(.astro-avdet4wd){font-size:var(--sl-text-sm);padding:.175rem .35rem}.large:where(.astro-avdet4wd){font-size:var(--sl-text-base);padding:.225rem .45rem}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6) .sl-badge:where(.astro-avdet4wd){vertical-align:middle}}
|
||||||
|
@layer starlight.components{.card:where(.astro-v5tidmuc){--sl-card-border: var(--sl-color-purple);--sl-card-bg: var(--sl-color-purple-low);border:1px solid var(--sl-color-gray-5);background-color:var(--sl-color-black);padding:clamp(1rem,calc(.125rem + 3vw),2.5rem);flex-direction:column;gap:clamp(.5rem,calc(.125rem + 1vw),1rem)}.card:where(.astro-v5tidmuc):nth-child(4n+1){--sl-card-border: var(--sl-color-orange);--sl-card-bg: var(--sl-color-orange-low)}.card:where(.astro-v5tidmuc):nth-child(4n+3){--sl-card-border: var(--sl-color-green);--sl-card-bg: var(--sl-color-green-low)}.card:where(.astro-v5tidmuc):nth-child(4n+4){--sl-card-border: var(--sl-color-red);--sl-card-bg: var(--sl-color-red-low)}.card:where(.astro-v5tidmuc):nth-child(4n+5){--sl-card-border: var(--sl-color-blue);--sl-card-bg: var(--sl-color-blue-low)}.title:where(.astro-v5tidmuc){font-weight:600;font-size:var(--sl-text-h4);color:var(--sl-color-white);line-height:var(--sl-line-height-headings);gap:1rem;align-items:center}.card:where(.astro-v5tidmuc) .icon:where(.astro-v5tidmuc){border:1px solid var(--sl-card-border);background-color:var(--sl-card-bg);padding:.2em;border-radius:.25rem;flex-shrink:0}.card:where(.astro-v5tidmuc) .body:where(.astro-v5tidmuc){margin:0;font-size:clamp(var(--sl-text-sm),calc(.5rem + 1vw),var(--sl-text-body))}}
|
||||||
|
@layer starlight.components{.card-grid:where(.astro-zntqmydn){display:grid;grid-template-columns:100%;gap:1rem}.card-grid:where(.astro-zntqmydn)>*{margin-top:0!important}@media(min-width:50rem){.card-grid:where(.astro-zntqmydn){grid-template-columns:1fr 1fr;gap:1.5rem}.stagger:where(.astro-zntqmydn){--stagger-height: 5rem;padding-bottom:var(--stagger-height)}.stagger:where(.astro-zntqmydn)>*:nth-child(2n){transform:translateY(var(--stagger-height))}}}
|
||||||
|
@layer starlight.components{svg:where(.astro-c6vsoqas){color:var(--sl-icon-color);font-size:var(--sl-icon-size, 1em);width:1em;height:1em}}
|
||||||
|
@layer starlight.components{.sl-steps{--bullet-size: calc(var(--sl-line-height) * 1rem);--bullet-margin: .375rem;list-style:none;counter-reset:steps-counter var(--sl-steps-start, 0);padding-inline-start:0}.sl-steps>li{counter-increment:steps-counter;position:relative;padding-inline-start:calc(var(--bullet-size) + 1rem);padding-bottom:1px;min-height:calc(var(--bullet-size) + var(--bullet-margin))}.sl-steps>li+li{margin-top:0}.sl-steps>li:before{content:counter(steps-counter);position:absolute;top:0;inset-inline-start:0;width:var(--bullet-size);height:var(--bullet-size);line-height:var(--bullet-size);font-size:var(--sl-text-xs);font-weight:600;text-align:center;color:var(--sl-color-white);background-color:var(--sl-color-gray-6);border-radius:99rem;box-shadow:inset 0 0 0 1px var(--sl-color-gray-5)}.sl-steps>li:after{--guide-width: 1px;content:"";position:absolute;top:calc(var(--bullet-size) + var(--bullet-margin));bottom:var(--bullet-margin);inset-inline-start:calc((var(--bullet-size) - var(--guide-width)) / 2);width:var(--guide-width);background-color:var(--sl-color-hairline-light)}}@layer starlight.content{.sl-steps>li>:first-child{--lh: calc(1em * var(--sl-line-height));--shift-y: calc(.5 * (var(--bullet-size) - var(--lh)));transform:translateY(var(--shift-y));margin-bottom:var(--shift-y)}.sl-steps>li>:first-child:where(h1,h2,h3,h4,h5,h6){--lh: calc(1em * var(--sl-line-height-headings))}@supports (--prop: 1lh){.sl-steps>li>:first-child{--lh: 1lh}}}
|
||||||
|
@layer starlight.components{.sl-link-button:where(.astro-xwgiixxa){align-items:center;border:1px solid transparent;border-radius:999rem;display:inline-flex;font-size:var(--sl-text-sm);gap:.5em;line-height:1.1875;outline-offset:.25rem;padding:.4375rem 1.125rem;text-decoration:none}.sl-link-button:where(.astro-xwgiixxa).primary{background:var(--sl-color-text-accent);border-color:var(--sl-color-text-accent);color:var(--sl-color-black)}.sl-link-button:where(.astro-xwgiixxa).primary:hover{color:var(--sl-color-black)}.sl-link-button:where(.astro-xwgiixxa).secondary{border-color:inherit;color:var(--sl-color-white)}.sl-link-button:where(.astro-xwgiixxa).minimal{color:var(--sl-color-white);padding-inline:0}.sl-link-button:where(.astro-xwgiixxa) svg{flex-shrink:0}@media(min-width:50rem){.sl-link-button:where(.astro-xwgiixxa){font-size:var(--sl-text-base);padding:.9375rem 1.25rem}}.sl-markdown-content .sl-link-button:where(.astro-xwgiixxa){margin-inline-end:1rem}.sl-markdown-content .sl-link-button:where(.astro-xwgiixxa):not(:where(p *)){margin-block:1rem}}
|
||||||
|
</style><script type="module" src="/_astro/page.B1D-nYk3.js"></script></head> <body class="astro-bguv2lll"> <a href="#_top" class="astro-7q3lir66">Skip to content</a> <div class="page sl-flex astro-vrdttmbt"> <header class="header astro-vrdttmbt"><div class="header astro-kmkmnagf"> <div class="title-wrapper sl-flex astro-kmkmnagf"> <a href="/" class="site-title sl-flex astro-m46x6ez3"> <span class="astro-m46x6ez3" translate="no"> loom </span> </a> </div> <div class="sl-flex print:hidden astro-kmkmnagf"> <site-search class="astro-kmkmnagf astro-v37mnknz" data-translations="{"placeholder":"Search"}"> <button data-open-modal disabled aria-label="Search" aria-keyshortcuts="Control+K" class="astro-v37mnknz"> <svg aria-hidden="true" class="astro-v37mnknz astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.71 20.29 18 16.61A9 9 0 1 0 16.61 18l3.68 3.68a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.39ZM11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z"/></svg> <span class="sl-hidden md:sl-block astro-v37mnknz" aria-hidden="true">Search</span> <kbd class="sl-hidden md:sl-flex astro-v37mnknz" style="display: none;"> <kbd class="astro-v37mnknz">Ctrl</kbd><kbd class="astro-v37mnknz">K</kbd> </kbd> </button> <dialog style="padding:0" aria-label="Search" class="astro-v37mnknz"> <div class="dialog-frame sl-flex astro-v37mnknz"> <button data-close-modal class="sl-flex md:sl-hidden astro-v37mnknz"> Cancel </button> <div class="search-container astro-v37mnknz"> <div id="starlight__search" class="astro-v37mnknz"></div> </div> </div> </dialog> </site-search> <script>
|
||||||
|
(() => {
|
||||||
|
const openBtn = document.querySelector('button[data-open-modal]');
|
||||||
|
const shortcut = openBtn?.querySelector('kbd');
|
||||||
|
if (!openBtn || !(shortcut instanceof HTMLElement)) return;
|
||||||
|
const platformKey = shortcut.querySelector('kbd');
|
||||||
|
if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
|
||||||
|
platformKey.textContent = '⌘';
|
||||||
|
openBtn.setAttribute('aria-keyshortcuts', 'Meta+K');
|
||||||
|
}
|
||||||
|
shortcut.style.display = '';
|
||||||
|
})();
|
||||||
|
</script> <script type="module" src="/_astro/Search.astro_astro_type_script_index_0_lang.CJiOmi4V.js"></script> </div> <div class="sl-hidden md:sl-flex print:hidden right-group astro-kmkmnagf"> <div class="sl-flex social-icons astro-kmkmnagf"> <a href="https://git.hypertheory-labs.dev/loom" rel="me" class="sl-flex astro-wy4te6ga"><span class="sr-only astro-wy4te6ga">Source</span><svg aria-hidden="true" class="astro-wy4te6ga astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M0.355 12.231L0.355 12.231L0.355 11.853Q0.397 11.517 0.565 11.265L0.565 11.265Q0.691 11.097 0.943 10.803L0.943 10.803L1.279 10.425L7.831 3.831L7.873 3.873Q7.957 3.873 7.957 3.957L7.957 3.957L10.309 6.351Q10.519 6.519 10.309 6.729L10.309 6.729Q10.225 7.317 10.435 7.863Q10.645 8.409 11.107 8.703L11.107 8.703Q11.275 8.787 11.317 8.892Q11.359 8.997 11.359 9.207L11.359 9.207L11.359 15.003Q11.359 15.255 11.107 15.507L11.107 15.507Q10.603 15.843 10.372 16.389Q10.141 16.935 10.330 17.502Q10.519 18.069 10.981 18.447Q11.443 18.825 12.031 18.825Q12.619 18.825 13.081 18.489Q13.543 18.153 13.732 17.586Q13.921 17.019 13.753 16.473Q13.585 15.927 13.081 15.507L13.081 15.507Q12.913 15.423 12.871 15.318Q12.829 15.213 12.829 15.003L12.829 15.003L12.829 9.081L12.955 9.081L15.055 11.181Q15.139 11.265 15.139 11.391L15.139 11.391L15.181 11.475L15.181 12.231Q15.265 13.029 15.874 13.470Q16.483 13.911 17.281 13.806Q18.079 13.701 18.562 13.029Q19.045 12.357 18.835 11.601L18.835 11.601Q18.751 10.929 18.100 10.488Q17.449 10.047 16.735 10.131L16.735 10.131Q16.483 10.131 16.231 10.005L16.231 10.005L14.005 7.779Q13.879 7.653 13.879 7.401L13.879 7.401Q14.005 6.813 13.711 6.267Q13.417 5.721 12.871 5.448Q12.325 5.175 11.779 5.301L11.779 5.301Q11.359 5.301 11.359 5.175L11.359 5.175Q9.805 3.579 9.007 2.907L9.007 2.907Q8.797 2.739 9.007 2.529L9.007 2.529Q9.469 2.193 10.183 1.353L10.183 1.353L10.729 0.807Q11.905-0.369 13.081 0.807L13.081 0.807L23.035 10.803Q24.253 11.979 23.035 13.155L23.035 13.155L13.291 22.941Q12.913 23.319 12.745 23.445L12.745 23.445Q12.409 23.739 12.031 23.781L12.031 23.781L11.779 23.781L11.569 23.697Q11.065 23.487 10.855 23.277L10.855 23.277L10.057 22.479Q8.881 21.387 8.335 20.757L8.335 20.757L1.153 13.575Q1.069 13.365 0.775 13.029L0.775 13.029Q0.397 12.525 0.355 12.231Z"/></svg></a> </div> <starlight-theme-select> <label style="--sl-select-width: 6.25em" class="astro-4yphtoen"> <span class="sr-only astro-4yphtoen">Select theme</span> <svg aria-hidden="true" class="icon label-icon astro-4yphtoen astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg> <select autocomplete="off" class="astro-4yphtoen"> <option value="dark" class="astro-4yphtoen">Dark</option><option value="light" class="astro-4yphtoen">Light</option><option value="auto" selected class="astro-4yphtoen">Auto</option> </select> <svg aria-hidden="true" class="icon caret astro-4yphtoen astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg> </label> </starlight-theme-select> <script>
|
||||||
|
StarlightThemeProvider.updatePickers();
|
||||||
|
</script> <script type="module">const r="starlight-theme",o=e=>e==="auto"||e==="dark"||e==="light"?e:"auto",c=()=>o(typeof localStorage<"u"&&localStorage.getItem(r));function n(e){typeof localStorage<"u"&&localStorage.setItem(r,e==="light"||e==="dark"?e:"")}const l=()=>matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";function t(e){StarlightThemeProvider.updatePickers(e),document.documentElement.dataset.theme=e==="auto"?l():e,n(e)}matchMedia("(prefers-color-scheme: light)").addEventListener("change",()=>{c()==="auto"&&t("auto")});class s extends HTMLElement{constructor(){super(),t(c()),this.querySelector("select")?.addEventListener("change",a=>{a.currentTarget instanceof HTMLSelectElement&&t(o(a.currentTarget.value))})}}customElements.define("starlight-theme-select",s);</script> <script type="module">class s extends HTMLElement{constructor(){super();const e=this.querySelector("select");e&&(e.addEventListener("change",t=>{t.currentTarget instanceof HTMLSelectElement&&(window.location.pathname=t.currentTarget.value)}),window.addEventListener("pageshow",t=>{if(!t.persisted)return;const n=e.querySelector("option[selected]")?.index;n!==e.selectedIndex&&(e.selectedIndex=n??0)}))}}customElements.define("starlight-lang-select",s);</script> </div> </div> </header> <div class="main-frame astro-vrdttmbt"> <script type="module">const a=document.getElementById("starlight__sidebar"),n=a?.querySelector("sl-sidebar-state-persist"),o="sl-sidebar-state",i=()=>{let t=[];const e=n?.dataset.hash||"";try{const s=sessionStorage.getItem(o),r=JSON.parse(s||"{}");Array.isArray(r.open)&&r.hash===e&&(t=r.open)}catch{}return{hash:e,open:t,scroll:a?.scrollTop||0}},c=t=>{try{sessionStorage.setItem(o,JSON.stringify(t))}catch{}},d=()=>c(i()),l=(t,e)=>{const s=i();s.open[e]=t,c(s)};n?.addEventListener("click",t=>{if(!(t.target instanceof Element))return;const e=t.target.closest("summary")?.closest("details");if(!e)return;const s=e.querySelector("sl-sidebar-restore"),r=parseInt(s?.dataset.index||"");isNaN(r)||l(!e.open,r)});addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&d()});addEventListener("pageHide",d);</script> <div class="lg:sl-flex astro-67yu43on"> <div class="main-pane astro-67yu43on"> <main class="astro-bguv2lll" lang="en" dir="ltr"> <div class="content-panel astro-7nkwcw3z"> <div class="sl-container astro-7nkwcw3z"> <div class="hero astro-jbfsktt5"> <div class="sl-flex stack astro-jbfsktt5"> <div class="sl-flex copy astro-jbfsktt5"> <h1 id="_top" data-page-title class="astro-jbfsktt5">404</h1> <div class="tagline astro-jbfsktt5">Page not found. Check the URL or try using the search bar.</div> </div> </div> </div> <div class="sl-markdown-content"> </div> <footer class="sl-flex astro-3yyafb3n"> <div class="meta sl-flex astro-3yyafb3n"> </div> <div class="pagination-links print:hidden astro-u2l5gyhi" dir="ltr"> </div> </footer> </div> </div> </main> </div> </div> </div> </div> </body></html>
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
import{S as r}from"./TableOfContents.astro_astro_type_script_index_0_lang.CKWWgpjV.js";class c extends r{set current(e){super.current=e;const t=this.querySelector(".display-current");t&&(t.textContent=e.textContent)}constructor(){super();const e=this.querySelector("details");if(!e)return;const t=()=>{e.open=!1};e.querySelectorAll("a").forEach(s=>{s.addEventListener("click",t)}),window.addEventListener("click",s=>{e.contains(s.target)||t()}),window.addEventListener("keydown",s=>{if(s.key==="Escape"&&e.open){const o=e.contains(document.activeElement);if(t(),o){const n=e.querySelector("summary");n&&n.focus()}}})}}customElements.define("mobile-starlight-toc",c);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
const y="modulepreload",w=function(f){return"/"+f},g={},S=function(m,l,c){let h=Promise.resolve();if(l&&l.length>0){let o=function(n){return Promise.all(n.map(a=>Promise.resolve(a).then(r=>({status:"fulfilled",value:r}),r=>({status:"rejected",reason:r}))))};document.getElementsByTagName("link");const t=document.querySelector("meta[property=csp-nonce]"),u=t?.nonce||t?.getAttribute("nonce");h=o(l.map(n=>{if(n=w(n),n in g)return;g[n]=!0;const a=n.endsWith(".css"),r=a?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${n}"]${r}`))return;const e=document.createElement("link");if(e.rel=a?"stylesheet":y,a||(e.as="script"),e.crossOrigin="",e.href=n,u&&e.setAttribute("nonce",u),document.head.appendChild(e),a)return new Promise((i,s)=>{e.addEventListener("load",i),e.addEventListener("error",()=>s(new Error(`Unable to preload CSS for ${n}`)))})}))}function d(o){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=o,window.dispatchEvent(t),!t.defaultPrevented)throw o}return h.then(o=>{for(const t of o||[])t.status==="rejected"&&d(t.reason);return m().catch(d)})},E={ranking:{pageLength:.1,termFrequency:.1,termSaturation:2,termSimilarity:9}};class v extends HTMLElement{constructor(){super();const m=this.querySelector("button[data-open-modal]"),l=this.querySelector("button[data-close-modal]"),c=this.querySelector("dialog"),h=this.querySelector(".dialog-frame"),d=e=>{("href"in(e.target||{})||document.body.contains(e.target)&&!h.contains(e.target))&&t()},o=e=>{c.showModal(),document.body.toggleAttribute("data-search-modal-open",!0),this.querySelector("input")?.focus(),e?.stopPropagation(),window.addEventListener("click",d)},t=()=>c.close();m.addEventListener("click",o),m.disabled=!1,l.addEventListener("click",t),c.addEventListener("close",()=>{document.body.toggleAttribute("data-search-modal-open",!1),window.removeEventListener("click",d)}),window.addEventListener("keydown",e=>{(e.metaKey===!0||e.ctrlKey===!0)&&e.key==="k"&&(c.open?t():o(),e.preventDefault())});let u={};try{u=JSON.parse(this.dataset.translations||"{}")}catch{}const r=this.dataset.stripTrailingSlash!==void 0?e=>e.replace(/(.)\/(#.*)?$/,"$1$2"):e=>e;window.addEventListener("DOMContentLoaded",()=>{(window.requestIdleCallback||(i=>setTimeout(i,1)))(async()=>{const{PagefindUI:i}=await S(async()=>{const{PagefindUI:s}=await import("./ui-core.C0u4jL1_.js");return{PagefindUI:s}},[]);new i({...E,element:"#starlight__search",baseUrl:"/",bundlePath:"/".replace(/\/$/,"")+"/pagefind/",showImages:!1,translations:u,showSubResults:!0,processResult:s=>{s.url=r(s.url),s.sub_results=s.sub_results.map(p=>(p.url=r(p.url),p))}})})})}}customElements.define("site-search",v);export{S as _};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
const g="_top";class f extends HTMLElement{constructor(){super(),this._current=this.querySelector('a[aria-current="true"]'),this.minH=parseInt(this.dataset.minH||"2",10),this.maxH=parseInt(this.dataset.maxH||"3",10),this.onIdle=e=>(window.requestIdleCallback||(o=>setTimeout(o,1)))(e),this.init=()=>{const e=[...this.querySelectorAll("a")],o=t=>{if(t instanceof HTMLHeadingElement){if(t.id===g)return!0;const s=t.tagName[1];if(s){const n=parseInt(s,10);if(n>=this.minH&&n<=this.maxH)return!0}}return!1},i=t=>{if(!t)return null;const s=t;for(;t;){if(o(t))return t;for(t=t.previousElementSibling;t?.lastElementChild;)t=t.lastElementChild;const n=i(t);if(n)return n}return i(s.parentElement)},c=t=>{for(const{isIntersecting:s,target:n}of t){if(!s)continue;const l=i(n);if(!l)continue;const m=e.find(d=>d.hash==="#"+encodeURIComponent(l.id));if(m){this.current=m;break}}},a=document.querySelectorAll("main [id], main [id] ~ *, main .content > *");let r;const u=()=>{r||(r=new IntersectionObserver(c,{rootMargin:this.getRootMargin()}),a.forEach(t=>r.observe(t)))};u();let h;window.addEventListener("resize",()=>{r&&(r.disconnect(),r=void 0),clearTimeout(h),h=setTimeout(()=>this.onIdle(u),200)})},this.onIdle(()=>this.init())}set current(e){e!==this._current&&(this._current&&this._current.removeAttribute("aria-current"),e.setAttribute("aria-current","true"),this._current=e)}getRootMargin(){const e=document.querySelector("header")?.getBoundingClientRect().height||0,o=this.querySelector("summary")?.getBoundingClientRect().height||0,i=e+o+32,c=i+53,a=document.documentElement.clientHeight;return`-${i}px 0% ${c-a}px`}}customElements.define("starlight-toc",f);export{f as S};
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
try{(()=>{function a(e){if(!e)return;let t=e.getAttribute("tabindex")!==null,r=e.scrollWidth>e.clientWidth;r&&!t?(e.setAttribute("tabindex","0"),e.setAttribute("role","region")):!r&&t&&(e.removeAttribute("tabindex"),e.removeAttribute("role"))}var u=window.requestIdleCallback||(e=>setTimeout(e,1)),s=window.cancelIdleCallback||clearTimeout;function l(e){let t=new Set,r,n;return new ResizeObserver(c=>{c.forEach(o=>t.add(o.target)),r&&clearTimeout(r),n&&s(n),r=setTimeout(()=>{n&&s(n),n=u(()=>{t.forEach(o=>e(o)),t.clear()})},250)})}function i(e,t){e.querySelectorAll?.(".expressive-code pre > code").forEach(r=>{let n=r.parentElement;n&&t.observe(n)})}var d=l(a);i(document,d);var b=new MutationObserver(e=>e.forEach(t=>t.addedNodes.forEach(r=>{i(r,d)})));b.observe(document.body,{childList:!0,subtree:!0});document.addEventListener("astro:page-load",()=>{i(document,d)});})();}catch(e){console.error("[EC] tabindex-js-module failed:",e)}
|
||||||
|
try{(()=>{function l(o){let e=document.createElement("pre");Object.assign(e.style,{opacity:"0",pointerEvents:"none",position:"absolute",overflow:"hidden",left:"0",top:"0",width:"20px",height:"20px",webkitUserSelect:"auto",userSelect:"all"}),e.ariaHidden="true",e.textContent=o,document.body.appendChild(e);let a=document.createRange();a.selectNode(e);let n=getSelection();if(!n)return!1;n.removeAllRanges(),n.addRange(a);let r=!1;try{r=document.execCommand("copy")}finally{n.removeAllRanges(),document.body.removeChild(e)}return r}async function u(o){let e=o.currentTarget,a=e.dataset,n=!1,r=a.code.replace(/\u007f/g,`
|
||||||
|
`);try{await navigator.clipboard.writeText(r),n=!0}catch{n=l(r)}if(!n||e.parentNode?.querySelector(".feedback"))return;let c=e.parentNode?.querySelector("[aria-live]"),t=document.createElement("div");t.classList.add("feedback"),t.append(a.copied),c.append(t),t.offsetWidth,requestAnimationFrame(()=>t?.classList.add("show"));let s=()=>!t||t.classList.remove("show"),d=()=>{!t||parseFloat(getComputedStyle(t).opacity)>0||(t.remove(),t=void 0)};setTimeout(s,1500),setTimeout(d,2500),e.addEventListener("blur",s),t.addEventListener("transitioncancel",d),t.addEventListener("transitionend",d)}function i(o){o.querySelectorAll?.(".expressive-code .copy button").forEach(e=>e.addEventListener("click",u))}i(document);var m=new MutationObserver(o=>o.forEach(e=>e.addedNodes.forEach(a=>{i(a)})));m.observe(document.body,{childList:!0,subtree:!0});document.addEventListener("astro:page-load",()=>{i(document)});})();}catch(e){console.error("[EC] copy-js-module failed:",e)}
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
const v={},d=new Set,s=new WeakSet;let f=!0,h,l=!1;function g(e){l||(l=!0,f??=!1,h??="hover",p(),w(),y(),S())}function p(){for(const e of["touchstart","mousedown"])document.addEventListener(e,t=>{a(t.target,"tap")&&c(t.target.href,{ignoreSlowConnection:!0})},{passive:!0})}function w(){let e;document.body.addEventListener("focusin",n=>{a(n.target,"hover")&&t(n)},{passive:!0}),document.body.addEventListener("focusout",o,{passive:!0}),u(()=>{for(const n of document.getElementsByTagName("a"))s.has(n)||a(n,"hover")&&(s.add(n),n.addEventListener("mouseenter",t,{passive:!0}),n.addEventListener("mouseleave",o,{passive:!0}))});function t(n){const r=n.target.href;e&&clearTimeout(e),e=setTimeout(()=>{c(r)},80)}function o(){e&&(clearTimeout(e),e=0)}}function y(){let e;u(()=>{for(const t of document.getElementsByTagName("a"))s.has(t)||a(t,"viewport")&&(s.add(t),e??=L(),e.observe(t))})}function L(){const e=new WeakMap;return new IntersectionObserver((t,o)=>{for(const n of t){const r=n.target,i=e.get(r);n.isIntersecting?(i&&clearTimeout(i),e.set(r,setTimeout(()=>{o.unobserve(r),e.delete(r),c(r.href)},300))):i&&(clearTimeout(i),e.delete(r))}})}function S(){u(()=>{for(const e of document.getElementsByTagName("a"))a(e,"load")&&c(e.href)})}function c(e,t){e=e.replace(/#.*/,"");const o=t?.ignoreSlowConnection??!1;if(E(e,o))if(d.add(e),document.createElement("link").relList?.supports?.("prefetch")&&t?.with!=="fetch"){const n=document.createElement("link");n.rel="prefetch",n.setAttribute("href",e),document.head.append(n)}else{const n=new Headers;for(const[r,i]of Object.entries(v))n.set(r,i);fetch(e,{priority:"low",headers:n})}}function E(e,t){if(!navigator.onLine||!t&&m())return!1;try{const o=new URL(e,location.href);return location.origin===o.origin&&(location.pathname!==o.pathname||location.search!==o.search)&&!d.has(e)}catch{}return!1}function a(e,t){if(e?.tagName!=="A")return!1;const o=e.dataset.astroPrefetch;return o==="false"?!1:t==="tap"&&(o!=null||f)&&m()?!0:o==null&&f||o===""?t===h:o===t}function m(){if("connection"in navigator){const e=navigator.connection;return e.saveData||/2g/.test(e.effectiveType)}return!1}function u(e){e();let t=!1;document.addEventListener("astro:page-load",()=>{if(!t){t=!0;return}e()})}g();
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
@media print{:root{--sl-color-white: hsl(224, 10%, 10%);--sl-color-gray-1: hsl(224, 14%, 16%);--sl-color-gray-2: hsl(224, 10%, 23%);--sl-color-gray-3: hsl(224, 7%, 36%);--sl-color-gray-4: hsl(224, 6%, 56%);--sl-color-gray-5: hsl(224, 6%, 77%);--sl-color-gray-6: hsl(224, 20%, 94%);--sl-color-gray-7: hsl(224, 19%, 97%);--sl-color-black: hsl(0, 0%, 100%);--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);--sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);--sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);--sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);--sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);--sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);--sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);--sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);--sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);--sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);--sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);--sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);--sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);--sl-color-accent-high: hsl(234, 80%, 30%);--sl-color-accent: hsl(234, 90%, 60%);--sl-color-accent-low: hsl(234, 88%, 90%);--sl-color-text-accent: var(--sl-color-accent);--sl-color-text-invert: var(--sl-color-black);--sl-color-bg-nav: var(--sl-color-gray-7);--sl-color-bg-sidebar: var(--sl-color-bg);--sl-color-bg-inline-code: var(--sl-color-gray-6);--sl-color-bg-accent: var(--sl-color-accent);--sl-color-hairline-light: var(--sl-color-gray-6);--sl-color-hairline-shade: var(--sl-color-gray-6);--sl-color-backdrop-overlay: hsla(225, 9%, 36%, .66);--sl-shadow-sm: none;--sl-shadow-md: none;--sl-shadow-lg: none}.print\:hidden{display:none}.print\:flex{display:flex}.print\:block{display:block}main{padding-bottom:0}main>.content-panel{padding-block-start:0}.content-panel+.content-panel{border:0}.page>header{position:relative}.page>.main-frame{padding-top:0;padding-inline-start:0}.main-pane{--sl-sidebar-width: 0px;--sl-content-width: 100%}.sl-banner{--sl-color-banner-text: var(--sl-color-white);background-color:transparent}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6){break-after:avoid}.sl-markdown-content :is(p,li){orphans:2;widows:2}.sl-markdown-content pre{overflow-x:hidden;white-space:pre-wrap}.sl-markdown-content .expressive-code,.sl-markdown-content figure,.sl-markdown-content pre{break-inside:avoid}.expressive-code .frame.is-terminal .header:before{box-shadow:inset 99rem 99rem var(--sl-color-gray-5)}.expressive-code .frame.has-title:not(.is-terminal) .header{background:transparent;border-bottom:1px solid var(--sl-color-gray-6)}.expressive-code .frame.has-title:not(.is-terminal) .title{background:transparent}.expressive-code .frame.has-title:not(.is-terminal) .title:after{border-top:0}.expressive-code .copy{display:none}.sl-markdown-content code:not(:where(.not-content *)){background-color:transparent;padding:0;margin-block:unset;font-size:.9375em}.sl-badge{background:transparent;color:var(--sl-color-white)}starlight-file-tree{break-inside:avoid}starlight-file-tree .highlight{outline:3px solid var(--sl-color-accent-low);color:var(--sl-color-text);background-color:transparent}.starlight-aside{break-inside:avoid}.sl-link-button.primary{background:transparent;border-color:var(--sl-color-white);color:var(--sl-color-white)}starlight-tabs{break-inside:avoid}.sl-steps>li:after{box-shadow:inset 99rem 99rem var(--sl-color-hairline-light)}}
|
||||||
Vendored
+2
File diff suppressed because one or more lines are too long
+180
File diff suppressed because one or more lines are too long
Vendored
+100
File diff suppressed because one or more lines are too long
Vendored
+116
File diff suppressed because one or more lines are too long
+99
File diff suppressed because one or more lines are too long
Vendored
+140
File diff suppressed because one or more lines are too long
+122
File diff suppressed because one or more lines are too long
+106
File diff suppressed because one or more lines are too long
Vendored
+138
File diff suppressed because one or more lines are too long
Vendored
+174
File diff suppressed because one or more lines are too long
Vendored
+347
File diff suppressed because one or more lines are too long
Vendored
+111
File diff suppressed because one or more lines are too long
Vendored
+249
File diff suppressed because one or more lines are too long
Vendored
+106
File diff suppressed because one or more lines are too long
Vendored
+71
@@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html><html lang="en" dir="ltr" data-theme="dark" data-has-hero class="astro-bguv2lll"> <head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>loom — conventions for what a repository knows</title><link rel="canonical" href="https://loom.hypertheory-labs.dev/"/><link rel="sitemap" href="/sitemap-index.xml"/><link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml"/><meta name="generator" content="Astro v5.18.2"/><meta name="generator" content="Starlight v0.36.3"/><meta property="og:title" content="loom"/><meta property="og:type" content="article"/><meta property="og:url" content="https://loom.hypertheory-labs.dev/"/><meta property="og:locale" content="en"/><meta property="og:description" content="Conventions for holding what a repository knows, so somebody arriving can find it."/><meta property="og:site_name" content="loom"/><meta name="twitter:card" content="summary_large_image"/><meta name="description" content="Conventions for holding what a repository knows, so somebody arriving can find it."/><script>
|
||||||
|
window.StarlightThemeProvider = (() => {
|
||||||
|
const storedTheme =
|
||||||
|
typeof localStorage !== 'undefined' && localStorage.getItem('starlight-theme');
|
||||||
|
const theme =
|
||||||
|
storedTheme ||
|
||||||
|
(window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
||||||
|
document.documentElement.dataset.theme = theme === 'light' ? 'light' : 'dark';
|
||||||
|
return {
|
||||||
|
updatePickers(theme = storedTheme || 'auto') {
|
||||||
|
document.querySelectorAll('starlight-theme-select').forEach((picker) => {
|
||||||
|
const select = picker.querySelector('select');
|
||||||
|
if (select) select.value = theme;
|
||||||
|
/** @type {HTMLTemplateElement | null} */
|
||||||
|
const tmpl = document.querySelector(`#theme-icons`);
|
||||||
|
const newIcon = tmpl && tmpl.content.querySelector('.' + theme);
|
||||||
|
if (newIcon) {
|
||||||
|
const oldIcon = picker.querySelector('svg.label-icon');
|
||||||
|
if (oldIcon) {
|
||||||
|
oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script><template id="theme-icons"><svg aria-hidden="true" class="light astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M5 12a1 1 0 0 0-1-1H3a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Zm.64 5-.71.71a1 1 0 0 0 0 1.41 1 1 0 0 0 1.41 0l.71-.71A1 1 0 0 0 5.64 17ZM12 5a1 1 0 0 0 1-1V3a1 1 0 0 0-2 0v1a1 1 0 0 0 1 1Zm5.66 2.34a1 1 0 0 0 .7-.29l.71-.71a1 1 0 1 0-1.41-1.41l-.66.71a1 1 0 0 0 0 1.41 1 1 0 0 0 .66.29Zm-12-.29a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.71-.71a1.004 1.004 0 1 0-1.43 1.41l.73.71ZM21 11h-1a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2Zm-2.64 6A1 1 0 0 0 17 18.36l.71.71a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.76-.66ZM12 6.5a5.5 5.5 0 1 0 5.5 5.5A5.51 5.51 0 0 0 12 6.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0 3.5a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1a1 1 0 0 0-1-1Z"/></svg><svg aria-hidden="true" class="dark astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.64 13a1 1 0 0 0-1.05-.14 8.049 8.049 0 0 1-3.37.73 8.15 8.15 0 0 1-8.14-8.1 8.59 8.59 0 0 1 .25-2A1 1 0 0 0 8 2.36a10.14 10.14 0 1 0 14 11.69 1 1 0 0 0-.36-1.05Zm-9.5 6.69A8.14 8.14 0 0 1 7.08 5.22v.27a10.15 10.15 0 0 0 10.14 10.14 9.784 9.784 0 0 0 2.1-.22 8.11 8.11 0 0 1-7.18 4.32v-.04Z"/></svg><svg aria-hidden="true" class="auto astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg></template><link rel="stylesheet" href="/_astro/print.DNXP8c50.css" media="print"><link rel="stylesheet" href="/_astro/index.Bl8FUrqd.css">
|
||||||
|
<style>@layer starlight.components{:root{--sl-badge-default-border: var(--sl-color-accent);--sl-badge-default-bg: var(--sl-color-accent-low);--sl-badge-default-text: #fff;--sl-badge-note-border: var(--sl-color-blue);--sl-badge-note-bg: var(--sl-color-blue-low);--sl-badge-note-text: #fff;--sl-badge-danger-border: var(--sl-color-red);--sl-badge-danger-bg: var(--sl-color-red-low);--sl-badge-danger-text: #fff;--sl-badge-success-border: var(--sl-color-green);--sl-badge-success-bg: var(--sl-color-green-low);--sl-badge-success-text: #fff;--sl-badge-caution-border: var(--sl-color-orange);--sl-badge-caution-bg: var(--sl-color-orange-low);--sl-badge-caution-text: #fff;--sl-badge-tip-border: var(--sl-color-purple);--sl-badge-tip-bg: var(--sl-color-purple-low);--sl-badge-tip-text: #fff}[data-theme=light]:root{--sl-badge-default-bg: var(--sl-color-accent-high);--sl-badge-note-bg: var(--sl-color-blue-high);--sl-badge-danger-bg: var(--sl-color-red-high);--sl-badge-success-bg: var(--sl-color-green-high);--sl-badge-caution-bg: var(--sl-color-orange-high);--sl-badge-tip-bg: var(--sl-color-purple-high)}.sl-badge:where(.astro-avdet4wd){display:inline-block;border:1px solid var(--sl-color-border-badge);border-radius:.25rem;font-family:var(--sl-font-system-mono);line-height:normal;color:var(--sl-color-text-badge);background-color:var(--sl-color-bg-badge);overflow-wrap:anywhere}.sidebar-content .sl-badge:where(.astro-avdet4wd){line-height:1;font-size:var(--sl-text-xs);padding:.125rem .375rem}.sidebar-content a[aria-current=page]>.sl-badge:where(.astro-avdet4wd){--sl-color-bg-badge: transparent;--sl-color-border-badge: currentColor;color:inherit}.default:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-default-bg);--sl-color-border-badge: var(--sl-badge-default-border);--sl-color-text-badge: var(--sl-badge-default-text)}.note:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-note-bg);--sl-color-border-badge: var(--sl-badge-note-border);--sl-color-text-badge: var(--sl-badge-note-text)}.danger:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-danger-bg);--sl-color-border-badge: var(--sl-badge-danger-border);--sl-color-text-badge: var(--sl-badge-danger-text)}.success:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-success-bg);--sl-color-border-badge: var(--sl-badge-success-border);--sl-color-text-badge: var(--sl-badge-success-text)}.tip:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-tip-bg);--sl-color-border-badge: var(--sl-badge-tip-border);--sl-color-text-badge: var(--sl-badge-tip-text)}.caution:where(.astro-avdet4wd){--sl-color-bg-badge: var(--sl-badge-caution-bg);--sl-color-border-badge: var(--sl-badge-caution-border);--sl-color-text-badge: var(--sl-badge-caution-text)}.small:where(.astro-avdet4wd){font-size:var(--sl-text-xs);padding:.125rem .25rem}.medium:where(.astro-avdet4wd){font-size:var(--sl-text-sm);padding:.175rem .35rem}.large:where(.astro-avdet4wd){font-size:var(--sl-text-base);padding:.225rem .45rem}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6) .sl-badge:where(.astro-avdet4wd){vertical-align:middle}}
|
||||||
|
@layer starlight.components{.card:where(.astro-v5tidmuc){--sl-card-border: var(--sl-color-purple);--sl-card-bg: var(--sl-color-purple-low);border:1px solid var(--sl-color-gray-5);background-color:var(--sl-color-black);padding:clamp(1rem,calc(.125rem + 3vw),2.5rem);flex-direction:column;gap:clamp(.5rem,calc(.125rem + 1vw),1rem)}.card:where(.astro-v5tidmuc):nth-child(4n+1){--sl-card-border: var(--sl-color-orange);--sl-card-bg: var(--sl-color-orange-low)}.card:where(.astro-v5tidmuc):nth-child(4n+3){--sl-card-border: var(--sl-color-green);--sl-card-bg: var(--sl-color-green-low)}.card:where(.astro-v5tidmuc):nth-child(4n+4){--sl-card-border: var(--sl-color-red);--sl-card-bg: var(--sl-color-red-low)}.card:where(.astro-v5tidmuc):nth-child(4n+5){--sl-card-border: var(--sl-color-blue);--sl-card-bg: var(--sl-color-blue-low)}.title:where(.astro-v5tidmuc){font-weight:600;font-size:var(--sl-text-h4);color:var(--sl-color-white);line-height:var(--sl-line-height-headings);gap:1rem;align-items:center}.card:where(.astro-v5tidmuc) .icon:where(.astro-v5tidmuc){border:1px solid var(--sl-card-border);background-color:var(--sl-card-bg);padding:.2em;border-radius:.25rem;flex-shrink:0}.card:where(.astro-v5tidmuc) .body:where(.astro-v5tidmuc){margin:0;font-size:clamp(var(--sl-text-sm),calc(.5rem + 1vw),var(--sl-text-body))}}
|
||||||
|
@layer starlight.components{.card-grid:where(.astro-zntqmydn){display:grid;grid-template-columns:100%;gap:1rem}.card-grid:where(.astro-zntqmydn)>*{margin-top:0!important}@media(min-width:50rem){.card-grid:where(.astro-zntqmydn){grid-template-columns:1fr 1fr;gap:1.5rem}.stagger:where(.astro-zntqmydn){--stagger-height: 5rem;padding-bottom:var(--stagger-height)}.stagger:where(.astro-zntqmydn)>*:nth-child(2n){transform:translateY(var(--stagger-height))}}}
|
||||||
|
@layer starlight.components{svg:where(.astro-c6vsoqas){color:var(--sl-icon-color);font-size:var(--sl-icon-size, 1em);width:1em;height:1em}}
|
||||||
|
@layer starlight.components{.sl-steps{--bullet-size: calc(var(--sl-line-height) * 1rem);--bullet-margin: .375rem;list-style:none;counter-reset:steps-counter var(--sl-steps-start, 0);padding-inline-start:0}.sl-steps>li{counter-increment:steps-counter;position:relative;padding-inline-start:calc(var(--bullet-size) + 1rem);padding-bottom:1px;min-height:calc(var(--bullet-size) + var(--bullet-margin))}.sl-steps>li+li{margin-top:0}.sl-steps>li:before{content:counter(steps-counter);position:absolute;top:0;inset-inline-start:0;width:var(--bullet-size);height:var(--bullet-size);line-height:var(--bullet-size);font-size:var(--sl-text-xs);font-weight:600;text-align:center;color:var(--sl-color-white);background-color:var(--sl-color-gray-6);border-radius:99rem;box-shadow:inset 0 0 0 1px var(--sl-color-gray-5)}.sl-steps>li:after{--guide-width: 1px;content:"";position:absolute;top:calc(var(--bullet-size) + var(--bullet-margin));bottom:var(--bullet-margin);inset-inline-start:calc((var(--bullet-size) - var(--guide-width)) / 2);width:var(--guide-width);background-color:var(--sl-color-hairline-light)}}@layer starlight.content{.sl-steps>li>:first-child{--lh: calc(1em * var(--sl-line-height));--shift-y: calc(.5 * (var(--bullet-size) - var(--lh)));transform:translateY(var(--shift-y));margin-bottom:var(--shift-y)}.sl-steps>li>:first-child:where(h1,h2,h3,h4,h5,h6){--lh: calc(1em * var(--sl-line-height-headings))}@supports (--prop: 1lh){.sl-steps>li>:first-child{--lh: 1lh}}}
|
||||||
|
@layer starlight.components{.sl-link-button:where(.astro-xwgiixxa){align-items:center;border:1px solid transparent;border-radius:999rem;display:inline-flex;font-size:var(--sl-text-sm);gap:.5em;line-height:1.1875;outline-offset:.25rem;padding:.4375rem 1.125rem;text-decoration:none}.sl-link-button:where(.astro-xwgiixxa).primary{background:var(--sl-color-text-accent);border-color:var(--sl-color-text-accent);color:var(--sl-color-black)}.sl-link-button:where(.astro-xwgiixxa).primary:hover{color:var(--sl-color-black)}.sl-link-button:where(.astro-xwgiixxa).secondary{border-color:inherit;color:var(--sl-color-white)}.sl-link-button:where(.astro-xwgiixxa).minimal{color:var(--sl-color-white);padding-inline:0}.sl-link-button:where(.astro-xwgiixxa) svg{flex-shrink:0}@media(min-width:50rem){.sl-link-button:where(.astro-xwgiixxa){font-size:var(--sl-text-base);padding:.9375rem 1.25rem}}.sl-markdown-content .sl-link-button:where(.astro-xwgiixxa){margin-inline-end:1rem}.sl-markdown-content .sl-link-button:where(.astro-xwgiixxa):not(:where(p *)){margin-block:1rem}}
|
||||||
|
</style><script type="module" src="/_astro/page.B1D-nYk3.js"></script></head> <body class="astro-bguv2lll"> <a href="#_top" class="astro-7q3lir66">Skip to content</a> <div class="page sl-flex astro-vrdttmbt"> <header class="header astro-vrdttmbt"><div class="header astro-kmkmnagf"> <div class="title-wrapper sl-flex astro-kmkmnagf"> <a href="/" class="site-title sl-flex astro-m46x6ez3"> <span class="astro-m46x6ez3" translate="no"> loom </span> </a> </div> <div class="sl-flex print:hidden astro-kmkmnagf"> <site-search class="astro-kmkmnagf astro-v37mnknz" data-translations="{"placeholder":"Search"}"> <button data-open-modal disabled aria-label="Search" aria-keyshortcuts="Control+K" class="astro-v37mnknz"> <svg aria-hidden="true" class="astro-v37mnknz astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.71 20.29 18 16.61A9 9 0 1 0 16.61 18l3.68 3.68a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.39ZM11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z"/></svg> <span class="sl-hidden md:sl-block astro-v37mnknz" aria-hidden="true">Search</span> <kbd class="sl-hidden md:sl-flex astro-v37mnknz" style="display: none;"> <kbd class="astro-v37mnknz">Ctrl</kbd><kbd class="astro-v37mnknz">K</kbd> </kbd> </button> <dialog style="padding:0" aria-label="Search" class="astro-v37mnknz"> <div class="dialog-frame sl-flex astro-v37mnknz"> <button data-close-modal class="sl-flex md:sl-hidden astro-v37mnknz"> Cancel </button> <div class="search-container astro-v37mnknz"> <div id="starlight__search" class="astro-v37mnknz"></div> </div> </div> </dialog> </site-search> <script>
|
||||||
|
(() => {
|
||||||
|
const openBtn = document.querySelector('button[data-open-modal]');
|
||||||
|
const shortcut = openBtn?.querySelector('kbd');
|
||||||
|
if (!openBtn || !(shortcut instanceof HTMLElement)) return;
|
||||||
|
const platformKey = shortcut.querySelector('kbd');
|
||||||
|
if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
|
||||||
|
platformKey.textContent = '⌘';
|
||||||
|
openBtn.setAttribute('aria-keyshortcuts', 'Meta+K');
|
||||||
|
}
|
||||||
|
shortcut.style.display = '';
|
||||||
|
})();
|
||||||
|
</script> <script type="module" src="/_astro/Search.astro_astro_type_script_index_0_lang.CJiOmi4V.js"></script> </div> <div class="sl-hidden md:sl-flex print:hidden right-group astro-kmkmnagf"> <div class="sl-flex social-icons astro-kmkmnagf"> <a href="https://git.hypertheory-labs.dev/loom" rel="me" class="sl-flex astro-wy4te6ga"><span class="sr-only astro-wy4te6ga">Source</span><svg aria-hidden="true" class="astro-wy4te6ga astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M0.355 12.231L0.355 12.231L0.355 11.853Q0.397 11.517 0.565 11.265L0.565 11.265Q0.691 11.097 0.943 10.803L0.943 10.803L1.279 10.425L7.831 3.831L7.873 3.873Q7.957 3.873 7.957 3.957L7.957 3.957L10.309 6.351Q10.519 6.519 10.309 6.729L10.309 6.729Q10.225 7.317 10.435 7.863Q10.645 8.409 11.107 8.703L11.107 8.703Q11.275 8.787 11.317 8.892Q11.359 8.997 11.359 9.207L11.359 9.207L11.359 15.003Q11.359 15.255 11.107 15.507L11.107 15.507Q10.603 15.843 10.372 16.389Q10.141 16.935 10.330 17.502Q10.519 18.069 10.981 18.447Q11.443 18.825 12.031 18.825Q12.619 18.825 13.081 18.489Q13.543 18.153 13.732 17.586Q13.921 17.019 13.753 16.473Q13.585 15.927 13.081 15.507L13.081 15.507Q12.913 15.423 12.871 15.318Q12.829 15.213 12.829 15.003L12.829 15.003L12.829 9.081L12.955 9.081L15.055 11.181Q15.139 11.265 15.139 11.391L15.139 11.391L15.181 11.475L15.181 12.231Q15.265 13.029 15.874 13.470Q16.483 13.911 17.281 13.806Q18.079 13.701 18.562 13.029Q19.045 12.357 18.835 11.601L18.835 11.601Q18.751 10.929 18.100 10.488Q17.449 10.047 16.735 10.131L16.735 10.131Q16.483 10.131 16.231 10.005L16.231 10.005L14.005 7.779Q13.879 7.653 13.879 7.401L13.879 7.401Q14.005 6.813 13.711 6.267Q13.417 5.721 12.871 5.448Q12.325 5.175 11.779 5.301L11.779 5.301Q11.359 5.301 11.359 5.175L11.359 5.175Q9.805 3.579 9.007 2.907L9.007 2.907Q8.797 2.739 9.007 2.529L9.007 2.529Q9.469 2.193 10.183 1.353L10.183 1.353L10.729 0.807Q11.905-0.369 13.081 0.807L13.081 0.807L23.035 10.803Q24.253 11.979 23.035 13.155L23.035 13.155L13.291 22.941Q12.913 23.319 12.745 23.445L12.745 23.445Q12.409 23.739 12.031 23.781L12.031 23.781L11.779 23.781L11.569 23.697Q11.065 23.487 10.855 23.277L10.855 23.277L10.057 22.479Q8.881 21.387 8.335 20.757L8.335 20.757L1.153 13.575Q1.069 13.365 0.775 13.029L0.775 13.029Q0.397 12.525 0.355 12.231Z"/></svg></a> </div> <starlight-theme-select> <label style="--sl-select-width: 6.25em" class="astro-4yphtoen"> <span class="sr-only astro-4yphtoen">Select theme</span> <svg aria-hidden="true" class="icon label-icon astro-4yphtoen astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg> <select autocomplete="off" class="astro-4yphtoen"> <option value="dark" class="astro-4yphtoen">Dark</option><option value="light" class="astro-4yphtoen">Light</option><option value="auto" selected class="astro-4yphtoen">Auto</option> </select> <svg aria-hidden="true" class="icon caret astro-4yphtoen astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg> </label> </starlight-theme-select> <script>
|
||||||
|
StarlightThemeProvider.updatePickers();
|
||||||
|
</script> <script type="module">const r="starlight-theme",o=e=>e==="auto"||e==="dark"||e==="light"?e:"auto",c=()=>o(typeof localStorage<"u"&&localStorage.getItem(r));function n(e){typeof localStorage<"u"&&localStorage.setItem(r,e==="light"||e==="dark"?e:"")}const l=()=>matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";function t(e){StarlightThemeProvider.updatePickers(e),document.documentElement.dataset.theme=e==="auto"?l():e,n(e)}matchMedia("(prefers-color-scheme: light)").addEventListener("change",()=>{c()==="auto"&&t("auto")});class s extends HTMLElement{constructor(){super(),t(c()),this.querySelector("select")?.addEventListener("change",a=>{a.currentTarget instanceof HTMLSelectElement&&t(o(a.currentTarget.value))})}}customElements.define("starlight-theme-select",s);</script> <script type="module">class s extends HTMLElement{constructor(){super();const e=this.querySelector("select");e&&(e.addEventListener("change",t=>{t.currentTarget instanceof HTMLSelectElement&&(window.location.pathname=t.currentTarget.value)}),window.addEventListener("pageshow",t=>{if(!t.persisted)return;const n=e.querySelector("option[selected]")?.index;n!==e.selectedIndex&&(e.selectedIndex=n??0)}))}}customElements.define("starlight-lang-select",s);</script> </div> </div> </header> <div class="main-frame astro-vrdttmbt"> <script type="module">const a=document.getElementById("starlight__sidebar"),n=a?.querySelector("sl-sidebar-state-persist"),o="sl-sidebar-state",i=()=>{let t=[];const e=n?.dataset.hash||"";try{const s=sessionStorage.getItem(o),r=JSON.parse(s||"{}");Array.isArray(r.open)&&r.hash===e&&(t=r.open)}catch{}return{hash:e,open:t,scroll:a?.scrollTop||0}},c=t=>{try{sessionStorage.setItem(o,JSON.stringify(t))}catch{}},d=()=>c(i()),l=(t,e)=>{const s=i();s.open[e]=t,c(s)};n?.addEventListener("click",t=>{if(!(t.target instanceof Element))return;const e=t.target.closest("summary")?.closest("details");if(!e)return;const s=e.querySelector("sl-sidebar-restore"),r=parseInt(s?.dataset.index||"");isNaN(r)||l(!e.open,r)});addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&d()});addEventListener("pageHide",d);</script> <div class="lg:sl-flex astro-67yu43on"> <div class="main-pane astro-67yu43on"> <main data-pagefind-body class="astro-bguv2lll" lang="en" dir="ltr"> <div class="content-panel astro-7nkwcw3z"> <div class="sl-container astro-7nkwcw3z"> <div class="hero astro-jbfsktt5"> <div class="sl-flex stack astro-jbfsktt5"> <div class="sl-flex copy astro-jbfsktt5"> <h1 id="_top" data-page-title class="astro-jbfsktt5">loom</h1> <div class="tagline astro-jbfsktt5">Conventions for holding what a repository knows — so that whoever arrives next, person or agent, can find it.</div> </div> <div class="sl-flex actions astro-jbfsktt5"> <a class="sl-link-button not-content primary astro-jbfsktt5 astro-xwgiixxa" href="/bedrock/starting/"> Start here <svg aria-hidden="true" class="astro-xwgiixxa astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17.92 11.62a1.001 1.001 0 0 0-.21-.33l-5-5a1.003 1.003 0 1 0-1.42 1.42l3.3 3.29H7a1 1 0 0 0 0 2h7.59l-3.3 3.29a1.002 1.002 0 0 0 .325 1.639 1 1 0 0 0 1.095-.219l5-5a1 1 0 0 0 .21-.33 1 1 0 0 0 0-.76Z"/></svg> </a> <a class="sl-link-button not-content minimal astro-jbfsktt5 astro-xwgiixxa" href="https://git.hypertheory-labs.dev/loom"> The repositories <svg aria-hidden="true" class="astro-xwgiixxa astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M19.33 10.18a1 1 0 0 1-.77 0 1 1 0 0 1-.62-.93l.01-1.83-8.2 8.2a1 1 0 0 1-1.41-1.42l8.2-8.2H14.7a1 1 0 0 1 0-2h4.25a1 1 0 0 1 1 1v4.25a1 1 0 0 1-.62.93Z"/><path d="M11 4a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h4Z"/></svg> </a> </div> </div> </div> <div class="sl-markdown-content"> <div class="sl-heading-wrapper level-h2"><h2 id="what-this-is">What this is</h2><a class="sl-anchor-link" href="#what-this-is"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only">Section titled “What this is”</span></a></div>
|
||||||
|
<p>A repository knows things nobody wrote down. Why the default ingress class is
|
||||||
|
the one that leaks. What was decided in March and what it was believed to buy.
|
||||||
|
What you tried that did not work. Somebody arriving — a new colleague, or an
|
||||||
|
agent asked to change one file — cannot find any of it, and the code cannot tell
|
||||||
|
them, because the code is the answer and not the question.</p>
|
||||||
|
<p><strong>These are conventions for holding that.</strong> Four of them, small, and each is a
|
||||||
|
document in a repository you can read.</p>
|
||||||
|
<div class="card-grid astro-zntqmydn"><article class="card sl-flex astro-v5tidmuc"> <p class="title sl-flex astro-v5tidmuc"> <svg aria-hidden="true" class="icon astro-v5tidmuc astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.333em;"></svg> <span class="astro-v5tidmuc">bedrock</span> </p> <div class="body astro-v5tidmuc"><p>The primitives. A <code dir="auto">.loom/</code> directory, what goes in it, and what publishing
|
||||||
|
something means. <a href="/bedrock/starting/">Start here</a>.</p></div> </article> <article class="card sl-flex astro-v5tidmuc"> <p class="title sl-flex astro-v5tidmuc"> <svg aria-hidden="true" class="icon astro-v5tidmuc astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.333em;"><path d="M8.7 10a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-6.27-6.3a1 1 0 0 0-1.42 1.42ZM21 14a1 1 0 0 0-1 1v3.59L15.44 14A1 1 0 0 0 14 15.44L18.59 20H15a1 1 0 0 0 0 2h6a1 1 0 0 0 .38-.08 1 1 0 0 0 .54-.54A1 1 0 0 0 22 21v-6a1 1 0 0 0-1-1Zm.92-11.38a1 1 0 0 0-.54-.54A1 1 0 0 0 21 2h-6a1 1 0 0 0 0 2h3.59L2.29 20.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L20 5.41V9a1 1 0 0 0 2 0V3a1 1 0 0 0-.08-.38Z"/></svg> <span class="astro-v5tidmuc">externals</span> </p> <div class="body astro-v5tidmuc"><p>How a repository records what it depends on — a copy, a lock, and what you
|
||||||
|
use it for. <a href="/externals/externals/">Read it</a>.</p></div> </article> <article class="card sl-flex astro-v5tidmuc"> <p class="title sl-flex astro-v5tidmuc"> <svg aria-hidden="true" class="icon astro-v5tidmuc astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.333em;"><path d="M22 7.24a1 1 0 0 0-.29-.71l-4.24-4.24a1 1 0 0 0-1.1-.22 1 1 0 0 0-.32.22l-2.83 2.83L2.29 16.05a1 1 0 0 0-.29.71V21a1 1 0 0 0 1 1h4.24a1 1 0 0 0 .76-.29l10.87-10.93L21.71 8c.1-.1.17-.2.22-.33a1 1 0 0 0 0-.24v-.14l.07-.05ZM6.83 20H4v-2.83l9.93-9.93 2.83 2.83L6.83 20ZM18.17 8.66l-2.83-2.83 1.42-1.41 2.82 2.82-1.41 1.42Z"/></svg> <span class="astro-v5tidmuc">annotating</span> </p> <div class="body astro-v5tidmuc"><p>Two parties who never edit each other’s text. <a href="/annotating/annotating/">Read it</a>.</p></div> </article> <article class="card sl-flex astro-v5tidmuc"> <p class="title sl-flex astro-v5tidmuc"> <svg aria-hidden="true" class="icon astro-v5tidmuc astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.333em;"><path d="M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z"/></svg> <span class="astro-v5tidmuc">cart</span> </p> <div class="body astro-v5tidmuc"><p>Two presences, one round, and never a third file.
|
||||||
|
<a href="/cart/cart/">Read it</a>.</p></div> </article> </div>
|
||||||
|
<div class="sl-heading-wrapper level-h2"><h2 id="this-site-is-an-instance-of-the-thing-it-documents">This site is an instance of the thing it documents</h2><a class="sl-anchor-link" href="#this-site-is-an-instance-of-the-thing-it-documents"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only">Section titled “This site is an instance of the thing it documents”</span></a></div>
|
||||||
|
<p>Every reference page here is a <strong>copy</strong> of a document published by one of those
|
||||||
|
repositories, adopted under <code dir="auto">externals</code>, locked to the version it was fetched
|
||||||
|
at, and checked against its source. Nothing here is written twice.</p>
|
||||||
|
<p>Which has a consequence worth stating plainly: <strong>a document has to be public to
|
||||||
|
appear here.</strong> Not as a policy — as arithmetic. The rule is <em>do not adopt from a
|
||||||
|
source less readable than the repository you are adopting into</em>, this site is on
|
||||||
|
the public internet, so its sources are public or they are not its sources.</p>
|
||||||
|
<p>The pages that are <em>not</em> copies are the section fronts — one per convention,
|
||||||
|
hand-written, holding worked examples and reading order. Those are stamped with
|
||||||
|
the version they were written against, and when a source moves they say so at
|
||||||
|
the top of the page rather than quietly going wrong.</p> </div> <footer class="sl-flex astro-3yyafb3n"> <div class="meta sl-flex astro-3yyafb3n"> <a href="https://git.hypertheory-labs.dev/loom/docs/_edit/main/src/content/docs/index.mdx" class="sl-flex print:hidden astro-eez2twj6"><svg aria-hidden="true" class="astro-eez2twj6 astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.2em;"><path d="M22 7.24a1 1 0 0 0-.29-.71l-4.24-4.24a1 1 0 0 0-1.1-.22 1 1 0 0 0-.32.22l-2.83 2.83L2.29 16.05a1 1 0 0 0-.29.71V21a1 1 0 0 0 1 1h4.24a1 1 0 0 0 .76-.29l10.87-10.93L21.71 8c.1-.1.17-.2.22-.33a1 1 0 0 0 0-.24v-.14l.07-.05ZM6.83 20H4v-2.83l9.93-9.93 2.83 2.83L6.83 20ZM18.17 8.66l-2.83-2.83 1.42-1.41 2.82 2.82-1.41 1.42Z"/></svg>Edit page</a> </div> <div class="pagination-links print:hidden astro-u2l5gyhi" dir="ltr"> <a href="/annotating/" rel="next" class="astro-u2l5gyhi"> <svg aria-hidden="true" class="astro-u2l5gyhi astro-c6vsoqas" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17.92 11.62a1.001 1.001 0 0 0-.21-.33l-5-5a1.003 1.003 0 1 0-1.42 1.42l3.3 3.29H7a1 1 0 0 0 0 2h7.59l-3.3 3.29a1.002 1.002 0 0 0 .325 1.639 1 1 0 0 0 1.095-.219l5-5a1 1 0 0 0 .21-.33 1 1 0 0 0 0-.76Z"/></svg> <span class="astro-u2l5gyhi"> Next <br class="astro-u2l5gyhi"> <span class="link-title astro-u2l5gyhi">annotating</span> </span> </a> </div> </footer> </div> </div> </main> </div> </div> </div> </div> </body></html>
|
||||||
Vendored
+166
File diff suppressed because one or more lines are too long
Vendored
+141
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1509
File diff suppressed because it is too large
Load Diff
+56
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.5.2","languages":{"en":{"hash":"en_49c5174ff0","wasm":"en","page_count":16}},"include_characters":["_","‿","⁀","⁔","︳","︴","﹍","﹎","﹏","_"]}
|
||||||
Vendored
+1070
File diff suppressed because it is too large
Load Diff
+214
@@ -0,0 +1,214 @@
|
|||||||
|
:root {
|
||||||
|
--pagefind-ui-scale: 0.8;
|
||||||
|
--pagefind-ui-primary: #034AD8;
|
||||||
|
--pagefind-ui-fade: #707070;
|
||||||
|
--pagefind-ui-text: #393939;
|
||||||
|
--pagefind-ui-background: #ffffff;
|
||||||
|
--pagefind-ui-border: #eeeeee;
|
||||||
|
--pagefind-ui-tag: #eeeeee;
|
||||||
|
--pagefind-ui-border-width: 2px;
|
||||||
|
--pagefind-ui-border-radius: 8px;
|
||||||
|
--pagefind-ui-image-border-radius: 8px;
|
||||||
|
--pagefind-ui-image-box-ratio: 3 / 2;
|
||||||
|
--pagefind-ui-font: system, -apple-system, ".SFNSText-Regular",
|
||||||
|
"San Francisco", "Roboto", "Segoe UI", "Helvetica Neue",
|
||||||
|
"Lucida Grande", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-suppressed] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-sr-hidden] {
|
||||||
|
-webkit-clip: rect(0 0 0 0) !important;
|
||||||
|
clip: rect(0 0 0 0) !important;
|
||||||
|
-webkit-clip-path: inset(100%) !important;
|
||||||
|
clip-path: inset(100%) !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
overflow: clip !important;
|
||||||
|
position: absolute !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-loading] {
|
||||||
|
color: var(--pagefind-ui-text);
|
||||||
|
background-color: var(--pagefind-ui-text);
|
||||||
|
border-radius: var(--pagefind-ui-border-radius);
|
||||||
|
opacity: 0.1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
|
||||||
|
.pagefind-modular-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input-wrapper::before {
|
||||||
|
background-color: var(--pagefind-ui-text);
|
||||||
|
width: calc(18px * var(--pagefind-ui-scale));
|
||||||
|
height: calc(18px * var(--pagefind-ui-scale));
|
||||||
|
top: calc(23px * var(--pagefind-ui-scale));
|
||||||
|
left: calc(20px * var(--pagefind-ui-scale));
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
opacity: 0.7;
|
||||||
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.7549 11.255H11.9649L11.6849 10.985C12.6649 9.845 13.2549 8.365 13.2549 6.755C13.2549 3.165 10.3449 0.255005 6.75488 0.255005C3.16488 0.255005 0.254883 3.165 0.254883 6.755C0.254883 10.345 3.16488 13.255 6.75488 13.255C8.36488 13.255 9.84488 12.665 10.9849 11.685L11.2549 11.965V12.755L16.2549 17.745L17.7449 16.255L12.7549 11.255ZM6.75488 11.255C4.26488 11.255 2.25488 9.245 2.25488 6.755C2.25488 4.26501 4.26488 2.255 6.75488 2.255C9.24488 2.255 11.2549 4.26501 11.2549 6.755C11.2549 9.245 9.24488 11.255 6.75488 11.255Z' fill='%23000000'/%3E%3C/svg%3E%0A");
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.7549 11.255H11.9649L11.6849 10.985C12.6649 9.845 13.2549 8.365 13.2549 6.755C13.2549 3.165 10.3449 0.255005 6.75488 0.255005C3.16488 0.255005 0.254883 3.165 0.254883 6.755C0.254883 10.345 3.16488 13.255 6.75488 13.255C8.36488 13.255 9.84488 12.665 10.9849 11.685L11.2549 11.965V12.755L16.2549 17.745L17.7449 16.255L12.7549 11.255ZM6.75488 11.255C4.26488 11.255 2.25488 9.245 2.25488 6.755C2.25488 4.26501 4.26488 2.255 6.75488 2.255C9.24488 2.255 11.2549 4.26501 11.2549 6.755C11.2549 9.245 9.24488 11.255 6.75488 11.255Z' fill='%23000000'/%3E%3C/svg%3E%0A");
|
||||||
|
-webkit-mask-size: 100%;
|
||||||
|
mask-size: 100%;
|
||||||
|
z-index: 9;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input {
|
||||||
|
height: calc(64px * var(--pagefind-ui-scale));
|
||||||
|
padding: 0 calc(70px * var(--pagefind-ui-scale)) 0 calc(54px * var(--pagefind-ui-scale));
|
||||||
|
background-color: var(--pagefind-ui-background);
|
||||||
|
border: var(--pagefind-ui-border-width) solid var(--pagefind-ui-border);
|
||||||
|
border-radius: var(--pagefind-ui-border-radius);
|
||||||
|
font-size: calc(21px * var(--pagefind-ui-scale));
|
||||||
|
position: relative;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input::placeholder {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input-clear {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(2px * var(--pagefind-ui-scale));
|
||||||
|
right: calc(2px * var(--pagefind-ui-scale));
|
||||||
|
height: calc(60px * var(--pagefind-ui-scale));
|
||||||
|
border-radius: var(--pagefind-ui-border-radius);
|
||||||
|
padding: 0 calc(15px * var(--pagefind-ui-scale)) 0 calc(2px * var(--pagefind-ui-scale));
|
||||||
|
color: var(--pagefind-ui-text);
|
||||||
|
font-size: calc(14px * var(--pagefind-ui-scale));
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--pagefind-ui-background);
|
||||||
|
border: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ResultList */
|
||||||
|
|
||||||
|
.pagefind-modular-list-result {
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: min(calc(40px * var(--pagefind-ui-scale)), 3%);
|
||||||
|
padding: calc(30px * var(--pagefind-ui-scale)) 0 calc(40px * var(--pagefind-ui-scale));
|
||||||
|
border-top: solid var(--pagefind-ui-border-width) var(--pagefind-ui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-result:last-of-type {
|
||||||
|
border-bottom: solid var(--pagefind-ui-border-width) var(--pagefind-ui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-thumb {
|
||||||
|
width: min(30%,
|
||||||
|
calc((30% - (100px * var(--pagefind-ui-scale))) * 100000));
|
||||||
|
max-width: calc(120px * var(--pagefind-ui-scale));
|
||||||
|
margin-top: calc(10px * var(--pagefind-ui-scale));
|
||||||
|
aspect-ratio: var(--pagefind-ui-image-box-ratio);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-image {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: var(--pagefind-ui-image-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-inner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: calc(10px * var(--pagefind-ui-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-title {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: calc(21px * var(--pagefind-ui-scale));
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-link {
|
||||||
|
color: var(--pagefind-ui-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-excerpt {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: calc(16px * var(--pagefind-ui-scale));
|
||||||
|
margin-top: calc(4px * var(--pagefind-ui-scale));
|
||||||
|
margin-bottom: 0;
|
||||||
|
min-width: calc(250px * var(--pagefind-ui-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FilterPills */
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pills-wrapper {
|
||||||
|
overflow-x: scroll;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pills {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pill {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
appearance: none;
|
||||||
|
padding: 0 calc(24px * var(--pagefind-ui-scale));
|
||||||
|
background-color: var(--pagefind-ui-background);
|
||||||
|
color: var(--pagefind-ui-fade);
|
||||||
|
border: var(--pagefind-ui-border-width) solid var(--pagefind-ui-border);
|
||||||
|
border-radius: calc(25px * var(--pagefind-ui-scale));
|
||||||
|
font-size: calc(18px * var(--pagefind-ui-scale));
|
||||||
|
height: calc(50px * var(--pagefind-ui-scale));
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pill:hover {
|
||||||
|
border-color: var(--pagefind-ui-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pill[aria-pressed="true"] {
|
||||||
|
border-color: var(--pagefind-ui-primary);
|
||||||
|
color: var(--pagefind-ui-primary);
|
||||||
|
}
|
||||||
Vendored
+8
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+6
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
Vendored
+6
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://loom.hypertheory-labs.dev/</loc></url><url><loc>https://loom.hypertheory-labs.dev/annotating/</loc></url><url><loc>https://loom.hypertheory-labs.dev/annotating/annotating/</loc></url><url><loc>https://loom.hypertheory-labs.dev/bedrock/</loc></url><url><loc>https://loom.hypertheory-labs.dev/bedrock/loom-directory/</loc></url><url><loc>https://loom.hypertheory-labs.dev/bedrock/publication/</loc></url><url><loc>https://loom.hypertheory-labs.dev/bedrock/recording-decisions/</loc></url><url><loc>https://loom.hypertheory-labs.dev/bedrock/sibling-facets/</loc></url><url><loc>https://loom.hypertheory-labs.dev/bedrock/starting/</loc></url><url><loc>https://loom.hypertheory-labs.dev/bedrock/venues/</loc></url><url><loc>https://loom.hypertheory-labs.dev/cart/</loc></url><url><loc>https://loom.hypertheory-labs.dev/cart/cart/</loc></url><url><loc>https://loom.hypertheory-labs.dev/externals/</loc></url><url><loc>https://loom.hypertheory-labs.dev/externals/externals/</loc></url><url><loc>https://loom.hypertheory-labs.dev/loom-cli/</loc></url><url><loc>https://loom.hypertheory-labs.dev/loom-cli/guarantees/</loc></url></urlset>
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>https://loom.hypertheory-labs.dev/sitemap-0.xml</loc></sitemap></sitemapindex>
|
||||||
Generated
-6911
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "loom-docs",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"generate": "node scripts/generate.mjs",
|
|
||||||
"ack": "node scripts/generate.mjs --ack",
|
|
||||||
"dev": "npm run generate && astro dev",
|
|
||||||
"build": "npm run generate && astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"check": "node scripts/generate.mjs --strict",
|
|
||||||
"publish-site": "bash scripts/publish-site.sh"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/starlight": "^0.36.0",
|
|
||||||
"astro": "^5.15.0",
|
|
||||||
"sharp": "^0.34.0",
|
|
||||||
"yaml": "^2.8.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Walk .loom/externals/.locks and project every adopted document into a
|
|
||||||
// Starlight page.
|
|
||||||
//
|
|
||||||
// Two kinds of page, and the difference is the whole design:
|
|
||||||
//
|
|
||||||
// generated one per adopted document. A rendering of somebody else's
|
|
||||||
// document, safe to overwrite because nobody typed it.
|
|
||||||
// guide one index.mdx per section. Hand-written, never overwritten,
|
|
||||||
// and stamped with the etags it was written against.
|
|
||||||
//
|
|
||||||
// A guide goes stale when a source moves. The build cannot tell whether the
|
|
||||||
// guide is still true — only a person can — so it renders the question onto
|
|
||||||
// the page rather than answering it. It reports; a person raises.
|
|
||||||
//
|
|
||||||
// The one thing this writes into a hand-written file is the `loom:` block in
|
|
||||||
// its frontmatter. That block is machine-owned. Everything below the closing
|
|
||||||
// `---` is yours and is copied byte for byte.
|
|
||||||
|
|
||||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from 'node:fs'
|
|
||||||
import { dirname, join } from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
||||||
|
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
||||||
const LOCKS = join(root, '.loom/externals/.locks')
|
|
||||||
const EXTERNALS = join(root, '.loom/externals')
|
|
||||||
const OUT = join(root, 'src/content/docs')
|
|
||||||
|
|
||||||
const argv = new Set(process.argv.slice(2))
|
|
||||||
const ACK = argv.has('--ack')
|
|
||||||
const STRICT = argv.has('--strict')
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- locks
|
|
||||||
|
|
||||||
// path <TAB> url <TAB> etag [<TAB> visibility]. Comments start with #.
|
|
||||||
function readLocks() {
|
|
||||||
if (!existsSync(LOCKS)) return []
|
|
||||||
return readFileSync(LOCKS, 'utf8')
|
|
||||||
.split('\n')
|
|
||||||
.filter((l) => l.trim() && !l.startsWith('#'))
|
|
||||||
.map((line) => {
|
|
||||||
const [path, url, etag, visibility] = line.split('\t')
|
|
||||||
// host/owner/repo/name.md
|
|
||||||
const parts = path.split('/')
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
url,
|
|
||||||
etag,
|
|
||||||
visibility: visibility ?? 'unknown',
|
|
||||||
host: parts[0],
|
|
||||||
owner: parts[1],
|
|
||||||
repo: parts[2],
|
|
||||||
name: parts.slice(3).join('/').replace(/\.md$/, ''),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Ordered by path so the output is byte-identical across runs. The diff is
|
|
||||||
// most of the value; a generated file that churns is a file nobody reads.
|
|
||||||
.sort((a, b) => a.path.localeCompare(b.path))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- frontmatter
|
|
||||||
|
|
||||||
function splitFrontmatter(text) {
|
|
||||||
if (!text.startsWith('---\n')) return { data: {}, body: text }
|
|
||||||
const end = text.indexOf('\n---\n', 3)
|
|
||||||
if (end === -1) return { data: {}, body: text }
|
|
||||||
return {
|
|
||||||
data: parseYaml(text.slice(4, end + 1)) ?? {},
|
|
||||||
body: text.slice(end + 5),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function joinFrontmatter(data, body) {
|
|
||||||
return `---\n${stringifyYaml(data, { lineWidth: 0 })}---\n${body}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The published documents carry no frontmatter, so the title is the first H1.
|
|
||||||
// It is removed from the body, or Starlight renders the heading twice.
|
|
||||||
function takeTitle(md, fallback) {
|
|
||||||
const lines = md.split('\n')
|
|
||||||
const i = lines.findIndex((l) => l.startsWith('# '))
|
|
||||||
if (i === -1) return { title: fallback, body: md.trimStart() }
|
|
||||||
const title = lines[i].slice(2).trim()
|
|
||||||
lines.splice(i, 1)
|
|
||||||
return { title, body: lines.join('\n').trimStart() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- pages
|
|
||||||
|
|
||||||
function generatedPage(rec) {
|
|
||||||
const src = readFileSync(join(EXTERNALS, rec.path), 'utf8')
|
|
||||||
const { title, body } = takeTitle(src, rec.name)
|
|
||||||
|
|
||||||
// What we hold beside the copy. Named, never summarised: extracting a claim
|
|
||||||
// from prose eventually extracts it wrong.
|
|
||||||
const facets = []
|
|
||||||
for (const [suffix, label] of [
|
|
||||||
['.usages.md', 'What of ours depends on it'],
|
|
||||||
['.gaps.md', 'What we expected and did not find'],
|
|
||||||
]) {
|
|
||||||
const p = `${rec.path.replace(/\.md$/, '')}${suffix}`
|
|
||||||
if (existsSync(join(EXTERNALS, p))) facets.push(`${label}: \`.loom/externals/${p}\``)
|
|
||||||
}
|
|
||||||
|
|
||||||
const provenance = [
|
|
||||||
'',
|
|
||||||
'<div class="loom-provenance">',
|
|
||||||
'',
|
|
||||||
`This page is a copy of a document published by \`${rec.owner}/${rec.repo}\`, rendered here.`,
|
|
||||||
`The source is [${rec.url}](${rec.url}) and is what the copy is checked against.`,
|
|
||||||
facets.length ? '\n' + facets.map((f) => `${f} `).join('\n') : '',
|
|
||||||
'',
|
|
||||||
'</div>',
|
|
||||||
'',
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
return joinFrontmatter(
|
|
||||||
{
|
|
||||||
title,
|
|
||||||
// Nobody typed this page, so offering to edit it is an invitation to
|
|
||||||
// lose work on the next build.
|
|
||||||
editUrl: false,
|
|
||||||
loom: {
|
|
||||||
generated: true,
|
|
||||||
path: rec.path,
|
|
||||||
source: rec.url,
|
|
||||||
etag: rec.etag,
|
|
||||||
visibility: rec.visibility,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body + '\n' + provenance,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const GUIDE_TEMPLATE = (repo) => `
|
|
||||||
This page is yours. Nothing overwrites it.
|
|
||||||
|
|
||||||
Put what a copy cannot carry here — a worked example, the order to read things
|
|
||||||
in, the thing that only makes sense once you have done it twice.
|
|
||||||
|
|
||||||
Prefer an example to an explanation. An explanation is a second saying of a rule
|
|
||||||
that is owned on one of the pages beside this one, and it goes stale silently. An
|
|
||||||
example goes stale visibly, because the artifacts in it are the wrong shape.
|
|
||||||
|
|
||||||
The pages in this section are copies of what \`loom/${repo}\` publishes, and are
|
|
||||||
regenerated. When one of them moves, this page gets a banner asking whether it is
|
|
||||||
still true — the build cannot answer that, so it asks.
|
|
||||||
`.trimStart()
|
|
||||||
|
|
||||||
function guidePage(repo, records, existing) {
|
|
||||||
const writtenAgainst = records.map((r) => ({ path: r.path, etag: r.etag }))
|
|
||||||
|
|
||||||
let data = { title: repo, loom: {} }
|
|
||||||
let body = GUIDE_TEMPLATE(repo)
|
|
||||||
let stale = []
|
|
||||||
let changed = []
|
|
||||||
let added = []
|
|
||||||
let gone = []
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
const split = splitFrontmatter(existing)
|
|
||||||
data = { ...split.data }
|
|
||||||
body = split.body
|
|
||||||
const was = new Map((data.loom?.writtenAgainst ?? []).map((s) => [s.path, s.etag]))
|
|
||||||
// A section gaining or losing a document changes the guide too, and this
|
|
||||||
// did not notice until bedrock grew a fifth page under a guide that
|
|
||||||
// enumerates its pages by hand. Comparing only what is present in both is a
|
|
||||||
// comparison that cannot see an arrival.
|
|
||||||
changed = records.filter((r) => was.has(r.path) && was.get(r.path) !== r.etag).map((r) => r.name)
|
|
||||||
added = records.filter((r) => !was.has(r.path)).map((r) => r.name)
|
|
||||||
gone = [...was.keys()]
|
|
||||||
.filter((p) => !records.some((r) => r.path === p))
|
|
||||||
.map((p) => p.split('/').pop().replace(/\.md$/, ''))
|
|
||||||
stale = [...changed, ...added, ...gone]
|
|
||||||
}
|
|
||||||
|
|
||||||
data.title ??= repo
|
|
||||||
data.loom = { writtenAgainst: existing && !ACK ? (data.loom?.writtenAgainst ?? writtenAgainst) : writtenAgainst }
|
|
||||||
|
|
||||||
if (stale.length && !ACK) {
|
|
||||||
// Starlight renders `banner` above the page. The reader sees the doubt even
|
|
||||||
// if nobody has reconciled it yet — the site degrades honestly rather than
|
|
||||||
// reading as true.
|
|
||||||
// Deliberately says nothing about which side is newer. An etag is opaque —
|
|
||||||
// that is the point of it — so two of them cannot be ordered, and this can
|
|
||||||
// only ever know that they differ. The first time it fired in real use, the
|
|
||||||
// guide was written against a version newer than the copy here, and a
|
|
||||||
// message claiming "an older X" was confidently backwards.
|
|
||||||
const code = (xs) => xs.map((x) => `<code>${x}</code>`).join(', ')
|
|
||||||
const parts = []
|
|
||||||
if (changed.length)
|
|
||||||
parts.push(
|
|
||||||
`${code(changed)} differs from the version this page was written against — so one of the two is behind`,
|
|
||||||
)
|
|
||||||
if (added.length) parts.push(`${code(added)} was adopted after this page was written`)
|
|
||||||
if (gone.length) parts.push(`${code(gone)} is no longer adopted`)
|
|
||||||
data.banner = {
|
|
||||||
content: `${parts.join('; ')}. Run <code>loomctl external check</code>, or re-read this page, then <code>npm run ack</code>.`,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete data.banner
|
|
||||||
}
|
|
||||||
|
|
||||||
return { text: joinFrontmatter(data, body), stale }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- run
|
|
||||||
|
|
||||||
const locks = readLocks()
|
|
||||||
if (locks.length === 0) {
|
|
||||||
console.error('generate: no adopted documents in .loom/externals/.locks')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sections = new Map()
|
|
||||||
for (const rec of locks) {
|
|
||||||
// A section is a repository, not a host and not a document. `bedrock` with
|
|
||||||
// five pages is how a person holds it; nine one-page sections is not.
|
|
||||||
if (!sections.has(rec.repo)) sections.set(rec.repo, [])
|
|
||||||
sections.get(rec.repo).push(rec)
|
|
||||||
}
|
|
||||||
|
|
||||||
let staleTotal = 0
|
|
||||||
const written = []
|
|
||||||
|
|
||||||
for (const [repo, records] of [...sections].sort(([a], [b]) => a.localeCompare(b))) {
|
|
||||||
const dir = join(OUT, repo)
|
|
||||||
mkdirSync(dir, { recursive: true })
|
|
||||||
|
|
||||||
const keep = new Set(['index.mdx'])
|
|
||||||
for (const rec of records) {
|
|
||||||
const file = join(dir, `${rec.name}.md`)
|
|
||||||
keep.add(`${rec.name}.md`)
|
|
||||||
writeFileSync(file, generatedPage(rec))
|
|
||||||
written.push(`${repo}/${rec.name}.md`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A document that is no longer adopted leaves. A page for something this
|
|
||||||
// repository no longer depends on is worse than a missing page: it reads as
|
|
||||||
// current and nothing will ever correct it.
|
|
||||||
for (const f of readdirSync(dir)) {
|
|
||||||
if (!keep.has(f)) {
|
|
||||||
rmSync(join(dir, f), { recursive: true })
|
|
||||||
console.log(` removed ${repo}/${f} (no longer adopted)`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const indexPath = join(dir, 'index.mdx')
|
|
||||||
const existing = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : null
|
|
||||||
const { text, stale } = guidePage(repo, records, existing)
|
|
||||||
writeFileSync(indexPath, text)
|
|
||||||
staleTotal += stale.length
|
|
||||||
if (stale.length && ACK) console.log(` acked ${repo}/index.mdx now current with ${stale.join(', ')}`)
|
|
||||||
else if (stale.length) console.log(` stale ${repo}/index.mdx written against older ${stale.join(', ')}`)
|
|
||||||
else if (!existing) console.log(` created ${repo}/index.mdx (blank guide, yours to fill)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The sidebar named its sections by hand, which made adopting from a new
|
|
||||||
// repository a config edit — a second place to update, which is the thing the
|
|
||||||
// autogenerated directories were already avoiding one level down.
|
|
||||||
const sidebar = [
|
|
||||||
{ label: 'Start here', link: '/' },
|
|
||||||
...[...sections.keys()].sort().map((repo) => ({ label: repo, autogenerate: { directory: repo } })),
|
|
||||||
]
|
|
||||||
writeFileSync(join(root, 'src/sidebar.json'), JSON.stringify(sidebar, null, 2) + '\n')
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`generate: ${written.length} pages from ${sections.size} sections` +
|
|
||||||
(ACK ? ', stamps refreshed' : staleTotal ? `, ${staleTotal} stale` : ''),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (STRICT && staleTotal && !ACK) process.exit(1)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Put dist/ on the `site` branch, which holds build output and nothing else.
|
|
||||||
# git-sync follows that branch; there is no image and no rollout.
|
|
||||||
#
|
|
||||||
# The orphan branch is named per-run and deleted in the trap. A fixed name
|
|
||||||
# survives the worktree and collides on the second run, which it did twice.
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
[ -d dist ] || { echo "no dist/ — run npm run build first" >&2; exit 1; }
|
|
||||||
ROOT=$PWD
|
|
||||||
SHA=$(git rev-parse --short HEAD)
|
|
||||||
TMP="publish-$$"
|
|
||||||
WORK=$(mktemp -d)
|
|
||||||
cleanup() {
|
|
||||||
git -C "$ROOT" worktree remove --force "$WORK" 2>/dev/null || true
|
|
||||||
rm -rf "$WORK"
|
|
||||||
git -C "$ROOT" branch -qD "$TMP" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
git worktree add -q --detach "$WORK"
|
|
||||||
cd "$WORK"
|
|
||||||
git checkout -q --orphan "$TMP"
|
|
||||||
git rm -rq --cached . 2>/dev/null || true
|
|
||||||
find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
|
|
||||||
mkdir dist && cp -R "$ROOT/dist/." dist/
|
|
||||||
git add -A
|
|
||||||
git commit -q -m "site: built from $SHA"
|
|
||||||
git push -qf origin HEAD:site
|
|
||||||
cd "$ROOT"
|
|
||||||
echo "published site branch from $SHA"
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { defineCollection, z } from 'astro:content'
|
|
||||||
import { docsLoader } from '@astrojs/starlight/loaders'
|
|
||||||
import { docsSchema } from '@astrojs/starlight/schema'
|
|
||||||
|
|
||||||
// `loom` is the machine-owned block. Everything else on a page is a person's.
|
|
||||||
const source = z.object({
|
|
||||||
path: z.string(),
|
|
||||||
etag: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const collections = {
|
|
||||||
docs: defineCollection({
|
|
||||||
loader: docsLoader(),
|
|
||||||
schema: docsSchema({
|
|
||||||
extend: z.object({
|
|
||||||
loom: z
|
|
||||||
.object({
|
|
||||||
// Set on a generated page: this is a rendering of somebody's document.
|
|
||||||
generated: z.boolean().optional(),
|
|
||||||
source: z.string().optional(),
|
|
||||||
path: z.string().optional(),
|
|
||||||
etag: z.string().optional(),
|
|
||||||
visibility: z.string().optional(),
|
|
||||||
// Set on a hand-written page: what it was written against.
|
|
||||||
writtenAgainst: z.array(source).optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
---
|
|
||||||
title: Agreement — annotating
|
|
||||||
editUrl: false
|
|
||||||
loom:
|
|
||||||
generated: true
|
|
||||||
path: git.hypertheory-labs.dev/loom/annotating/annotating.md
|
|
||||||
source: https://git.hypertheory-labs.dev/loom/annotating/raw/branch/main/.loom/published/annotating.md
|
|
||||||
etag: '"9b1f7e6ca92f2339b2d433686c27845362944bdb"'
|
|
||||||
visibility: public
|
|
||||||
---
|
|
||||||
**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.
|
|
||||||
|
|
||||||
|
|
||||||
<div class="loom-provenance">
|
|
||||||
|
|
||||||
This page is a copy of a document published by `loom/annotating`, rendered here.
|
|
||||||
The source is [https://git.hypertheory-labs.dev/loom/annotating/raw/branch/main/.loom/published/annotating.md](https://git.hypertheory-labs.dev/loom/annotating/raw/branch/main/.loom/published/annotating.md) and is what the copy is checked against.
|
|
||||||
|
|
||||||
What of ours depends on it: `.loom/externals/git.hypertheory-labs.dev/loom/annotating/annotating.usages.md`
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
title: annotating
|
|
||||||
loom:
|
|
||||||
writtenAgainst:
|
|
||||||
- path: git.hypertheory-labs.dev/loom/annotating/annotating.md
|
|
||||||
etag: '"9b1f7e6ca92f2339b2d433686c27845362944bdb"'
|
|
||||||
---
|
|
||||||
|
|
||||||
Two parties who never edit each other's text.
|
|
||||||
|
|
||||||
Annotations for `x.md` live beside it in `x.notes.md`, and while that file
|
|
||||||
exists, `x.md` is **frozen** — not a typo fix, not a reflow. The annotations are
|
|
||||||
anchored to line numbers, and editing the source silently invalidates every one
|
|
||||||
of them.
|
|
||||||
|
|
||||||
## The move that is easy to get wrong
|
|
||||||
|
|
||||||
Responding is not replying. You do not thread the notes file. You **dissolve**:
|
|
||||||
read every annotation, rewrite the source to incorporate what they say, and
|
|
||||||
delete the notes file. That reopens the document for the next round.
|
|
||||||
|
|
||||||
It is all-or-nothing. There is no per-annotation resolved state, and a
|
|
||||||
partially-emptied notes file leaves the pair in a state the model does not
|
|
||||||
describe.
|
|
||||||
|
|
||||||
## When to annotate rather than write in your own file
|
|
||||||
|
|
||||||
An annotation creates an obligation, so it is the blocking form. Correspondence
|
|
||||||
— writing in a file you own — is not.
|
|
||||||
|
|
||||||
> **Annotate to ask or to challenge. Write in your own file to assert.**
|
|
||||||
|
|
||||||
That discriminator is not in the agreement, and finding out it was missing took
|
|
||||||
a round: a daily must keep growing, annotating freezes what it annotates, so the
|
|
||||||
one annotatable thing looked like the one thing that could not be frozen.
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
---
|
|
||||||
title: bedrock
|
|
||||||
loom:
|
|
||||||
writtenAgainst:
|
|
||||||
- path: git.hypertheory-labs.dev/loom/bedrock/loom-directory.md
|
|
||||||
etag: '"ee0f49cb900c0812678061971194325d9cba366a"'
|
|
||||||
- path: git.hypertheory-labs.dev/loom/bedrock/publication.md
|
|
||||||
etag: '"c0d9231aa8eed3041c60b72da5735585457fbc90"'
|
|
||||||
- path: git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md
|
|
||||||
etag: '"d190eeae7929b9a99ecf09211d94e7e5b32635bd"'
|
|
||||||
- path: git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md
|
|
||||||
etag: '"a46446a34ccb8bfc533d3cce19f4c88548c4fa04"'
|
|
||||||
- path: git.hypertheory-labs.dev/loom/bedrock/starting.md
|
|
||||||
etag: '"b9eefba0f4668a496ccfc6a1377277f0721456d2"'
|
|
||||||
- path: git.hypertheory-labs.dev/loom/bedrock/venues.md
|
|
||||||
etag: '"35e940195e2ba9a30b4b4dc81a57f9383c709f64"'
|
|
||||||
---
|
|
||||||
|
|
||||||
Read [starting](/bedrock/starting/) first — it is two questions and a directory,
|
|
||||||
and most repositories need nothing else.
|
|
||||||
|
|
||||||
The other four pages are what those two questions turn into once a second
|
|
||||||
repository starts depending on you:
|
|
||||||
|
|
||||||
- [publication](/bedrock/publication/) — moving a document into `.loom/published/`
|
|
||||||
is a change of kind, not of status, and the step people skip is the last one.
|
|
||||||
- [recording-decisions](/bedrock/recording-decisions/) — an entry says what was
|
|
||||||
decided and **the belief that could turn out false**, because only a stated
|
|
||||||
belief can later be shown wrong.
|
|
||||||
- [sibling-facets](/bedrock/sibling-facets/) — `x.notes.md` beside `x.md`, owned
|
|
||||||
by whoever wrote the facet.
|
|
||||||
- [loom-directory](/bedrock/loom-directory/) — type by location. A stream is a
|
|
||||||
file; a collection of things with identity is a directory.
|
|
||||||
- [venues](/bedrock/venues/) — a choice you made about something that publishes
|
|
||||||
nothing you could adopt. Kafka, React, the organisation you work for.
|
|
||||||
|
|
||||||
## Venue and external are two questions, not two bins
|
|
||||||
|
|
||||||
Worth reading [venues](/bedrock/venues/) alongside
|
|
||||||
[externals](/externals/externals/), because sorting things into two kinds is the
|
|
||||||
mistake almost everybody makes first, and we made it for a day.
|
|
||||||
|
|
||||||
**`external` is who published it. `venue` is whether you had to decide anything
|
|
||||||
about it.** They are orthogonal, so a thing can be both, either, or neither —
|
|
||||||
and the same rule governs both files: record only the delta. Nobody needs telling
|
|
||||||
what Kafka is, so there is no external entry; *we use protobuf for schema* is a
|
|
||||||
choice, so there is a venue file.
|
|
||||||
|
|
||||||
## The one that gets skipped
|
|
||||||
|
|
||||||
`publication` ends with a five-step procedure whose fourth step is *fetch it,
|
|
||||||
from somewhere that is not your machine and not your account.*
|
|
||||||
|
|
||||||
Steps one to three are things you do. Step four is a fact about the world that
|
|
||||||
you find out — which is why it is the one that gets skipped, and why skipping it
|
|
||||||
is invisible. Every check available to the publisher passes. Only a reader can
|
|
||||||
detect the failure, and a reader who cannot fetch the document does not know it
|
|
||||||
exists to ask about.
|
|
||||||
|
|
||||||
This was measured three times in one afternoon on these very repositories. The
|
|
||||||
worst instance was a page written that morning to explain why something was
|
|
||||||
unreadable, published inside the thing that could not be read.
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
title: The `.loom` directory
|
|
||||||
editUrl: false
|
|
||||||
loom:
|
|
||||||
generated: true
|
|
||||||
path: git.hypertheory-labs.dev/loom/bedrock/loom-directory.md
|
|
||||||
source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/loom-directory.md
|
|
||||||
etag: '"ee0f49cb900c0812678061971194325d9cba366a"'
|
|
||||||
visibility: public
|
|
||||||
---
|
|
||||||
**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.**
|
|
||||||
|
|
||||||
|
|
||||||
<div class="loom-provenance">
|
|
||||||
|
|
||||||
This page is a copy of a document published by `loom/bedrock`, rendered here.
|
|
||||||
The source is [https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/loom-directory.md](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/loom-directory.md) and is what the copy is checked against.
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
---
|
|
||||||
title: Publication
|
|
||||||
editUrl: false
|
|
||||||
loom:
|
|
||||||
generated: true
|
|
||||||
path: git.hypertheory-labs.dev/loom/bedrock/publication.md
|
|
||||||
source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md
|
|
||||||
etag: '"c0d9231aa8eed3041c60b72da5735585457fbc90"'
|
|
||||||
visibility: public
|
|
||||||
---
|
|
||||||
**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.
|
|
||||||
|
|
||||||
## Publishing is not an act you can complete alone
|
|
||||||
|
|
||||||
**A file in `.loom/published/` that its intended readers cannot fetch is not
|
|
||||||
published.** *The move is the act, but the act is not finished until somebody who
|
|
||||||
is not you can perform the read.*
|
|
||||||
|
|
||||||
> **The failure is invisible from the inside.** *Every check available to the
|
|
||||||
> publisher passes — the file is there, the commit landed, the path is right.*
|
|
||||||
> **Only a reader can detect it, and a reader who cannot fetch it does not know
|
|
||||||
> the document exists to ask about.**
|
|
||||||
|
|
||||||
*The commonest form is a document that answers "why can you not see this?" and is
|
|
||||||
published **inside** the thing that cannot be seen.*
|
|
||||||
|
|
||||||
**So publication has a visibility, and it is a property of the act rather than of
|
|
||||||
the file.** *"Others" is not "everyone".* **Naming who the readers are is part of
|
|
||||||
publishing**, because a repository that depends on this one has to know whether it
|
|
||||||
is inside that set — *and because a reader who copies a document out inherits the
|
|
||||||
copy without inheriting the visibility it was published under.*
|
|
||||||
|
|
||||||
**The test is a fetch you did not perform.**
|
|
||||||
|
|
||||||
## Doing it
|
|
||||||
|
|
||||||
1. **Decide it is a contract and not a detail.** *This is the whole decision and
|
|
||||||
the rest is mechanics.*
|
|
||||||
2. **Move the file into `.loom/published/`.** *One commit, so the diff carries it.*
|
|
||||||
3. **Say who the readers are** — *if it is not obvious from where it landed.*
|
|
||||||
4. **Fetch it.** *From somewhere that is not your machine and not your account.*
|
|
||||||
5. **Record the decision**, if you keep a log.
|
|
||||||
|
|
||||||
**Step 4 is the one that gets skipped and the only one that can fail.** *Steps 1
|
|
||||||
to 3 are things you do; step 4 is a fact about the world that you find out.*
|
|
||||||
|
|
||||||
*Then say what it depends on. A published document that quietly requires a private
|
|
||||||
one has moved the problem rather than solved it.*
|
|
||||||
|
|
||||||
## 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.*
|
|
||||||
|
|
||||||
|
|
||||||
<div class="loom-provenance">
|
|
||||||
|
|
||||||
This page is a copy of a document published by `loom/bedrock`, rendered here.
|
|
||||||
The source is [https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md) and is what the copy is checked against.
|
|
||||||
|
|
||||||
What of ours depends on it: `.loom/externals/git.hypertheory-labs.dev/loom/bedrock/publication.usages.md`
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
---
|
|
||||||
title: Recording decisions
|
|
||||||
editUrl: false
|
|
||||||
loom:
|
|
||||||
generated: true
|
|
||||||
path: git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md
|
|
||||||
source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/recording-decisions.md
|
|
||||||
etag: '"d190eeae7929b9a99ecf09211d94e7e5b32635bd"'
|
|
||||||
visibility: public
|
|
||||||
---
|
|
||||||
**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.*
|
|
||||||
|
|
||||||
## Compaction, and the entries it may never touch
|
|
||||||
|
|
||||||
**"Never revised" and "somebody must be able to read it" both hold, and far
|
|
||||||
enough out they conflict** — *a log nobody can get through has stopped being a
|
|
||||||
record.*
|
|
||||||
|
|
||||||
**Compaction is allowed. Dropping an entry is not the same as revising one**, and
|
|
||||||
the uncompacted log is in git history, which is where a reader who needs it goes.
|
|
||||||
|
|
||||||
> **An entry may be dropped when a competent reader could recover it by reading
|
|
||||||
> the artifact.**
|
|
||||||
>
|
|
||||||
> **An entry may never be dropped when it records a decline, a measurement, a
|
|
||||||
> belief that was shown wrong, or one entry superseding another.**
|
|
||||||
|
|
||||||
*Which means **compaction is safe in exactly the cases where the entry was
|
|
||||||
redundant with the artifact, and unsafe in exactly the cases the log exists
|
|
||||||
for.*** **The four protected kinds are the ones with no other home:** *the code
|
|
||||||
shows what was built and can never show what was refused, what was measured, what
|
|
||||||
turned out false, or that somebody changed their mind.*
|
|
||||||
|
|
||||||
**A compaction is itself a decision, and gets an entry.**
|
|
||||||
|
|
||||||
## Where it lives
|
|
||||||
|
|
||||||
`.loom/event-log.md`. **A stream, so a file.**
|
|
||||||
|
|
||||||
|
|
||||||
<div class="loom-provenance">
|
|
||||||
|
|
||||||
This page is a copy of a document published by `loom/bedrock`, rendered here.
|
|
||||||
The source is [https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/recording-decisions.md](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/recording-decisions.md) and is what the copy is checked against.
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
title: Sibling facets
|
|
||||||
editUrl: false
|
|
||||||
loom:
|
|
||||||
generated: true
|
|
||||||
path: git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md
|
|
||||||
source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md
|
|
||||||
etag: '"a46446a34ccb8bfc533d3cce19f4c88548c4fa04"'
|
|
||||||
visibility: public
|
|
||||||
---
|
|
||||||
**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.*
|
|
||||||
|
|
||||||
|
|
||||||
<div class="loom-provenance">
|
|
||||||
|
|
||||||
This page is a copy of a document published by `loom/bedrock`, rendered here.
|
|
||||||
The source is [https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md) and is what the copy is checked against.
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
---
|
|
||||||
title: Starting one of these in a repository
|
|
||||||
editUrl: false
|
|
||||||
loom:
|
|
||||||
generated: true
|
|
||||||
path: git.hypertheory-labs.dev/loom/bedrock/starting.md
|
|
||||||
source: https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md
|
|
||||||
etag: '"b9eefba0f4668a496ccfc6a1377277f0721456d2"'
|
|
||||||
visibility: public
|
|
||||||
---
|
|
||||||
**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.* **It describes what a home cluster
|
|
||||||
offers to somebody deciding whether to put something on it**, and a reader who
|
|
||||||
has never heard of any of this still gets what they came for.
|
|
||||||
|
|
||||||
**It was private, and being unable to link to it is what made the split obvious.**
|
|
||||||
*The operational tree is a version inventory and a target list; the pages telling
|
|
||||||
somebody what to decide are neither.* **So the manifests moved to a private
|
|
||||||
sibling and the documents stayed** — *and the documents got better for being
|
|
||||||
written to a stranger, which was not the reason for doing it.*
|
|
||||||
|
|
||||||
> **A public page naming a private thing as its canonical answer is worse than no
|
|
||||||
> example**, and this page did exactly that until somebody measured it.
|
|
||||||
|
|
||||||
|
|
||||||
<div class="loom-provenance">
|
|
||||||
|
|
||||||
This page is a copy of a document published by `loom/bedrock`, rendered here.
|
|
||||||
The source is [https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md](https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md) and is what the copy is checked against.
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user