a802f59f55
- cmd/fn/doctor.go - cmd/fn/main.go - cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt - cpp/apps/primitives_gallery/playground/tables/data_table.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.h - cpp/apps/primitives_gallery/playground/tables/self_test.cpp - cpp/apps/primitives_gallery/playground/tables/tql.cpp - cpp/apps/primitives_gallery/playground/tables/viz.cpp - cpp/apps/primitives_gallery/playground/tables/viz.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
253 lines
8.0 KiB
Go
253 lines
8.0 KiB
Go
package infra
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// LayoutReport describes what VaultLayoutEnsure did (or would do) to a vault directory.
|
|
type LayoutReport struct {
|
|
VaultPath string `json:"vault_path"`
|
|
Created []string `json:"created"` // dirs created (relative paths)
|
|
Migrated []string `json:"migrated"` // renames executed, format "src -> dst" (relative)
|
|
AlreadyOK []string `json:"already_ok"` // dirs that already existed at the target location
|
|
Skipped []string `json:"skipped"` // unrecognized root-level entries, left untouched
|
|
DryRun bool `json:"dry_run"`
|
|
}
|
|
|
|
// dataBuckets are root-level directories that belong under data/.
|
|
var dataBuckets = []string{"raw", "processed", "exports"}
|
|
|
|
// knowledgeBuckets are root-level directories that belong under knowledge/.
|
|
var knowledgeBuckets = []string{"decisions", "domains", "models", "benchmarks", "test_documents"}
|
|
|
|
// knownRootFiles are root-level files that should be moved to knowledge/.
|
|
var knownRootFiles = []string{"README.md", "README.txt"}
|
|
|
|
// VaultLayoutEnsure ensures a vault directory uses the canonical hybrid layout:
|
|
//
|
|
// data/{raw,processed,exports}
|
|
// knowledge/{decisions,domains,models,benchmarks,test_documents}
|
|
//
|
|
// Legacy vaults that have these directories at the root are migrated by renaming
|
|
// (or merging when both src and dst already exist). The operation is idempotent:
|
|
// a second run returns everything in AlreadyOK.
|
|
//
|
|
// When dryRun is true the function computes the report but does not touch the disk.
|
|
func VaultLayoutEnsure(vaultPath string, dryRun bool) (LayoutReport, error) {
|
|
report := LayoutReport{DryRun: dryRun}
|
|
|
|
// --- resolve path ---
|
|
vaultPath = strings.TrimRight(vaultPath, "/\\")
|
|
|
|
var err error
|
|
vaultPath, err = filepath.Abs(vaultPath)
|
|
if err != nil {
|
|
return report, fmt.Errorf("vault_layout_ensure: abs(%q): %w", vaultPath, err)
|
|
}
|
|
|
|
// Follow symlinks for the vault root itself.
|
|
resolved, err := filepath.EvalSymlinks(vaultPath)
|
|
if err != nil {
|
|
return report, fmt.Errorf("vault_layout_ensure: eval symlinks %q: %w", vaultPath, err)
|
|
}
|
|
vaultPath = resolved
|
|
report.VaultPath = vaultPath
|
|
|
|
// --- check that vault exists and is a directory ---
|
|
info, err := os.Stat(vaultPath)
|
|
if err != nil {
|
|
return report, fmt.Errorf("vault_layout_ensure: stat %q: %w", vaultPath, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return report, fmt.Errorf("vault_layout_ensure: %q is not a directory", vaultPath)
|
|
}
|
|
|
|
// --- ensure top-level containers ---
|
|
for _, container := range []string{"data", "knowledge"} {
|
|
dst := filepath.Join(vaultPath, container)
|
|
if err := ensureDir(dst, dryRun, container, &report); err != nil {
|
|
return report, err
|
|
}
|
|
}
|
|
|
|
// --- build migration table: root name -> relative destination ---
|
|
type migration struct {
|
|
rootName string // name in vault root (dir or file)
|
|
dstRel string // relative destination path inside vault
|
|
isFile bool
|
|
}
|
|
|
|
var migrations []migration
|
|
for _, b := range dataBuckets {
|
|
migrations = append(migrations, migration{rootName: b, dstRel: filepath.Join("data", b)})
|
|
}
|
|
for _, b := range knowledgeBuckets {
|
|
migrations = append(migrations, migration{rootName: b, dstRel: filepath.Join("knowledge", b)})
|
|
}
|
|
for _, rf := range knownRootFiles {
|
|
migrations = append(migrations, migration{rootName: rf, dstRel: filepath.Join("knowledge", "README.md"), isFile: true})
|
|
}
|
|
|
|
// Track which root names are "known" so we can compute Skipped.
|
|
knownNames := make(map[string]struct{})
|
|
for _, m := range migrations {
|
|
knownNames[strings.ToLower(m.rootName)] = struct{}{}
|
|
}
|
|
knownNames["data"] = struct{}{}
|
|
knownNames["knowledge"] = struct{}{}
|
|
|
|
// --- apply migrations ---
|
|
for _, m := range migrations {
|
|
src := filepath.Join(vaultPath, m.rootName)
|
|
dst := filepath.Join(vaultPath, m.dstRel)
|
|
srcRel := m.rootName
|
|
dstRel := m.dstRel
|
|
|
|
srcExists := pathExists(src)
|
|
dstExists := pathExists(dst)
|
|
|
|
switch {
|
|
case srcExists && dstExists:
|
|
// Both exist: merge if directory, error on file collision.
|
|
if m.isFile {
|
|
return report, fmt.Errorf("vault_layout_ensure: conflict: both %q and %q exist", srcRel, dstRel)
|
|
}
|
|
if err := mergeDirs(src, dst, srcRel, dstRel, dryRun, &report); err != nil {
|
|
return report, err
|
|
}
|
|
|
|
case srcExists && !dstExists:
|
|
// Only source exists: rename.
|
|
report.Migrated = append(report.Migrated, fmt.Sprintf("%s -> %s", srcRel, dstRel))
|
|
if !dryRun {
|
|
if err := os.Rename(src, dst); err != nil {
|
|
return report, fmt.Errorf("vault_layout_ensure: rename %q -> %q: %w", src, dst, err)
|
|
}
|
|
}
|
|
|
|
case !srcExists && dstExists:
|
|
// Already migrated.
|
|
report.AlreadyOK = append(report.AlreadyOK, dstRel)
|
|
|
|
default:
|
|
// Neither exists: create empty destination directory (skip for files).
|
|
if !m.isFile {
|
|
report.Created = append(report.Created, dstRel)
|
|
if !dryRun {
|
|
if err := os.MkdirAll(dst, 0o755); err != nil {
|
|
return report, fmt.Errorf("vault_layout_ensure: mkdir %q: %w", dst, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- collect skipped (unrecognized root entries) ---
|
|
entries, err := os.ReadDir(vaultPath)
|
|
if err != nil {
|
|
return report, fmt.Errorf("vault_layout_ensure: readdir %q: %w", vaultPath, err)
|
|
}
|
|
for _, e := range entries {
|
|
if _, known := knownNames[strings.ToLower(e.Name())]; !known {
|
|
report.Skipped = append(report.Skipped, e.Name())
|
|
}
|
|
}
|
|
|
|
return report, nil
|
|
}
|
|
|
|
// ensureDir adds the dir to Created (and creates it) if it doesn't exist,
|
|
// or to AlreadyOK if it does. Used for top-level containers "data" and "knowledge".
|
|
func ensureDir(path string, dryRun bool, rel string, report *LayoutReport) error {
|
|
if pathExists(path) {
|
|
report.AlreadyOK = append(report.AlreadyOK, rel)
|
|
return nil
|
|
}
|
|
report.Created = append(report.Created, rel)
|
|
if dryRun {
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
|
return fmt.Errorf("vault_layout_ensure: mkdir %q: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mergeDirs moves the contents of src into dst, then removes src if empty.
|
|
// Returns an error if any file in src already exists in dst (no overwrite policy).
|
|
func mergeDirs(src, dst, srcRel, dstRel string, dryRun bool, report *LayoutReport) error {
|
|
children, err := os.ReadDir(src)
|
|
if err != nil {
|
|
return fmt.Errorf("vault_layout_ensure: readdir %q: %w", src, err)
|
|
}
|
|
|
|
for _, child := range children {
|
|
childDst := filepath.Join(dst, child.Name())
|
|
if pathExists(childDst) {
|
|
return fmt.Errorf("vault_layout_ensure: merge conflict: %q already exists in %q (cannot overwrite %q)",
|
|
child.Name(), dstRel, filepath.Join(srcRel, child.Name()))
|
|
}
|
|
childSrc := filepath.Join(src, child.Name())
|
|
childSrcRel := filepath.Join(srcRel, child.Name())
|
|
childDstRel := filepath.Join(dstRel, child.Name())
|
|
report.Migrated = append(report.Migrated, fmt.Sprintf("%s -> %s", childSrcRel, childDstRel))
|
|
if !dryRun {
|
|
if err := os.Rename(childSrc, childDst); err != nil {
|
|
return fmt.Errorf("vault_layout_ensure: rename %q -> %q: %w", childSrc, childDst, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove the now-empty src directory.
|
|
if !dryRun {
|
|
// Re-check emptiness after renames.
|
|
remaining, _ := os.ReadDir(src)
|
|
if len(remaining) == 0 {
|
|
if err := os.Remove(src); err != nil {
|
|
return fmt.Errorf("vault_layout_ensure: remove empty src %q: %w", src, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pathExists returns true if path exists (any type).
|
|
func pathExists(path string) bool {
|
|
_, err := os.Lstat(path)
|
|
return err == nil
|
|
}
|
|
|
|
// dirIsEmpty returns true if a directory exists and has no entries.
|
|
func dirIsEmpty(path string) bool {
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return len(entries) == 0
|
|
}
|
|
|
|
// _ prevents "declared but not used" if dirIsEmpty is only used in tests.
|
|
var _ = dirIsEmpty
|
|
|
|
// vaultLayoutKnownNames returns the set of root-level names managed by this function.
|
|
// Exported for use in tests.
|
|
func vaultLayoutKnownNames() map[string]struct{} {
|
|
known := make(map[string]struct{})
|
|
for _, b := range dataBuckets {
|
|
known[b] = struct{}{}
|
|
}
|
|
for _, b := range knowledgeBuckets {
|
|
known[b] = struct{}{}
|
|
}
|
|
for _, rf := range knownRootFiles {
|
|
known[strings.ToLower(rf)] = struct{}{}
|
|
}
|
|
known["data"] = struct{}{}
|
|
known["knowledge"] = struct{}{}
|
|
return known
|
|
}
|
|
|