Compare commits

..
2 Commits
Author SHA1 Message Date
jeffryandClaude Opus 5 da6e8b9b51 loomctl orient: one table of contents, restating no rule
Generates .loom/orientation.md — what this repository depends on, where each copy
came from, and which facets sit beside it — for whoever arrives next, of any make.

One file rather than two, decided by loom's own rule. Generate what varies, adopt
what does not: the publishing half varies not at all and is already adopted, so
publication.md appears in the index like any other adopted document, in exactly the
repositories that adopted it. A second command would emit a file whose whole content
is a pointer to a file already in the tree, and would revive a noun declined when
published check went.

Restates no rule. The three moves are phrased as operations — what to run, what to
write, where — and every rule stays in the document that owns it. The .usages.md is
pointed at rather than summarised, because what depends on a document is free prose
and anything extracting a claim from prose eventually extracts it wrong. It says
nothing recorded when a document has no usages file, which is a finding rather than
an omission and is currently true of six of nine.

Byte-deterministic, pinned by a test that inserts records out of order and
generates three times. Written to .loom/orientation.md rather than inside
.loom/externals/, where check would report it as an unlocked external in every
repository using the feature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
2026-09-08 09:11:04 -04:00
jeffryandClaude Opus 5 1cc528c8aa sorrel: reconcile bedrock 82 and 83, and answer both prompts
publication.md and starting.md both came back 200 and are applied. Step 4 —
fetch it, from somewhere that is not your machine and not your account — is now the
procedure our declined command was an argument for.

Answers the Select for a fixed path but against .loom/externals/README.md, with a
measured reason: check walks that directory for adopted documents, so a generated
file there reports as unlocked in every repository using the feature, forever. The
deeper reason is the invariant — everything under externals/ is somebody else's
document, which is what makes do-not-edit a rule you can state in four words.
Proposes .loom/orientation.md, beside event-log.md, since .loom/README.md is
already given to what is in published/ and why.

