From 0095de2ce720fc02b336f93d0670b00f305ed25b Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 13:41:35 +0100 Subject: [PATCH 1/3] feat: CheckSnapshots y UpdateSnapshot en fn_operations library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade UpdateTypeSnapshot al store, CheckSnapshots para comparar snapshots locales vs registry (up_to_date/outdated/missing), y UpdateSnapshot para re-snapshot con retorno de old/new para diffing. --- fn_operations/operations.go | 88 +++++++++++++++++++++++++++++++++++++ fn_operations/store.go | 14 ++++++ 2 files changed, 102 insertions(+) diff --git a/fn_operations/operations.go b/fn_operations/operations.go index d52fc9a5..973c3f64 100644 --- a/fn_operations/operations.go +++ b/fn_operations/operations.go @@ -189,6 +189,94 @@ func GetEntityGraph(db *DB) (*Graph, error) { }, nil } +// SnapshotStatus describes the state of a snapshot vs the registry. +type SnapshotStatus string + +const ( + SnapshotUpToDate SnapshotStatus = "up_to_date" + SnapshotOutdated SnapshotStatus = "outdated" + SnapshotMissing SnapshotStatus = "missing" // exists locally but not in registry +) + +// SnapshotCheckResult holds the comparison for one type snapshot. +type SnapshotCheckResult struct { + ID string + LocalVersion string + RegistryVersion string + Status SnapshotStatus +} + +// CheckSnapshots compares all local snapshots against the registry. +func CheckSnapshots(opsDB *DB, registryDB *registry.DB) ([]SnapshotCheckResult, error) { + snaps, err := opsDB.ListTypeSnapshots() + if err != nil { + return nil, fmt.Errorf("listing snapshots: %w", err) + } + + var results []SnapshotCheckResult + for _, snap := range snaps { + regType, err := registryDB.GetType(snap.ID) + if err != nil { + // Not found in registry + results = append(results, SnapshotCheckResult{ + ID: snap.ID, + LocalVersion: snap.Version, + Status: SnapshotMissing, + }) + continue + } + + status := SnapshotUpToDate + if regType.Version != snap.Version { + status = SnapshotOutdated + } + + results = append(results, SnapshotCheckResult{ + ID: snap.ID, + LocalVersion: snap.Version, + RegistryVersion: regType.Version, + Status: status, + }) + } + return results, nil +} + +// UpdateSnapshot re-snapshots a type from the registry, replacing the local copy. +// Returns the old and new definitions for diffing. +func UpdateSnapshot(opsDB *DB, registryDB *registry.DB, typeID string) (old, new_ *TypeSnapshot, err error) { + // Get current local snapshot + oldSnap, err := opsDB.GetTypeSnapshot(typeID) + if err != nil { + return nil, nil, fmt.Errorf("reading local snapshot: %w", err) + } + if oldSnap == nil { + return nil, nil, fmt.Errorf("type %q not found in local snapshots", typeID) + } + + // Get current registry type + regType, err := registryDB.GetType(typeID) + if err != nil { + return nil, nil, fmt.Errorf("fetching type %q from registry: %w", typeID, err) + } + + // Build new snapshot + newSnap := &TypeSnapshot{ + ID: regType.ID, + Version: regType.Version, + Lang: regType.Lang, + Algebraic: string(regType.Algebraic), + Definition: regType.Definition, + Description: regType.Description, + SnappedAt: time.Now().UTC(), + } + + if err := opsDB.UpdateTypeSnapshot(newSnap); err != nil { + return nil, nil, fmt.Errorf("updating snapshot: %w", err) + } + + return oldSnap, newSnap, nil +} + func buildEntitySet(db *DB) (map[string]bool, error) { all, err := db.ListEntities("", "") if err != nil { diff --git a/fn_operations/store.go b/fn_operations/store.go index 52f2389a..ed1f47c3 100644 --- a/fn_operations/store.go +++ b/fn_operations/store.go @@ -58,6 +58,20 @@ func (db *DB) InsertTypeSnapshot(ts *TypeSnapshot) error { return err } +// UpdateTypeSnapshot replaces an existing snapshot with a new version. +func (db *DB) UpdateTypeSnapshot(ts *TypeSnapshot) error { + if ts.SnappedAt.IsZero() { + ts.SnappedAt = time.Now().UTC() + } + _, err := db.conn.Exec(` + UPDATE types_snapshot SET version=?, lang=?, algebraic=?, definition=?, description=?, snapped_at=? + WHERE id=?`, + ts.Version, ts.Lang, ts.Algebraic, ts.Definition, ts.Description, + ts.SnappedAt.Format(time.RFC3339), ts.ID, + ) + return err +} + // GetTypeSnapshot returns a type snapshot by ID. func (db *DB) GetTypeSnapshot(id string) (*TypeSnapshot, error) { row := db.conn.QueryRow("SELECT id, version, lang, algebraic, definition, description, snapped_at FROM types_snapshot WHERE id = ?", id) From 9ea13870cfd005b001c463b2640dc5881e8061d5 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 13:41:35 +0100 Subject: [PATCH 2/3] feat: CLI fn ops snapshot check y snapshot update Comando check compara versiones locales vs registry y marca outdated. Comando update re-snapshottea por ID o --all, muestra diff de definition y description. Requiere registry accesible via FN_REGISTRY_ROOT. --- cmd/fn/ops.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/cmd/fn/ops.go b/cmd/fn/ops.go index 385edbdc..bb57331e 100644 --- a/cmd/fn/ops.go +++ b/cmd/fn/ops.go @@ -56,6 +56,8 @@ Usage: fn ops relation delete Elimina relation fn ops graph Grafo ASCII de entities y relations fn ops snapshot list Lista tipos snapshotted + fn ops snapshot check Compara snapshots vs registry + fn ops snapshot update |--all Re-snapshot desde registry Entity flags: --id --name --type-ref --source @@ -540,11 +542,25 @@ func cmdOpsGraph() { // --- ops snapshot --- func cmdOpsSnapshot(args []string) { - if len(args) < 1 || args[0] != "list" { - fmt.Fprintln(os.Stderr, "usage: fn ops snapshot list") + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn ops snapshot [id|--all]") os.Exit(1) } + switch args[0] { + case "list": + cmdOpsSnapshotList() + case "check": + cmdOpsSnapshotCheck() + case "update": + cmdOpsSnapshotUpdate(args[1:]) + default: + fmt.Fprintf(os.Stderr, "unknown snapshot command: %s\n", args[0]) + os.Exit(1) + } +} + +func cmdOpsSnapshotList() { db := openOpsDB() defer db.Close() @@ -568,6 +584,115 @@ func cmdOpsSnapshot(args []string) { w.Flush() } +func cmdOpsSnapshotCheck() { + opsDB := openOpsDB() + defer opsDB.Close() + + regDB := requireRegistryDB() + defer regDB.Close() + + results, err := ops.CheckSnapshots(opsDB, regDB) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if len(results) == 0 { + fmt.Println("No type snapshots to check.") + return + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "ID\tLOCAL\tREGISTRY\tSTATUS") + for _, r := range results { + regVer := r.RegistryVersion + if regVer == "" { + regVer = "-" + } + status := "✓" + switch r.Status { + case ops.SnapshotOutdated: + status = "← OUTDATED" + case ops.SnapshotMissing: + status = "⚠ not in registry" + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.ID, r.LocalVersion, regVer, status) + } + w.Flush() +} + +func cmdOpsSnapshotUpdate(args []string) { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn ops snapshot update | --all") + os.Exit(1) + } + + opsDB := openOpsDB() + defer opsDB.Close() + + regDB := requireRegistryDB() + defer regDB.Close() + + if args[0] == "--all" { + // Update all outdated + results, err := ops.CheckSnapshots(opsDB, regDB) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + updated := 0 + for _, r := range results { + if r.Status != ops.SnapshotOutdated { + continue + } + old, new_, err := ops.UpdateSnapshot(opsDB, regDB, r.ID) + if err != nil { + fmt.Fprintf(os.Stderr, " error updating %s: %v\n", r.ID, err) + continue + } + printSnapshotDiff(r.ID, old, new_) + updated++ + } + if updated == 0 { + fmt.Println("All snapshots are up to date.") + } else { + fmt.Printf("\n%d snapshot(s) updated.\n", updated) + } + return + } + + // Update single + typeID := args[0] + old, new_, err := ops.UpdateSnapshot(opsDB, regDB, typeID) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + printSnapshotDiff(typeID, old, new_) +} + +func printSnapshotDiff(id string, old, new_ *ops.TypeSnapshot) { + fmt.Printf("Updated %s: %s → %s\n", id, old.Version, new_.Version) + if old.Definition != new_.Definition { + fmt.Println(" Definition changed:") + fmt.Printf(" - %s\n", strings.ReplaceAll(strings.TrimSpace(old.Definition), "\n", "\n - ")) + fmt.Printf(" + %s\n", strings.ReplaceAll(strings.TrimSpace(new_.Definition), "\n", "\n + ")) + } + if old.Description != new_.Description { + fmt.Printf(" Description: %q → %q\n", old.Description, new_.Description) + } +} + +func requireRegistryDB() *registry.DB { + db := tryOpenRegistryDB() + if db == nil { + fmt.Fprintln(os.Stderr, "error: cannot find registry.db") + fmt.Fprintln(os.Stderr, "Set FN_REGISTRY_ROOT or run from the registry directory.") + os.Exit(1) + } + return db +} + // --- helpers --- func findOpsDB() string { From 801d9b906a2c900072e958abb9badcedaf36c8a3 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 13:41:35 +0100 Subject: [PATCH 3/3] chore: build.sh para docker_tui y operations.db actualizado Script de build standalone y operations.db con snapshots sincronizados. --- fn_operations/docker_tui/build.sh | 14 ++++++++++++++ fn_operations/docker_tui/operations.db | Bin 53248 -> 53248 bytes 2 files changed, 14 insertions(+) create mode 100755 fn_operations/docker_tui/build.sh diff --git a/fn_operations/docker_tui/build.sh b/fn_operations/docker_tui/build.sh new file mode 100755 index 00000000..e877df85 --- /dev/null +++ b/fn_operations/docker_tui/build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +echo "==> Tidying modules..." +go mod tidy + +echo "==> Building docker-tui..." +mkdir -p build +go build -trimpath -ldflags='-s -w' -o build/docker-tui . + +echo "==> Done: build/docker-tui ($(du -h build/docker-tui | cut -f1))" +echo " Run with: ./build/docker-tui" diff --git a/fn_operations/docker_tui/operations.db b/fn_operations/docker_tui/operations.db index 5196b536e044a485e480b6b6c2b3b26731416b9e..e730dc7b152e426f5b4683121f31deec608f9925 100644 GIT binary patch delta 218 zcmZozz}&EadBaypo~A%XE@N>)>1NhO%SrNLjE0lN<@F}J$P3pi6qgib=B0Bf0D)(2 zVtOi&MiL4xNh~QX#t=eN6_8(40#t;o(I+t}H3uk`n^+JHF)tR$Sm&bD#FEq$h2qHz yI+9sctQ{Xf-vNAEWGBSwT>?ysDg#*ep-TYskaRUJ8(L$O4 delta 31 ncmZozz}&EadBa!9%?1J?jGH~B*Rf3YvFDuRz_yu9;n#Wqyyy#;