a changed external is a polad, and add no longer overwrites anything
Restores something the specimen said and the round that discarded the specimen lost with it: a changed external becomes a polad in the cart, and somebody decides. check now stages what moved into .loom/cart/current/polad/ with the ETag that was served alongside the bytes, and prints the .usages.md beside it, because reconciliation runs the other way — the facets usually survive and what moves is the code a usage named. It says so when there is no usages file, which is its own finding. With no cart open, check reports and stages nothing. The tool does not open a round: a cart is a bounded exchange between two presences and starting one is somebody's act, not a side effect of asking about freshness. add now adopts what is not here and refuses what is already adopted, superseding the entry that had it announce an overwrite — it no longer overwrites at all. The one exception is the only way out of a dead end: a document present but unlocked was fetched by hand, nothing records its origin, and the path does not round-trip, so check cannot ask about it and a refusal would strand it forever. add accepts it and the bytes decide — identical locks it without rewriting anything, which makes the lock's assertion verified rather than assumed, and different stages a polad and leaves the local copy alone because it is the only evidence anything moved. apply exists because the lock is the half a person forgets: moving a polad by hand leaves a lock describing the copy you just replaced. Recorded with its limit — for an external, discard does not mean the change goes away, so discarding is really knowingly stale and nothing yet records that choice. Measured end to end on this repository: eight hand-fetched documents, all eight locked, nothing rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
This commit is contained in:
Vendored
+121
@@ -0,0 +1,121 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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 := rel[:len(rel)-len(filepath.Ext(rel))] + ".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
|
||||
}
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user