Answers the Question yes. The visibility check was built as a moment and loom is
right that it is a fact that decays: access is verified once and the copy is
durable. It costs nothing at add time, since the anonymous request already happens
and the answer is thrown away, and one request per run at check time rather than per
document, since only our own visibility must be current. Adds the refinement that
the stored value decays both ways, so a source recorded private is re-checked only
when the alarm would fire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
2026-09-08 09:05:38 -04:00
7 changed files with 289 additions and 8 deletions
+2 -2
View File
@@ -4,9 +4,9 @@
git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.md https://git.hypertheory-labs.dev/jeffry/homelab-cluster/raw/branch/main/.loom/published/gitea.md "530c5bef62bbd325956ed170bb2decf37975e4b9"
git.hypertheory-labs.dev/loom/annotating/annotating.md https://git.hypertheory-labs.dev/loom/annotating/raw/branch/main/.loom/published/annotating.md "9b1f7e6ca92f2339b2d433686c27845362944bdb"
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"
git.hypertheory-labs.dev/loom/bedrock/publication.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md "385e26ef4108029c12a4267600399da1eece52af"
git.hypertheory-labs.dev/loom/bedrock/publication.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md "c0d9231aa8eed3041c60b72da5735585457fbc90"
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"
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"
git.hypertheory-labs.dev/loom/bedrock/starting.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md "7d997a30248a88c90b23f2f9453d7cfceb03848e"
git.hypertheory-labs.dev/loom/bedrock/starting.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md "b9eefba0f4668a496ccfc6a1377277f0721456d2"
git.hypertheory-labs.dev/loom/cart/cart.md https://git.hypertheory-labs.dev/loom/cart/raw/branch/main/.loom/published/cart.md "15331f1a9cc81bf61a44830cfbb7c274f4c2b119"
git.hypertheory-labs.dev/loom/externals/externals.md https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md "a7586eb52caf275d9bcedbbd8042c43e5aaad0b9"
@@ -37,6 +37,21 @@ 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
@@ -60,13 +60,16 @@ what they came for.*
## Look at one instead of reading this
**There is a worked example — six documents, one gap, no decomposition — and it
is private.**
**[`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 describes a cluster in enough detail to be a target list, so it is not
published, and **this page will not link you to something you cannot fetch.***
**If you have access, ask for it by name; if you do not, the two questions at the
top are the whole of it.**
**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.
+63
View File
@@ -0,0 +1,63 @@
<!-- 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/jeffry/homelab-cluster/gitea.md`
- source: https://git.hypertheory-labs.dev/jeffry/homelab-cluster/raw/branch/main/.loom/published/gitea.md
- what of ours depends on it: `.loom/externals/git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.usages.md`
## `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: **nothing recorded** — no `.usages.md`
- what we expected and did not find: `.loom/externals/git.hypertheory-labs.dev/loom/annotating/annotating.gaps.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: **nothing recorded** — no `.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`
- what we expected and did not find: `.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.gaps.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`
+109
View File
@@ -0,0 +1,109 @@
// Package orient generates a table of contents over what a repository depends
// on, for whoever arrives next — a person, or an agent of any make.
//
// It restates no rule. Every rule it might repeat is owned by a document already
// in the working tree, and a copy of a rule is a copy that goes stale: this file
// says only what is here, where it came from, and where the rules live.
//
// The output is byte-deterministic. A generated file that churns produces diffs
// nobody reads, and the diff is most of the value — it is how somebody sees that
// their dependencies moved.
package orient
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"git.hypertheory-labs.dev/loom/loom-cli/internal/lock"
)
// File is where the orientation lives, relative to the repository root.
//
// Beside .loom/event-log.md rather than inside .loom/externals/, because
// everything in that directory is somebody else's document — which is what makes
// "do not edit these" a rule you can state in four words — and because `check`
// walks it and would report a generated file as an unlocked external forever.
const File = ".loom/orientation.md"
const preamble = `<!-- 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.
`
// Generate writes the orientation for the repository at root.
func Generate(root, out string) (string, error) {
locks, err := lock.Load(root)
if err != nil {
return "", err
}
var b strings.Builder
b.WriteString(preamble)
recs := locks.All() // already ordered by path
if len(recs) == 0 {
b.WriteString("\nThis repository adopts nothing yet.\n")
}
for _, r := range recs {
fmt.Fprintf(&b, "\n## `%s`\n\n", r.Path)
fmt.Fprintf(&b, "- source: %s\n", r.URL)
facets := facetsFor(root, r.Path)
if u, ok := facets["usages"]; ok {
fmt.Fprintf(&b, "- what of ours depends on it: `%s`\n", u)
} else {
fmt.Fprintf(&b, "- what of ours depends on it: **nothing recorded** — no `.usages.md`\n")
}
if g, ok := facets["gaps"]; ok {
fmt.Fprintf(&b, "- what we expected and did not find: `%s`\n", g)
}
}
if dir := filepath.Join(root, ".loom", "published"); isDir(dir) {
b.WriteString("\n## This repository publishes\n\n")
b.WriteString("See `.loom/published/`. What is handed over for others to depend on is\n")
b.WriteString("there; the rest of the repository is not hidden, it is simply not what\n")
b.WriteString("anybody depends on.\n")
}
dest := out
if dest == "" {
dest = filepath.Join(root, filepath.FromSlash(File))
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return "", err
}
return dest, os.WriteFile(dest, []byte(b.String()), 0o644)
}
// facetsFor finds the files written beside an adopted document.
func facetsFor(root, rel string) map[string]string {
found := map[string]string{}
stem := strings.TrimSuffix(rel, path.Ext(rel))
for _, kind := range []string{"usages", "gaps"} {
p := stem + "." + kind + ".md"
if _, err := os.Stat(filepath.Join(root, lock.Dir, filepath.FromSlash(p))); err == nil {
found[kind] = lock.Dir + "/" + p
}
}
return found
}
func isDir(p string) bool {
fi, err := os.Stat(p)
return err == nil && fi.IsDir()
}
+73
View File
@@ -0,0 +1,73 @@
package orient
import (
"os"
"path/filepath"
"strings"
"testing"
"git.hypertheory-labs.dev/loom/loom-cli/internal/lock"
)
// A generated file that churns produces diffs nobody reads, and the diff is most
// of the value.
func TestGenerateIsDeterministic(t *testing.T) {
root := t.TempDir()
locks, err := lock.LoadFile(lock.Path(root))
if err != nil {
t.Fatal(err)
}
// Inserted out of order on purpose: the output must not depend on it.
for _, r := range []lock.Record{
{Path: "h/o/zeta/z.md", URL: "https://h/z", ETag: `"3"`},
{Path: "h/o/alpha/a.md", URL: "https://h/a", ETag: `"1"`},
{Path: "h/o/mid/m.md", URL: "https://h/m", ETag: `"2"`},
} {
locks.Put(r)
}
if err := locks.Save(); err != nil {
t.Fatal(err)
}
var first string
for i := 0; i < 3; i++ {
dest, err := Generate(root, filepath.Join(root, "out.md"))
if err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(dest)
if err != nil {
t.Fatal(err)
}
if i == 0 {
first = string(b)
continue
}
if string(b) != first {
t.Fatal("output changed between runs")
}
}
if a, z := strings.Index(first, "alpha"), strings.Index(first, "zeta"); a > z {
t.Error("entries are not ordered by path")
}
if strings.Contains(first, "publishes") {
t.Error("claimed the repository publishes with no .loom/published")
}
}
func TestSaysWhenNothingRecordsADependency(t *testing.T) {
root := t.TempDir()
locks, _ := lock.LoadFile(lock.Path(root))
locks.Put(lock.Record{Path: "h/o/r/doc.md", URL: "https://h/doc", ETag: `"1"`})
if err := locks.Save(); err != nil {
t.Fatal(err)
}
dest, err := Generate(root, filepath.Join(root, "out.md"))
if err != nil {
t.Fatal(err)
}
b, _ := os.ReadFile(dest)
if !strings.Contains(string(b), "nothing recorded") {
t.Error("a document with no .usages.md should say so — it is a finding, not an omission")
}
}
+18
View File
@@ -17,6 +17,7 @@ import (
"git.hypertheory-labs.dev/loom/loom-cli/internal/config"
"git.hypertheory-labs.dev/loom/loom-cli/internal/external"
"git.hypertheory-labs.dev/loom/loom-cli/internal/orient"
)
const usage = `loomctl — fetch what you depend on, and find out when it changed.
@@ -50,6 +51,7 @@ Adopted documents live in .loom/externals/<host>/<owner>/<repo>/<name>.md, and
their origins in .loom/externals/.locks. The path is for a person to read; the
lock is what a machine uses, because the path does not round-trip to a URL.
loomctl orient [--out path] write .loom/orientation.md for whoever arrives next
loomctl config which context is current, and from where
A bare owner/repo is resolved against the current context in
@@ -87,6 +89,22 @@ func run(args []string) error {
switch args[0] {
case "external":
return runExternal(args[1:])
case "orient":
fs := flag.NewFlagSet("orient", flag.ContinueOnError)
out := fs.String("out", "", "write here instead of "+orient.File)
if err := fs.Parse(args[1:]); err != nil {
return err
}
root, err := root()
if err != nil {
return err
}
dest, err := orient.Generate(root, *out)
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "wrote %s\n", dest)
return nil
case "config":
return showConfig(os.Stdout)
default: