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>
This commit is contained in:
2026-03-30 14:23:45 +02:00
parent 250914c319
commit e349a06ad4
5 changed files with 188 additions and 24 deletions
+36 -23
View File
@@ -57,11 +57,13 @@ func unmarshalProps(s string) []PropDef {
// InsertFunction inserts or replaces a function entry.
func (db *DB) InsertFunction(f *Function) error {
now := time.Now().UTC().Format(time.RFC3339)
now := time.Now().UTC()
if f.CreatedAt.IsZero() {
f.CreatedAt = time.Now().UTC()
f.CreatedAt = now
}
if f.UpdatedAt.IsZero() {
f.UpdatedAt = now
}
f.UpdatedAt = time.Now().UTC()
if f.ID == "" {
f.ID = GenerateID(f.Name, f.Lang, f.Domain)
@@ -81,34 +83,39 @@ func (db *DB) InsertFunction(f *Function) error {
id, name, kind, lang, domain, version, purity, signature,
description, tags, uses_functions, uses_types, returns,
returns_optional, error_type, imports, example, tested,
tests, test_file_path, file_path, created_at, updated_at,
tests, test_file_path, file_path, content_hash, created_at, updated_at,
props, emits, has_state, framework, variant,
notes, documentation, code
notes, documentation, code,
source_repo, source_license, source_file
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?,
?, ?, ?
)`,
f.ID, f.Name, string(f.Kind), f.Lang, f.Domain, f.Version, string(f.Purity), f.Signature,
f.Description, marshalStrings(f.Tags), marshalStrings(f.UsesFunctions), marshalStrings(f.UsesTypes), marshalStrings(f.Returns),
f.ReturnsOptional, f.ErrorType, marshalStrings(f.Imports), f.Example, f.Tested,
marshalStrings(f.Tests), f.TestFilePath, f.FilePath, f.CreatedAt.Format(time.RFC3339), now,
marshalStrings(f.Tests), f.TestFilePath, f.FilePath, f.ContentHash, f.CreatedAt.Format(time.RFC3339), f.UpdatedAt.Format(time.RFC3339),
marshalProps(f.Props), marshalStrings(f.Emits), hasState, f.Framework, marshalStrings(f.Variant),
f.Notes, f.Documentation, f.Code,
f.SourceRepo, f.SourceLicense, f.SourceFile,
)
return err
}
// InsertType inserts or replaces a type entry.
func (db *DB) InsertType(t *Type) error {
now := time.Now().UTC().Format(time.RFC3339)
now := time.Now().UTC()
if t.CreatedAt.IsZero() {
t.CreatedAt = time.Now().UTC()
t.CreatedAt = now
}
if t.UpdatedAt.IsZero() {
t.UpdatedAt = now
}
t.UpdatedAt = time.Now().UTC()
if t.ID == "" {
t.ID = GenerateID(t.Name, t.Lang, t.Domain)
@@ -118,13 +125,15 @@ func (db *DB) InsertType(t *Type) error {
INSERT OR REPLACE INTO types (
id, name, lang, domain, version, algebraic,
definition, description, tags, uses_types,
file_path, created_at, updated_at,
examples, notes, documentation, code
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
file_path, content_hash, created_at, updated_at,
examples, notes, documentation, code,
source_repo, source_license, source_file
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
t.ID, t.Name, t.Lang, t.Domain, t.Version, string(t.Algebraic),
t.Definition, t.Description, marshalStrings(t.Tags), marshalStrings(t.UsesTypes),
t.FilePath, t.CreatedAt.Format(time.RFC3339), now,
t.FilePath, t.ContentHash, t.CreatedAt.Format(time.RFC3339), t.UpdatedAt.Format(time.RFC3339),
t.Examples, t.Notes, t.Documentation, t.Code,
t.SourceRepo, t.SourceLicense, t.SourceFile,
)
return err
}
@@ -263,11 +272,13 @@ func (db *DB) DeleteType(id string) error {
// InsertApp inserts or replaces an app entry.
func (db *DB) InsertApp(a *App) error {
now := time.Now().UTC().Format(time.RFC3339)
now := time.Now().UTC()
if a.CreatedAt.IsZero() {
a.CreatedAt = time.Now().UTC()
a.CreatedAt = now
}
if a.UpdatedAt.IsZero() {
a.UpdatedAt = now
}
a.UpdatedAt = time.Now().UTC()
if a.ID == "" {
a.ID = GenerateID(a.Name, a.Lang, a.Domain)
@@ -277,11 +288,11 @@ func (db *DB) InsertApp(a *App) error {
INSERT OR REPLACE INTO apps (
id, name, lang, domain, description, tags,
uses_functions, uses_types, framework, entry_point,
documentation, notes, dir_path, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
documentation, notes, dir_path, content_hash, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
a.Documentation, a.Notes, a.DirPath, a.CreatedAt.Format(time.RFC3339), now,
a.Documentation, a.Notes, a.DirPath, a.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
)
return err
}
@@ -347,7 +358,7 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
err := rows.Scan(
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt,
&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt, &a.ContentHash,
)
if err != nil {
return nil, fmt.Errorf("scanning app: %w", err)
@@ -391,7 +402,8 @@ func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Functio
&f.ReturnsOptional, &f.ErrorType, &importsJSON, &f.Example, &f.Tested,
&testsJSON, &f.TestFilePath, &f.FilePath, &createdAt, &updatedAt,
&propsJSON, &emitsJSON, &hasState, &f.Framework, &variantJSON,
&f.Notes, &f.Documentation, &f.Code,
&f.Notes, &f.Documentation, &f.Code, &f.ContentHash,
&f.SourceRepo, &f.SourceLicense, &f.SourceFile,
)
if err != nil {
return nil, fmt.Errorf("scanning function: %w", err)
@@ -430,7 +442,8 @@ func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error
&t.ID, &t.Name, &t.Lang, &t.Domain, &t.Version, &t.Algebraic,
&t.Definition, &t.Description, &tagsJSON, &usesTypJSON,
&t.FilePath, &createdAt, &updatedAt,
&t.Examples, &t.Notes, &t.Documentation, &t.Code,
&t.Examples, &t.Notes, &t.Documentation, &t.Code, &t.ContentHash,
&t.SourceRepo, &t.SourceLicense, &t.SourceFile,
)
if err != nil {
return nil, fmt.Errorf("scanning type: %w", err)