Files
fn_registry/functions/infra/vault_layout_ensure.go
egutierrez e3c8979e8d chore: auto-commit (95 archivos)
- 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>
2026-05-13 00:50:34 +02:00

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
}