package db import ( "database/sql" "encoding/json" "errors" "strings" "time" "github.com/google/uuid" ) // StoredStrainCard is a persisted lineage card for a winning spread tree. type StoredStrainCard struct { ID string RootAgentID string SourceAgentID string SourceAgentName string SpreadStrain string SpreadLane string Persona string CardJSON string PeakHashrate float64 ErasureRecoveryRate float64 TreeSize int CreatedAt time.Time UpdatedAt time.Time } func (d *Database) UpsertStrainCard(rootAgentID, sourceAgentID string, cardJSON []byte, meta StoredStrainCard) (string, error) { if d == nil { return "", errors.New("database unavailable") } rootAgentID = strings.TrimSpace(rootAgentID) sourceAgentID = strings.TrimSpace(sourceAgentID) if rootAgentID == "" || sourceAgentID == "" || len(cardJSON) == 0 { return "", errors.New("strain card requires root, source, and JSON") } existing, err := d.GetStrainCardByRoot(rootAgentID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return "", err } id := uuid.NewString() now := time.Now().UTC() if existing != nil { id = existing.ID if meta.PeakHashrate <= existing.PeakHashrate && existing.PeakHashrate > 0 { return id, nil } } if meta.ID != "" { id = meta.ID } _, err = d.Exec( `INSERT INTO strain_cards ( id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane, persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(root_agent_id) DO UPDATE SET source_agent_id = excluded.source_agent_id, source_agent_name = excluded.source_agent_name, spread_strain = excluded.spread_strain, spread_lane = excluded.spread_lane, persona = excluded.persona, card_json = excluded.card_json, peak_hashrate = excluded.peak_hashrate, erasure_recovery_rate = excluded.erasure_recovery_rate, tree_size = excluded.tree_size, updated_at = excluded.updated_at WHERE excluded.peak_hashrate >= strain_cards.peak_hashrate`, id, rootAgentID, sourceAgentID, meta.SourceAgentName, meta.SpreadStrain, meta.SpreadLane, meta.Persona, string(cardJSON), meta.PeakHashrate, meta.ErasureRecoveryRate, meta.TreeSize, now.Format(time.RFC3339), now.Format(time.RFC3339), ) return id, err } func (d *Database) GetStrainCard(id string) (*StoredStrainCard, error) { id = strings.TrimSpace(id) if id == "" { return nil, sql.ErrNoRows } row := d.QueryRow( `SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane, persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at FROM strain_cards WHERE id = ?`, id, ) return scanStrainCard(row) } func (d *Database) GetStrainCardByRoot(rootAgentID string) (*StoredStrainCard, error) { rootAgentID = strings.TrimSpace(rootAgentID) if rootAgentID == "" { return nil, sql.ErrNoRows } row := d.QueryRow( `SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane, persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at FROM strain_cards WHERE root_agent_id = ?`, rootAgentID, ) return scanStrainCard(row) } func (d *Database) ListStrainCardsForAgent(agentID string) ([]StoredStrainCard, error) { agentID = strings.TrimSpace(agentID) if agentID == "" { return nil, nil } rows, err := d.Query( `SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane, persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at FROM strain_cards WHERE root_agent_id = ? OR source_agent_id = ? ORDER BY peak_hashrate DESC, updated_at DESC`, agentID, agentID, ) if err != nil { return nil, err } defer rows.Close() var out []StoredStrainCard for rows.Next() { c, err := scanStrainCardRow(rows) if err != nil { return nil, err } out = append(out, *c) } return out, rows.Err() } func (d *Database) ListStrainCards(limit int) ([]StoredStrainCard, error) { if limit <= 0 { limit = 50 } rows, err := d.Query( `SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane, persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at FROM strain_cards ORDER BY updated_at DESC LIMIT ?`, limit, ) if err != nil { return nil, err } defer rows.Close() var out []StoredStrainCard for rows.Next() { c, err := scanStrainCardRow(rows) if err != nil { return nil, err } out = append(out, *c) } return out, rows.Err() } type strainCardScanner interface { Scan(dest ...interface{}) error } func scanStrainCard(row *sql.Row) (*StoredStrainCard, error) { return scanStrainCardRow(row) } func scanStrainCardRow(row strainCardScanner) (*StoredStrainCard, error) { var c StoredStrainCard var createdAt, updatedAt string if err := row.Scan( &c.ID, &c.RootAgentID, &c.SourceAgentID, &c.SourceAgentName, &c.SpreadStrain, &c.SpreadLane, &c.Persona, &c.CardJSON, &c.PeakHashrate, &c.ErasureRecoveryRate, &c.TreeSize, &createdAt, &updatedAt, ); err != nil { return nil, err } c.CreatedAt = parseSQLiteTime(createdAt) c.UpdatedAt = parseSQLiteTime(updatedAt) return &c, nil } func parseSQLiteTime(raw string) time.Time { if t, err := time.Parse(time.RFC3339, raw); err == nil { return t } if t, err := time.Parse("2006-01-02 15:04:05", raw); err == nil { return t.UTC() } return time.Time{} } // DecodeStrainCardJSON unmarshals the stored card JSON blob. func DecodeStrainCardJSON(raw string) (map[string]interface{}, error) { out := map[string]interface{}{} if strings.TrimSpace(raw) == "" { return out, nil } err := json.Unmarshal([]byte(raw), &out) return out, err }