Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da6e8b9b51 | ||
|
|
1cc528c8aa |
Vendored
+2
-2
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user