add appended .md unconditionally, so openapi.json became openapi.json.md. Reported from use against a repository that publishes an OpenAPI document beside its prose. It is now supplied only when the name carries no extension of its own. Two consequences came with it that the report could not have seen. A facet's name was built by stripping the extension, so openapi.json would have taken openapi.usages.md and collided with the facet for a sibling openapi.md; only a trailing .md is stripped now. And the unlocked walk matched *.md only, so a hand-fetched .json under .loom/externals was adopted, unlocked, and invisible to check; it now walks everything that is not the lock file or a dotfile. The assumption was never decided, it was typed. externals says a repository publishes documents and publication says prose in whatever form its readers need. Neither says markdown and three places in this tool did. Also records that the wrong diagnosis cost more than the bug: the repository is private, a scratch config with no token made every fetch look broken, and loomctl config would have said so in one line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
112 lines
4.1 KiB
Go
112 lines
4.1 KiB
Go
// 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/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{}
|
|
// Only a trailing .md is stripped: openapi.json keeps its extension, so
|
|
// its facet is openapi.json.usages.md and cannot collide with a facet for
|
|
// a sibling openapi.md.
|
|
stem := strings.TrimSuffix(rel, ".md")
|
|
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()
|
|
}
|