// 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 = ` # 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 ` + "`.gaps.md`" + ` beside the copy. - **What of ours depends on a copy** — write it in ` + "`.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() }