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() }