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
+38 -1
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
)
// IndexResult holds stats from an indexing run.
@@ -24,6 +25,12 @@ type IndexResult struct {
// Scans functions/ and types/ at the root level, plus any language-specific
// directories (e.g. python/functions/, python/types/).
func Index(db *DB, root string) (*IndexResult, error) {
// Load existing timestamps before purging so we can preserve created_at
oldFuncs, oldTypes, oldApps, err := db.LoadTimestamps()
if err != nil {
return nil, fmt.Errorf("loading timestamps: %w", err)
}
if err := db.Purge(); err != nil {
return nil, fmt.Errorf("purging database: %w", err)
}
@@ -109,12 +116,16 @@ func Index(db *DB, root string) (*IndexResult, error) {
knownTypes[t.ID] = true
}
// Pass 2: validate and insert
now := time.Now().UTC()
// Pass 2: validate, assign timestamps via hash comparison, and insert
for _, t := range types {
if verr := ValidateType(t, knownTypes); verr != nil {
result.ValidationErrors = append(result.ValidationErrors, verr.Error())
continue
}
t.ContentHash = ComputeTypeHash(t)
applyTimestamps(&t.CreatedAt, &t.UpdatedAt, t.ContentHash, oldTypes[t.ID], now)
if err := db.InsertType(t); err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", t.ID, err))
continue
@@ -127,6 +138,8 @@ func Index(db *DB, root string) (*IndexResult, error) {
result.ValidationErrors = append(result.ValidationErrors, verr.Error())
continue
}
f.ContentHash = ComputeFunctionHash(f)
applyTimestamps(&f.CreatedAt, &f.UpdatedAt, f.ContentHash, oldFuncs[f.ID], now)
if err := db.InsertFunction(f); err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", f.ID, err))
continue
@@ -139,6 +152,8 @@ func Index(db *DB, root string) (*IndexResult, error) {
result.ValidationErrors = append(result.ValidationErrors, verr.Error())
continue
}
a.ContentHash = ComputeAppHash(a)
applyTimestamps(&a.CreatedAt, &a.UpdatedAt, a.ContentHash, oldApps[a.ID], now)
if err := db.InsertApp(a); err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", a.ID, err))
continue
@@ -149,6 +164,28 @@ func Index(db *DB, root string) (*IndexResult, error) {
return result, nil
}
// applyTimestamps sets created_at and updated_at based on whether the entry
// existed before and whether its content changed.
// - New entry (no old record): both set to now
// - Unchanged (hash matches): both preserved from old record
// - Changed (hash differs): created_at preserved, updated_at set to now
func applyTimestamps(createdAt, updatedAt *time.Time, newHash string, old timestampRecord, now time.Time) {
if old.CreatedAt.IsZero() {
// New entry
*createdAt = now
*updatedAt = now
return
}
// Existing entry — always preserve created_at
*createdAt = old.CreatedAt
if old.ContentHash == newHash {
// No changes — preserve updated_at too
*updatedAt = old.UpdatedAt
} else {
*updatedAt = now
}
}
// walkMD walks a directory recursively and calls fn for each .md file found.
func walkMD(dir string, fn func(path string)) {
if _, err := os.Stat(dir); err != nil {