Files
fn_registry/registry/hash.go
T
egutierrez e3bb9c3b38 feat: content hash y timestamps inteligentes en registry
Agrega content_hash a functions, types y apps para detectar cambios reales
entre reindexaciones. Los timestamps created_at se preservan si el contenido
no cambió, y updated_at solo se actualiza cuando hay cambios efectivos.
Incluye migración 005, hash.go con SHA256 determinístico, y ajustes en
store/indexer/models para el nuevo flujo de timestamps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:23:45 +02:00

102 lines
3.5 KiB
Go

package registry
import (
"crypto/sha256"
"fmt"
"time"
)
// timestampRecord holds preserved timestamps and hash for an existing entry.
type timestampRecord struct {
CreatedAt time.Time
UpdatedAt time.Time
ContentHash string
}
// ComputeFunctionHash computes a deterministic hash of all content fields of a Function
// (excluding created_at, updated_at, and content_hash itself).
func ComputeFunctionHash(f *Function) string {
h := sha256.New()
fmt.Fprintf(h, "%s|%s|%s|%s|%s|%s|%s|%s|%s",
f.ID, f.Name, f.Kind, f.Lang, f.Domain, f.Version, f.Purity, f.Signature, f.Description)
fmt.Fprintf(h, "|%s", marshalStrings(f.Tags))
fmt.Fprintf(h, "|%s", marshalStrings(f.UsesFunctions))
fmt.Fprintf(h, "|%s", marshalStrings(f.UsesTypes))
fmt.Fprintf(h, "|%s", marshalStrings(f.Returns))
fmt.Fprintf(h, "|%t|%s", f.ReturnsOptional, f.ErrorType)
fmt.Fprintf(h, "|%s", marshalStrings(f.Imports))
fmt.Fprintf(h, "|%s|%t", f.Example, f.Tested)
fmt.Fprintf(h, "|%s", marshalStrings(f.Tests))
fmt.Fprintf(h, "|%s|%s", f.TestFilePath, f.FilePath)
fmt.Fprintf(h, "|%s", marshalProps(f.Props))
fmt.Fprintf(h, "|%s", marshalStrings(f.Emits))
if f.HasState != nil {
fmt.Fprintf(h, "|%t", *f.HasState)
}
fmt.Fprintf(h, "|%s", f.Framework)
fmt.Fprintf(h, "|%s", marshalStrings(f.Variant))
fmt.Fprintf(h, "|%s|%s|%s", f.Notes, f.Documentation, f.Code)
fmt.Fprintf(h, "|%s|%s|%s", f.SourceRepo, f.SourceLicense, f.SourceFile)
return fmt.Sprintf("%x", h.Sum(nil))
}
// ComputeTypeHash computes a deterministic hash of all content fields of a Type.
func ComputeTypeHash(t *Type) string {
h := sha256.New()
fmt.Fprintf(h, "%s|%s|%s|%s|%s|%s|%s|%s",
t.ID, t.Name, t.Lang, t.Domain, t.Version, t.Algebraic, t.Definition, t.Description)
fmt.Fprintf(h, "|%s", marshalStrings(t.Tags))
fmt.Fprintf(h, "|%s", marshalStrings(t.UsesTypes))
fmt.Fprintf(h, "|%s|%s|%s|%s|%s", t.FilePath, t.Examples, t.Notes, t.Documentation, t.Code)
fmt.Fprintf(h, "|%s|%s|%s", t.SourceRepo, t.SourceLicense, t.SourceFile)
return fmt.Sprintf("%x", h.Sum(nil))
}
// ComputeAppHash computes a deterministic hash of all content fields of an App.
func ComputeAppHash(a *App) string {
h := sha256.New()
fmt.Fprintf(h, "%s|%s|%s|%s|%s",
a.ID, a.Name, a.Lang, a.Domain, a.Description)
fmt.Fprintf(h, "|%s", marshalStrings(a.Tags))
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesFunctions))
fmt.Fprintf(h, "|%s", marshalStrings(a.UsesTypes))
fmt.Fprintf(h, "|%s|%s|%s|%s|%s", a.Framework, a.EntryPoint, a.Documentation, a.Notes, a.DirPath)
return fmt.Sprintf("%x", h.Sum(nil))
}
// LoadTimestamps reads existing id → {created_at, updated_at, content_hash} from all tables.
// Called before Purge so we can preserve dates across reindexing.
func (db *DB) LoadTimestamps() (funcs, types, apps map[string]timestampRecord, err error) {
funcs, err = loadTable(db, "functions")
if err != nil {
return
}
types, err = loadTable(db, "types")
if err != nil {
return
}
apps, err = loadTable(db, "apps")
return
}
func loadTable(db *DB, table string) (map[string]timestampRecord, error) {
rows, err := db.conn.Query(fmt.Sprintf("SELECT id, created_at, updated_at, content_hash FROM %s", table))
if err != nil {
return nil, err
}
defer rows.Close()
m := make(map[string]timestampRecord)
for rows.Next() {
var id, ca, ua, ch string
if err := rows.Scan(&id, &ca, &ua, &ch); err != nil {
return nil, err
}
rec := timestampRecord{ContentHash: ch}
rec.CreatedAt, _ = time.Parse(time.RFC3339, ca)
rec.UpdatedAt, _ = time.Parse(time.RFC3339, ua)
m[id] = rec
}
return m, rows.Err()
}