Files
loom-cli/internal/external/polad.go
T
jeffryandClaude Opus 5 b92797b4a2 a published surface is not only markdown
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
2026-09-11 11:16:31 -04:00

132 lines
4.1 KiB
Go

package external
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"git.hypertheory-labs.dev/loom/loom-cli/internal/lock"
)
// CartDir is where a round happens. A cart is not committed — it lives in the
// working tree of the machine the two presences share — so everything staged
// here is deliberately outside version control.
const CartDir = ".loom/cart/current"
// PoladDir is where a changed external waits for somebody to decide.
//
// A polad is a candidate artifact, shaped exactly like what it would become,
// staged so you can see whether it fits. Its exits are apply or discard, and
// nothing may drift into being kept.
const PoladDir = CartDir + "/polad"
// cartOpen reports whether there is a round to stage into. The tool never opens
// one: a cart is a bounded exchange between two presences, and starting it is
// somebody's act, not a side effect of checking freshness.
func cartOpen(root string) bool {
fi, err := os.Stat(filepath.Join(root, CartDir))
return err == nil && fi.IsDir()
}
// stage writes a candidate copy into the cart, with the ETag that was served
// alongside the bytes, so that applying it locks what somebody actually read.
func stage(root, rel string, body []byte, rec lock.Record) error {
dest := filepath.Join(root, filepath.FromSlash(PoladDir), filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return err
}
if err := os.WriteFile(dest, body, 0o644); err != nil {
return err
}
locks, err := lock.LoadFile(poladLockPath(root))
if err != nil {
return err
}
locks.Put(rec)
return locks.Save()
}
func poladLockPath(root string) string {
return filepath.Join(root, filepath.FromSlash(PoladDir), lock.File)
}
// usagesFor returns the path of the facet naming what depends on a document, and
// whether it exists.
//
// Reconciliation runs the other way: the question is not what do we rewrite
// here, but 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.
func usagesFor(root, rel string) (string, bool) {
p := strings.TrimSuffix(rel, ".md") + ".usages.md"
_, err := os.Stat(filepath.Join(root, lock.Dir, filepath.FromSlash(p)))
return p, err == nil
}
// Apply moves a staged polad into place and moves its lock with it.
//
// This exists because the lock is the half a person forgets. Moving the file by
// hand leaves a lock describing the copy you just replaced, which is the drift
// the lock was there to prevent.
func Apply(root string, rels []string, out io.Writer) error {
staged, err := lock.LoadFile(poladLockPath(root))
if err != nil {
return err
}
locks, err := lock.Load(root)
if err != nil {
return err
}
if len(rels) == 0 {
for _, r := range staged.All() {
rels = append(rels, r.Path)
}
}
if len(rels) == 0 {
fmt.Fprintln(out, "nothing staged")
return nil
}
for _, rel := range rels {
rec, ok := staged.Get(rel)
if !ok {
return fmt.Errorf("%s is not staged", rel)
}
src := filepath.Join(root, filepath.FromSlash(PoladDir), filepath.FromSlash(rel))
body, err := os.ReadFile(src)
if err != nil {
return err
}
dest := filepath.Join(root, lock.Dir, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return err
}
if err := os.WriteFile(dest, body, 0o644); err != nil {
return err
}
// Never let an absent value overwrite a known one. A polad staged by an
// older build carries no visibility, and the lock header says visibility
// is recorded because access is checked once and the copy is durable —
// so losing it on update is the failure it exists to prevent.
if rec.Visibility == "" {
if prior, ok := locks.Get(rel); ok {
rec.Visibility = prior.Visibility
}
}
locks.Put(rec)
staged.Remove(rel)
if err := os.Remove(src); err != nil {
return err
}
fmt.Fprintf(out, "applied %s\n", rel)
if u, ok := usagesFor(root, rel); ok {
fmt.Fprintf(out, " check %s names what depends on this\n", u)
}
}
if err := locks.Save(); err != nil {
return err
}
return staged.Save()
}