feat(legacymove): also clear raw/ leftovers from before inbox zero (#209)

PlanRaw: an original that was archived but never removed (it has a .processed
marker AND a byte-identical copy under raw/processed/) is deleted with its
marker; a marker without a note is deleted; an unmarked note, a note without an
identical archive copy, and AutoTunnel's tunnel-candidates-* review queue are
never touched. The archive key flattens the whole path so raw/x.md and
wiki/sources/x.md cannot overwrite each other. Live dry run: 84 entries, 16
move, 53 delete (24 junk + 29 raw leftovers), 15 left.
Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 21:02:41 +02:00
co-authored by Claude Sonnet 5.5
parent e14577d814
commit 2b16f2cc2f
3 changed files with 205 additions and 8 deletions
+94 -3
View File
@@ -11,11 +11,11 @@
package legacymove
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io/fs"
"path"
"sort"
"strings"
"time"
@@ -150,7 +150,7 @@ func Apply(brainDir string, items []Item, now time.Time) (Result, error) {
if err != nil {
return res, fmt.Errorf("read %s: %w", it.From, err)
}
if err := bfs.PutRegular(res.ArchiveDir+"/originals/"+path.Base(it.From), content); err != nil {
if err := bfs.PutRegular(res.ArchiveDir+"/originals/"+archiveKey(it.From), content); err != nil {
return res, fmt.Errorf("archive %s: %w", it.From, err)
}
switch it.Action {
@@ -205,7 +205,7 @@ func Rollback(brainDir, archiveDir string) error {
return fmt.Errorf("parse manifest: %w", err)
}
restore := func(from string) error {
orig, err := bfs.ReadRegular(archiveDir + "/originals/" + path.Base(from))
orig, err := bfs.ReadRegular(archiveDir + "/originals/" + archiveKey(from))
if err != nil {
return fmt.Errorf("read archived %s: %w", from, err)
}
@@ -230,6 +230,11 @@ func Rollback(brainDir, archiveDir string) error {
return nil
}
// archiveKey flattens a brain path into one file name, so two pages with the
// same basename in different places (raw/x.md and wiki/sources/x.md) never
// overwrite each other in the archive.
func archiveKey(p string) string { return strings.ReplaceAll(p, "/", "__") }
// stamp inserts wing/hall after the opening fence, leaving every other byte
// alone. A page without frontmatter gets a block.
func stamp(content, wing string) string {
@@ -252,3 +257,89 @@ func frontmatterValue(content, key string) string {
}
return ""
}
const rawDir = "raw"
// PlanRaw finds what the watcher left in raw/ before inbox zero: originals that
// were archived but never removed, with their .processed markers, and markers
// whose note is gone. An original is deleted only when a byte-identical copy
// exists under raw/processed/ (so nothing is lost), and only when it has a
// marker (an unmarked note is still in the inbox). tunnel-candidates-* are
// AutoTunnel's own review queue and are never touched.
func PlanRaw(brainDir string) ([]Item, error) {
bfs, err := brainfs.Open(brainDir)
if err != nil {
return nil, fmt.Errorf("open brain: %w", err)
}
defer func() { _ = bfs.Close() }()
names, err := bfs.ReadDirRegular(rawDir)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("read %s: %w", rawDir, err)
}
present := map[string]bool{}
for _, n := range names {
present[n] = true
}
archived, err := archivedCopies(bfs)
if err != nil {
return nil, err
}
var items []Item
for _, n := range names {
if strings.HasPrefix(n, "tunnel-candidates-") {
continue
}
switch {
case strings.HasSuffix(n, ".processed"):
note := strings.TrimSuffix(n, ".processed")
if !present[note] {
items = append(items, Item{From: rawDir + "/" + n, Slug: n, Action: Delete, Reason: "marker without a note"})
}
case present[n+".processed"]:
content, rerr := bfs.ReadRegular(rawDir + "/" + n)
if rerr != nil {
items = append(items, Item{From: rawDir + "/" + n, Slug: n, Action: Skip, Reason: fmt.Sprintf("unreadable: %v", rerr)})
continue
}
if !archived[sha256.Sum256(content)] {
items = append(items, Item{From: rawDir + "/" + n, Slug: n, Action: Skip, Reason: "marked processed but no identical copy under raw/processed/; kept"})
continue
}
items = append(items,
Item{From: rawDir + "/" + n, Slug: n, Action: Delete, Reason: "archived copy exists under raw/processed/"},
Item{From: rawDir + "/" + n + ".processed", Slug: n + ".processed", Action: Delete, Reason: "marker of an archived note"})
}
}
sort.SliceStable(items, func(i, j int) bool { return items[i].From < items[j].From })
return items, nil
}
// archivedCopies returns the SHA-256 of every file under raw/processed/, as a
// set, so "is there an identical copy" is one lookup.
func archivedCopies(bfs *brainfs.Brain) (map[[32]byte]bool, error) {
out := map[[32]byte]bool{}
days, err := bfs.ReadDirDirs(rawDir + "/processed")
if errors.Is(err, fs.ErrNotExist) {
return out, nil
}
if err != nil {
return nil, fmt.Errorf("list %s/processed: %w", rawDir, err)
}
for _, d := range days {
files, err := bfs.ReadDirRegular(rawDir + "/processed/" + d)
if err != nil {
continue
}
for _, f := range files {
b, err := bfs.ReadRegular(rawDir + "/processed/" + d + "/" + f)
if err != nil {
continue
}
out[sha256.Sum256(b)] = true
}
}
return out, nil
}